join って何?まずはイメージから
JOIN は、「別々のテーブルに分かれている情報を、くっつけて一つの表として見る」ための仕組みです。
現実のアプリでは、ユーザー情報と注文情報、商品情報とカテゴリ情報、などを別テーブルに分けます。
ユーザーの名前と、そのユーザーの注文内容を一緒に一覧表示したい
商品名と、その商品が属するカテゴリ名を一緒に出したい
こういうときに使うのが JOIN です。
JOIN を理解すると、「テーブルをちゃんと分けて設計しつつ、必要なときにだけ賢くくっつける」ができるようになります。
例題テーブルを決める(users と orders)
users テーブル
まず、ユーザーを表す users テーブルを用意します。
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
SQLデータを少し入れておきます。
INSERT INTO users (id, name) VALUES
(1, 'Taro'),
(2, 'Hanako'),
(3, 'Ken');
SQLorders テーブル
次に、注文を表す orders テーブルです。
どのユーザーの注文かを表すために、user_id を持たせます。
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
item TEXT NOT NULL,
price INTEGER NOT NULL
);
SQLデータを入れます。
INSERT INTO orders (id, user_id, item, price) VALUES
(1, 1, 'Book', 1200),
(2, 1, 'Pen', 200),
(3, 2, 'Bag', 3000);
SQLここまでで、
users にはユーザーの情報だけ
orders には注文の情報だけ(誰の注文かは user_id で表現)
という状態になっています。
「Taro が何を買ったか」を知りたいときに、JOIN が登場します。
INNER JOIN(いちばん基本の join)
「両方に存在する組み合わせだけ」をくっつける
INNER JOIN は、「両方のテーブルに対応するデータがある行だけをくっつける」JOIN です。
さっきの users と orders を INNER JOIN して、「ユーザー名と注文内容」を一緒に出してみます。
SELECT
users.id,
users.name,
orders.item,
orders.price
FROM users
INNER JOIN orders
ON users.id = orders.user_id;
SQLON で「どのカラム同士を結びつけるか」を指定しています。
ここでは users.id = orders.user_id です。
この結果は、こうなります。
id | name | item | price
----+---------+------+------
1 | Taro | Book | 1200
1 | Taro | Pen | 200
2 | Hanako | Bag | 3000
Ken(id=3)は注文していないので、結果には出てきません。
「両方に存在する組み合わせだけ」が出る、というのが INNER JOIN のポイントです。
Python(sqlite3)で INNER JOIN を実行してみる
同じクエリを Python から実行すると、こうなります。
import sqlite3
conn = sqlite3.connect("app.db")
cur = conn.cursor()
cur.execute(
"""
SELECT
users.id,
users.name,
orders.item,
orders.price
FROM users
INNER JOIN orders
ON users.id = orders.user_id
"""
)
rows = cur.fetchall()
for row in rows:
print(row)
conn.close()
Python出力は、さきほどの表と同じ内容になります。
JOIN 自体は SQL の仕事で、Python は「その結果を受け取る」だけです。
LEFT JOIN(片方は全部出したいとき)
「左側は全部、右側はあればくっつける」
LEFT JOIN(LEFT OUTER JOIN)は、「左側のテーブルは全部出す。右側は対応する行があればくっつける」という JOIN です。
さっきの例で、「ユーザーは全員出したい。注文がない人も含めて一覧にしたい」とします。
その場合は LEFT JOIN を使います。
SELECT
users.id,
users.name,
orders.item,
orders.price
FROM users
LEFT JOIN orders
ON users.id = orders.user_id;
SQL結果はこうなります。
id | name | item | price
----+---------+------+------
1 | Taro | Book | 1200
1 | Taro | Pen | 200
2 | Hanako | Bag | 3000
3 | Ken | NULL | NULL
Ken は注文していないので、orders 側のカラム(item, price)は NULL になります。
「左側(users)は全員出す」「右側(orders)はあればくっつく、なければ NULL」というのが LEFT JOIN です。
INNER JOIN と LEFT JOIN の違いを感覚で押さえる
INNER JOIN:
「両方にデータがある組み合わせだけ欲しいとき」
→ 注文があるユーザーだけ知りたい、など。
LEFT JOIN:
「左側のテーブルは全部出したいとき」
→ 全ユーザーの一覧と、あれば注文も一緒に見たい、など。
この 2 つの違いが腹落ちすると、JOIN の 8 割は怖くなくなります。
JOIN の「ON」が超重要な理由
ON で「どう結びつけるか」を決める
JOIN で一番大事なのは、ON で書く条件です。
... JOIN orders
ON users.id = orders.user_id
SQLここを間違えると、全く意図しない組み合わせが大量にできてしまいます。
例えば、条件を書かないと「全件×全件」の組み合わせ(デカルト積)になってしまいます。
SELECT *
FROM users, orders;
SQLこれは、users の全行と orders の全行を全部組み合わせるので、
3 人 × 3 注文 = 9 行になります(意味のない結果)。
JOIN を使うときは、
どのテーブルの
どのカラムと
どのテーブルの
どのカラムを
結びつけたいのかを、必ず意識して ON に書きます。
多くの場合は、「外部キーと主キー」の組み合わせです(orders.user_id と users.id など)。
SQLAlchemy での JOIN のイメージ(ORM 版)
モデルにリレーションを書いておく
SQLAlchemy の ORM を使うと、JOIN をあまり意識せずに書けます。
さきほどの users と orders をモデルで表現します。
from sqlalchemy import Column, Integer, String, ForeignKey, create_engine
from sqlalchemy.orm import declarative_base, relationship, sessionmaker
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
orders = relationship("Order", back_populates="user")
class Order(Base):
__tablename__ = "orders"
id = Column(Integer, primary_key=True)
item = Column(String, nullable=False)
price = Column(Integer, nullable=False)
user_id = Column(Integer, ForeignKey("users.id"))
user = relationship("User", back_populates="orders")
Pythonこれで、「User 1 人に複数の Order が紐づく」という関係をクラスに書けました。
ORM で JOIN 相当のことをする
ユーザーとその注文を一緒に扱いたいとき、
SQL を直接書かなくても、こう書けます。
engine = create_engine("sqlite:///example.db", echo=True)
Base.metadata.create_all(engine)
SessionLocal = sessionmaker(bind=engine)
session = SessionLocal()
user = session.query(User).filter_by(name="Taro").first()
for order in user.orders:
print(order.item, order.price)
session.close()
Pythonuser.orders の裏では JOIN が行われていますが、
コード上は「オブジェクト同士の関係」として書けています。
もちろん、明示的に JOIN することもできます。
from sqlalchemy import select
session = SessionLocal()
stmt = (
select(User.name, Order.item, Order.price)
.join(Order, User.id == Order.user_id)
)
for name, item, price in session.execute(stmt):
print(name, item, price)
session.close()
Pythonこの join が、SQL の JOIN に対応しています。
ORM を使うと、「JOIN の考え方はそのままに、書き味だけ Python っぽくなる」という感覚です。
まとめ(join は「分けたテーブルを必要なときだけ賢くくっつける」)
JOIN を初心者目線で整理すると、こうなります。
JOIN は、別々のテーブルにある情報を、「共通のキー(id と user_id など)」でくっつけて、一つの結果として見るための仕組み。
INNER JOIN は「両方にデータがある組み合わせだけ」、LEFT JOIN は「左側は全部、右側はあればくっつける」という違いがある。
ON で「どのカラム同士を結びつけるか」を正しく書くことが、JOIN の最重要ポイント。
ORM(SQLAlchemy)では、リレーションをモデルに書いておくと、JOIN をあまり意識せずに「オブジェクト同士の関係」として扱える。
ここから一歩進むなら、
自分で「2 テーブル以上を持つミニアプリ」(例:ユーザーとタスク、ユーザーと注文)を決めて、
そのテーブルを設計し、JOIN を使ったクエリを 3〜4 本書いてみるのが一番身につきます。
