基礎から学ぶPython入門 90日コース | データベース・Web API - Day 65:JOIN

Python 90日で身につけるPython
スポンサーリンク
スポンサーリンク

Day 65:JOIN ― 「別々のテーブルを、ひとつの景色として眺める」

Day 65では、データベースの世界で一気に視界が広がるテーマ、 JOIN(ジョイン) を扱っていきます。

キーワードはこの2つです。

  • INNER JOIN
  • LEFT JOIN

これまで、users テーブルや orders テーブルをそれぞれ単体で扱ってきました。 でも現実のシステムでは、「ユーザーと注文を一緒に見たい」「顧客と売上をまとめて眺めたい」といった場面が当たり前のように出てきます。

JOINは、そんなときに 「別々のテーブルを、ひとつの結果として結びつける」ための仕組み です。 ここを理解できると、データベースの世界がぐっと立体的に見えてきます。

JOINの世界観をつかむ ― 「2つの表を横にくっつける」イメージ

まずはテーブルを2つ用意する

Day 64で作った usersorders を使います。

  • users テーブル(ユーザー情報)
idnameemail
1Tarotaro@example.com
2Hanakohanako@example.com
3Kenken@example.com
  • orders テーブル(注文情報)
iduser_idproductprice
11Book1200
21Pen200
32Notebook500

ここで orders.user_id が、users.id を参照する 外部キー になっています。

「ユーザー名と注文内容を一緒に見たい」という欲求

例えば、こんな情報が欲しくなる場面を想像してみてください。

誰が、どの商品を、いくらで買ったのかを一覧で見たい

