Python | DB・SQL:join

Python
スポンサーリンク

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');
SQL

orders テーブル

次に、注文を表す 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;
SQL

ON で「どのカラム同士を結びつけるか」を指定しています。
ここでは 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()
Python

user.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 本書いてみるのが一番身につきます。

タイトルとURLをコピーしました