つまり、

  • ユーザー名(users.name
  • 商品名(orders.product
  • 価格(orders.price

を、ひとつの表として眺めたいわけです。

ここで登場するのが JOIN です。

INNER JOIN ― 「両方に共通しているものだけ結びつける」

INNER JOINの基本イメージ

INNER JOIN は、

両方のテーブルに「対応するレコードが存在するものだけ」を結びつける

という動きをします。

図でイメージすると、

  • users の丸
  • orders の丸

があって、その 重なっている部分だけを取り出す 感じです。

INNER JOINの基本SQL

ユーザーと注文を結びつける INNER JOIN は、こう書きます。

SELECT
    users.id,
    users.name,
    orders.product,
    orders.price
FROM users
INNER JOIN orders
    ON users.id = orders.user_id;
  • FROM users
    • ベースとなるテーブルを指定
  • INNER JOIN orders
    • orders テーブルを結びつける
  • ON users.id = orders.user_id
    • 「どのカラム同士を結びつけるか」を指定(結合条件)

Python+SQLiteで INNER JOIN を実行してみる

# day65_join_demo.py
import sqlite3
from pathlib import Path


def get_connection(db_name: str = "day65_example.db") -> sqlite3.Connection:
    """
    SQLiteデータベースに接続するための関数です。
    Day 64 で作ったテーブルを再利用しても構いません。
    """
    return sqlite3.connect(Path(db_name))


def create_sample_tables(conn: sqlite3.Connection) -> None:
    """
    users と orders のテーブルを作成し、
    サンプルデータを投入する関数です。
    JOINの動きを確認しやすいように、少しだけデータを入れます。
    """

    cur = conn.cursor()

    # users テーブル
    cur.execute(
        """
        CREATE TABLE IF NOT EXISTS users (
            id     INTEGER PRIMARY KEY AUTOINCREMENT,
            name   TEXT NOT NULL,
            email  TEXT NOT NULL UNIQUE
        );
        """
    )

    # orders テーブル
    cur.execute(
        """
        CREATE TABLE IF NOT EXISTS orders (
            id        INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id   INTEGER NOT NULL,
            product   TEXT NOT NULL,
            price     REAL NOT NULL,
            FOREIGN KEY (user_id) REFERENCES users(id)
        );
        """
    )

    conn.commit()

    # 既存データを一旦削除して、毎回同じ状態にします。
    cur.execute("DELETE FROM orders;")
    cur.execute("DELETE FROM users;")
    conn.commit()

    # users にサンプルデータを追加
    cur.execute("INSERT INTO users (name, email) VALUES (?, ?);", ("Taro", "taro@example.com"))
    cur.execute("INSERT INTO users (name, email) VALUES (?, ?);", ("Hanako", "hanako@example.com"))
    cur.execute("INSERT INTO users (name, email) VALUES (?, ?);", ("Ken", "ken@example.com"))

    conn.commit()

    # orders にサンプルデータを追加
    cur.execute("INSERT INTO orders (user_id, product, price) VALUES (?, ?, ?);", (1, "Book", 1200))
    cur.execute("INSERT INTO orders (user_id, product, price) VALUES (?, ?, ?);", (1, "Pen", 200))
    cur.execute("INSERT INTO orders (user_id, product, price) VALUES (?, ?, ?);", (2, "Notebook", 500))

    conn.commit()


def inner_join_users_orders(conn: sqlite3.Connection) -> None:
    """
    INNER JOIN を使って、ユーザーと注文を結びつけて表示する関数です。
    """

    print("=== INNER JOIN: ユーザーと注文を結びつけて表示します ===")

    sql = """
    SELECT
        users.id,
        users.name,
        orders.product,
        orders.price
    FROM users
    INNER JOIN orders
        ON users.id = orders.user_id;
    """

    cur = conn.cursor()
    cur.execute(sql)

    rows = cur.fetchall()

    for row in rows:
        user_id, name, product, price = row
        print(f"user_id={user_id}, name={name}, product={product}, price={price}")


def main():
    conn = get_connection()
    create_sample_tables(conn)
    inner_join_users_orders(conn)
    conn.close()


if __name__ == "__main__":
    main()
Python

このスクリプトを実行すると、 「注文を持っているユーザーだけ」が表示されます。

  • Taro(id=1)は Book と Pen を購入
  • Hanako(id=2)は Notebook を購入
  • Ken(id=3)は注文なし → INNER JOINでは出てこない

ここが、INNER JOINの大事なポイントです。

LEFT JOIN ― 「左側のテーブルを全部残しつつ、右側をくっつける」

LEFT JOINの基本イメージ

LEFT JOIN は、

左側のテーブルのレコードを「全部」残しつつ、 右側のテーブルに対応するものがあれば結びつける。 なければ右側は NULL にする。

という動きをします。

図でイメージすると、

  • 左側の丸(users)は全部残す
  • 右側の丸(orders)と重なっている部分は結びつける
  • 重なっていない左側の部分も、そのまま出てくる(右側は空)

LEFT JOINの基本SQL

ユーザーを基準にして、注文があるかどうかを見たい場合は、こう書きます。

SELECT
    users.id,
    users.name,
    orders.product,
    orders.price
FROM users
LEFT JOIN orders
    ON users.id = orders.user_id;

INNER JOINとの違いは、INNERLEFT かだけ。 でも、結果はかなり違ってきます。

Python+SQLiteで LEFT JOIN を実行してみる

def left_join_users_orders(conn: sqlite3.Connection) -> None:
    """
    LEFT JOIN を使って、ユーザーを基準に注文を結びつけて表示する関数です。
    注文がないユーザーも、NULL付きで表示されます。
    """

    print("=== LEFT JOIN: ユーザーを基準に注文を結びつけて表示します ===")

    sql = """
    SELECT
        users.id,
        users.name,
        orders.product,
        orders.price
    FROM users
    LEFT JOIN orders
        ON users.id = orders.user_id;
    """

    cur = conn.cursor()
    cur.execute(sql)

    rows = cur.fetchall()

    for row in rows:
        user_id, name, product, price = row
        print(f"user_id={user_id}, name={name}, product={product}, price={price}")
Python

main() に追加して、INNER JOINとの違いを見てみましょう。

def main():
    conn = get_connection()
    create_sample_tables(conn)

    inner_join_users_orders(conn)
    print()
    left_join_users_orders(conn)

    conn.close()
Python

結果の違いをしっかり感じてみる

  • INNER JOIN
    • 注文があるユーザーだけが表示される
    • Ken(id=3)は出てこない
  • LEFT JOIN
    • 全ユーザーが表示される
    • Ken(id=3)は、productpriceNULL で表示される

LEFT JOINは、

「左側のテーブルを基準に、右側の情報があればくっつける。なければ空欄でいいから、とにかく左側は全部見たい」

というときに使います。

INNER JOINとLEFT JOINの使い分けを感覚でつかむ

INNER JOINが向いている場面

  • 「両方に対応するデータがあるものだけ見たい」
  • 「注文があるユーザーだけ」「売上がある商品だけ」
  • 「紐づいていないデータは、今回の分析には不要」

たとえば、売上集計や「購入者だけの一覧」を作るときなどに向いています。

LEFT JOINが向いている場面

  • 「左側のテーブルを基準に、右側の状況を知りたい」
  • 「全ユーザーのうち、誰が注文していて、誰がしていないかを知りたい」
  • 「全商品リストに、売上があるかどうかをくっつけたい」

「抜け漏れなく全体を見たいけれど、右側の情報はあれば嬉しい」という場面で活躍します。

Day 65ミニテンプレート:JOINの基本セット

最後に、今日の内容をコンパクトにまとめた JOIN基礎テンプレート を載せておきます。

# day65_join_template.py
import sqlite3
from pathlib import Path


def get_connection(db_name: str = "day65_example.db") -> sqlite3.Connection:
    return sqlite3.connect(Path(db_name))


def setup(conn: sqlite3.Connection) -> None:
    cur = conn.cursor()

    cur.execute(
        """
        CREATE TABLE IF NOT EXISTS users (
            id     INTEGER PRIMARY KEY AUTOINCREMENT,
            name   TEXT NOT NULL,
            email  TEXT NOT NULL UNIQUE
        );
        """
    )

    cur.execute(
        """
        CREATE TABLE IF NOT EXISTS orders (
            id        INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id   INTEGER NOT NULL,
            product   TEXT NOT NULL,
            price     REAL NOT NULL,
            FOREIGN KEY (user_id) REFERENCES users(id)
        );
        """
    )

    conn.commit()

    # データを初期化
    cur.execute("DELETE FROM orders;")
    cur.execute("DELETE FROM users;")
    conn.commit()

    # users
    cur.execute("INSERT INTO users (name, email) VALUES (?, ?);", ("Taro", "taro@example.com"))
    cur.execute("INSERT INTO users (name, email) VALUES (?, ?);", ("Hanako", "hanako@example.com"))
    cur.execute("INSERT INTO users (name, email) VALUES (?, ?);", ("Ken", "ken@example.com"))
    conn.commit()

    # orders
    cur.execute("INSERT INTO orders (user_id, product, price) VALUES (?, ?, ?);", (1, "Book", 1200))
    cur.execute("INSERT INTO orders (user_id, product, price) VALUES (?, ?, ?);", (1, "Pen", 200))
    cur.execute("INSERT INTO orders (user_id, product, price) VALUES (?, ?, ?);", (2, "Notebook", 500))
    conn.commit()


def inner_join(conn: sqlite3.Connection) -> None:
    sql = """
    SELECT users.id, users.name, orders.product, orders.price
    FROM users
    INNER JOIN orders
        ON users.id = orders.user_id;
    """
    cur = conn.cursor()
    cur.execute(sql)
    print("=== INNER JOIN ===")
    for row in cur.fetchall():
        print(row)


def left_join(conn: sqlite3.Connection) -> None:
    sql = """
    SELECT users.id, users.name, orders.product, orders.price
    FROM users
    LEFT JOIN orders
        ON users.id = orders.user_id;
    """
    cur = conn.cursor()
    cur.execute(sql)
    print("=== LEFT JOIN ===")
    for row in cur.fetchall():
        print(row)


def main():
    conn = get_connection()
    setup(conn)
    inner_join(conn)
    print()
    left_join(conn)
    conn.close()


if __name__ == "__main__":
    main()
Python

Day 65のまとめ ― テーブル同士がつながると、データが「物語」を持ち始める

今日の主役は、

  • INNER JOIN:両方に対応するデータがあるものだけ結びつける
  • LEFT JOIN:左側のテーブルを全部残しつつ、右側をくっつける

という2つのJOINでした。

JOINを理解すると、

  • 「ユーザーと注文」
  • 「商品と売上」
  • 「記事とタグ」

といった、複数テーブルにまたがる情報を、 ひとつの結果として自然に扱えるようになります。

データがただの「点」ではなく、 関係性を持った「物語」 として見えてくる瞬間です。

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