はじめに

リレーショナルデータベースでは、データの重複を排除し整合性を保つために、情報を複数のテーブルに分割して管理します。顧客情報、商品情報、注文情報がそれぞれ別のテーブルに格納されている場合、これらを組み合わせて「誰が何を注文したか」といった情報を取得するにはテーブル結合が必要です。

本記事では、PostgreSQLにおけるJOIN構文の基本から応用までを解説します。INNER JOIN、LEFT/RIGHT/FULL OUTER JOIN、CROSS JOIN、自己結合、複数テーブルの結合、そしてサブクエリとの組み合わせまで、実践的なサンプルコードと図解で学んでいきます。

この記事を読むことで、以下のことができるようになります。

  • JOINの種類と動作の違いを理解し、適切な結合方法を選択できる
  • INNER JOINで共通するデータを効率的に取得できる
  • LEFT/RIGHT/FULL OUTER JOINで片側または両側にしか存在しないデータも含めて取得できる
  • 自己結合で階層構造やデータ間の関係を表現できる
  • 複数テーブルを連鎖的に結合して複雑なデータ取得ができる
  • サブクエリと組み合わせて高度なデータ抽出ができる

前提条件

  • PostgreSQL 14以降がインストールされていること
  • psqlまたはGUIツール(pgAdmin、DBeaver等)でデータベースに接続できること
  • SELECT文の基本的な構文(WHERE句、ORDER BY等)を理解していること

サンプルデータの準備

本記事で使用するサンプルテーブルとデータを準備します。以下のSQLを実行してください。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
-- 顧客テーブルの作成
CREATE TABLE customers (
    customer_id   SERIAL PRIMARY KEY,
    name          VARCHAR(100) NOT NULL,
    email         VARCHAR(255) UNIQUE,
    region        VARCHAR(50) NOT NULL,
    registered_at DATE DEFAULT CURRENT_DATE
);

-- 商品テーブルの作成
CREATE TABLE products (
    product_id   SERIAL PRIMARY KEY,
    name         VARCHAR(100) NOT NULL,
    category     VARCHAR(50) NOT NULL,
    price        INTEGER NOT NULL,
    stock        INTEGER DEFAULT 0
);

-- 注文テーブルの作成
CREATE TABLE orders (
    order_id     SERIAL PRIMARY KEY,
    customer_id  INTEGER REFERENCES customers(customer_id),
    product_id   INTEGER REFERENCES products(product_id),
    quantity     INTEGER NOT NULL,
    order_date   DATE NOT NULL
);

-- 社員テーブルの作成(自己結合用)
CREATE TABLE employees (
    employee_id   SERIAL PRIMARY KEY,
    name          VARCHAR(100) NOT NULL,
    department    VARCHAR(50) NOT NULL,
    manager_id    INTEGER REFERENCES employees(employee_id)
);

-- 顧客データの挿入
INSERT INTO customers (name, email, region) VALUES
('田中太郎', 'tanaka@example.com', '東京'),
('山田花子', 'yamada@example.com', '大阪'),
('佐藤次郎', 'sato@example.com', '東京'),
('鈴木一郎', 'suzuki@example.com', '名古屋'),
('高橋美咲', 'takahashi@example.com', '福岡'),
('伊藤健太', 'ito@example.com', '東京');

-- 商品データの挿入
INSERT INTO products (name, category, price, stock) VALUES
('ノートPC', '電子機器', 120000, 25),
('マウス', '電子機器', 3500, 150),
('キーボード', '電子機器', 8000, 80),
('デスク', '家具', 45000, 12),
('オフィスチェア', '家具', 35000, 20),
('モニター', '電子機器', 55000, 30),
('ヘッドセット', '電子機器', 12000, 45),
('スタンディングデスク', '家具', 68000, 5);

-- 注文データの挿入
INSERT INTO orders (customer_id, product_id, quantity, order_date) VALUES
(1, 1, 2, '2025-12-01'),
(2, 2, 5, '2025-12-01'),
(1, 3, 3, '2025-12-02'),
(3, 1, 1, '2025-12-03'),
(4, 4, 1, '2025-12-03'),
(1, 6, 3, '2025-12-05'),
(2, 2, 10, '2025-12-05'),
(3, 7, 4, '2025-12-06'),
(NULL, 5, 2, '2025-12-07'),  -- 顧客不明の注文
(1, 3, 2, '2025-12-10');

-- 社員データの挿入(自己結合用)
INSERT INTO employees (name, department, manager_id) VALUES
('山本社長', '経営', NULL),
('田中部長', '開発部', 1),
('佐藤課長', '開発部', 2),
('鈴木主任', '開発部', 3),
('高橋社員', '開発部', 3),
('伊藤部長', '営業部', 1),
('渡辺課長', '営業部', 6),
('中村社員', '営業部', 7);

JOINの基本概念

なぜJOINが必要なのか

リレーショナルデータベースでは、データの正規化によってテーブルを分割します。例えば、注文情報に顧客名を直接格納するのではなく、customer_idという外部キーで顧客テーブルを参照します。この設計により、顧客名が変更された場合も1箇所の更新で済みますが、データを取得する際にはテーブルを結合する必要があります。

erDiagram
    customers ||--o{ orders : "places"
    products ||--o{ orders : "ordered"
    customers {
        int customer_id PK
        string name
        string email
        string region
    }
    products {
        int product_id PK
        string name
        string category
        int price
    }
    orders {
        int order_id PK
        int customer_id FK
        int product_id FK
        int quantity
        date order_date
    }

JOINの種類と概要

PostgreSQLで使用できる主なJOINの種類は以下の通りです。

JOIN種類 説明 結果に含まれる行
INNER JOIN 両方のテーブルに存在するデータのみ 結合条件に一致する行のみ
LEFT OUTER JOIN 左テーブルの全データ + 右テーブルの一致データ 左テーブルの全行(右に一致がなければNULL)
RIGHT OUTER JOIN 右テーブルの全データ + 左テーブルの一致データ 右テーブルの全行(左に一致がなければNULL)
FULL OUTER JOIN 両テーブルの全データ 両テーブルの全行(一致がなければNULL)
CROSS JOIN 全組み合わせ(直積) 行数 = 左テーブル行数 × 右テーブル行数

JOINの基本構文

JOIN句の基本構文は以下の通りです。

1
2
3
SELECT カラムリスト
FROM テーブル1
JOIN種類 テーブル2 ON 結合条件;

結合条件には通常、外部キーと主キーの等価条件を指定します。

INNER JOIN(内部結合)

INNER JOINの基本

INNER JOINは最も基本的な結合方法で、両方のテーブルに存在するデータのみを取得します。結合条件に一致しない行は結果から除外されます。

flowchart LR
    subgraph 左テーブル
        A1["データA"]
        A2["データB"]
        A3["データC"]
    end
    subgraph 右テーブル
        B1["データA"]
        B2["データB"]
        B3["データD"]
    end
    subgraph INNER JOIN結果
        R1["データA"]
        R2["データB"]
    end
    A1 --> R1
    A2 --> R2
    B1 --> R1
    B2 --> R2

2つのテーブルを結合する

注文テーブルと顧客テーブルをINNER JOINで結合し、顧客名を含む注文情報を取得します。

1
2
3
4
5
6
7
8
9
-- 注文情報に顧客名を結合
SELECT
    o.order_id,
    c.name AS customer_name,
    o.product_id,
    o.quantity,
    o.order_date
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id;

実行結果:

order_id customer_name product_id quantity order_date
1 田中太郎 1 2 2025-12-01
2 山田花子 2 5 2025-12-01
3 田中太郎 3 3 2025-12-02
4 佐藤次郎 1 1 2025-12-03
5 鈴木一郎 4 1 2025-12-03
6 田中太郎 6 3 2025-12-05
7 山田花子 2 10 2025-12-05
8 佐藤次郎 7 4 2025-12-06
10 田中太郎 3 2 2025-12-10

order_id = 9の注文(customer_id = NULL)は、顧客テーブルに一致する行がないため結果から除外されています。

テーブルエイリアスの活用

上記の例ではocというエイリアス(別名)を使用しています。JOINを使用する際はエイリアスを活用することで、クエリの可読性が向上します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
-- エイリアスなしの場合(冗長)
SELECT
    orders.order_id,
    customers.name,
    orders.quantity
FROM orders
INNER JOIN customers ON orders.customer_id = customers.customer_id;

-- エイリアスありの場合(簡潔)
SELECT
    o.order_id,
    c.name,
    o.quantity
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id;

JOINとWHERE句の組み合わせ

JOINとWHERE句を組み合わせることで、結合後の結果をさらに絞り込めます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-- 東京の顧客による注文のみを取得
SELECT
    o.order_id,
    c.name AS customer_name,
    c.region,
    o.quantity,
    o.order_date
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id
WHERE c.region = '東京'
ORDER BY o.order_date;

実行結果:

order_id customer_name region quantity order_date
1 田中太郎 東京 2 2025-12-01
3 田中太郎 東京 3 2025-12-02
4 佐藤次郎 東京 1 2025-12-03
6 田中太郎 東京 3 2025-12-05
8 佐藤次郎 東京 4 2025-12-06
10 田中太郎 東京 2 2025-12-10

3つのテーブルを結合する

注文テーブル、顧客テーブル、商品テーブルの3つを結合して、完全な注文情報を取得します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
-- 顧客名と商品名を含む注文情報
SELECT
    o.order_id,
    c.name AS customer_name,
    p.name AS product_name,
    p.category,
    o.quantity,
    p.price,
    o.quantity * p.price AS subtotal,
    o.order_date
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id
INNER JOIN products p ON o.product_id = p.product_id
ORDER BY o.order_date, o.order_id;

実行結果:

order_id customer_name product_name category quantity price subtotal order_date
1 田中太郎 ノートPC 電子機器 2 120000 240000 2025-12-01
2 山田花子 マウス 電子機器 5 3500 17500 2025-12-01
3 田中太郎 キーボード 電子機器 3 8000 24000 2025-12-02
4 佐藤次郎 ノートPC 電子機器 1 120000 120000 2025-12-03
5 鈴木一郎 デスク 家具 1 45000 45000 2025-12-03

LEFT OUTER JOIN(左外部結合)

LEFT OUTER JOINの基本

LEFT OUTER JOIN(またはLEFT JOIN)は、左テーブルの全ての行を結果に含め、右テーブルに一致する行がない場合はNULLで埋めます。「左テーブルを基準に、関連データがあれば結合する」というユースケースで使用します。

flowchart LR
    subgraph 左テーブル
        A1["データA"]
        A2["データB"]
        A3["データC"]
    end
    subgraph 右テーブル
        B1["データA"]
        B2["データB"]
        B3["データD"]
    end
    subgraph LEFT JOIN結果
        R1["データA + 右A"]
        R2["データB + 右B"]
        R3["データC + NULL"]
    end
    A1 --> R1
    A2 --> R2
    A3 --> R3
    B1 --> R1
    B2 --> R2

全顧客と注文情報を取得

全ての顧客を取得し、注文がある場合はその情報も表示します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-- 全顧客と注文情報(注文がない顧客も含む)
SELECT
    c.customer_id,
    c.name AS customer_name,
    c.region,
    o.order_id,
    o.quantity,
    o.order_date
FROM customers c
LEFT OUTER JOIN orders o ON c.customer_id = o.customer_id
ORDER BY c.customer_id, o.order_date;

実行結果:

customer_id customer_name region order_id quantity order_date
1 田中太郎 東京 1 2 2025-12-01
1 田中太郎 東京 3 3 2025-12-02
1 田中太郎 東京 6 3 2025-12-05
1 田中太郎 東京 10 2 2025-12-10
2 山田花子 大阪 2 5 2025-12-01
2 山田花子 大阪 7 10 2025-12-05
3 佐藤次郎 東京 4 1 2025-12-03
3 佐藤次郎 東京 8 4 2025-12-06
4 鈴木一郎 名古屋 5 1 2025-12-03
5 高橋美咲 福岡 NULL NULL NULL
6 伊藤健太 東京 NULL NULL NULL

高橋美咲と伊藤健太は注文履歴がないため、注文関連カラムがNULLになっています。INNER JOINでは表示されない行がLEFT JOINでは含まれます。

注文がない顧客を抽出

LEFT JOINとWHERE句を組み合わせることで、右テーブルに一致する行がないレコードを抽出できます。

1
2
3
4
5
6
7
8
9
-- 注文履歴がない顧客を抽出
SELECT
    c.customer_id,
    c.name AS customer_name,
    c.email,
    c.region
FROM customers c
LEFT OUTER JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;

実行結果:

customer_id customer_name email region
5 高橋美咲 takahashi@example.com 福岡
6 伊藤健太 ito@example.com 東京

この手法は、未使用のマスタデータの検出、関連データがないレコードの特定など、データ整合性のチェックに有用です。

全商品と注文状況を確認

全商品を一覧表示し、注文された商品には注文数の合計を表示します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
-- 全商品と注文状況
SELECT
    p.product_id,
    p.name AS product_name,
    p.category,
    p.price,
    COALESCE(SUM(o.quantity), 0) AS total_ordered,
    p.stock
FROM products p
LEFT OUTER JOIN orders o ON p.product_id = o.product_id
GROUP BY p.product_id, p.name, p.category, p.price, p.stock
ORDER BY total_ordered DESC;

実行結果:

product_id product_name category price total_ordered stock
2 マウス 電子機器 3500 15 150
3 キーボード 電子機器 8000 5 80
7 ヘッドセット 電子機器 12000 4 45
1 ノートPC 電子機器 120000 3 25
6 モニター 電子機器 55000 3 30
5 オフィスチェア 家具 35000 2 20
4 デスク 家具 45000 1 12
8 スタンディングデスク 家具 68000 0 5

COALESCE関数を使用することで、注文がない商品(SUM結果がNULL)を0として表示しています。

RIGHT OUTER JOIN(右外部結合)

RIGHT OUTER JOINの基本

RIGHT OUTER JOIN(またはRIGHT JOIN)は、右テーブルの全ての行を結果に含め、左テーブルに一致する行がない場合はNULLで埋めます。LEFT JOINの逆の動作をします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-- 全注文と顧客情報(顧客不明の注文も含む)
SELECT
    o.order_id,
    o.customer_id,
    c.name AS customer_name,
    o.product_id,
    o.quantity,
    o.order_date
FROM customers c
RIGHT OUTER JOIN orders o ON c.customer_id = o.customer_id
ORDER BY o.order_date;

実行結果:

order_id customer_id customer_name product_id quantity order_date
1 1 田中太郎 1 2 2025-12-01
2 2 山田花子 2 5 2025-12-01
3 1 田中太郎 3 3 2025-12-02
4 3 佐藤次郎 1 1 2025-12-03
5 4 鈴木一郎 4 1 2025-12-03
6 1 田中太郎 6 3 2025-12-05
7 2 山田花子 2 10 2025-12-05
8 3 佐藤次郎 7 4 2025-12-06
9 NULL NULL 5 2 2025-12-07
10 1 田中太郎 3 2 2025-12-10

order_id = 9は顧客情報がNULLの注文ですが、RIGHT JOINにより結果に含まれています。

LEFT JOINとRIGHT JOINの使い分け

実務ではRIGHT JOINよりもLEFT JOINがよく使用されます。これは、クエリを左から右に読む際に「メインとなるテーブル」を最初に書く方が直感的だからです。

1
2
3
4
5
6
7
8
9
-- RIGHT JOINで書いた場合
SELECT o.*, c.name
FROM customers c
RIGHT JOIN orders o ON c.customer_id = o.customer_id;

-- 同等のLEFT JOINで書き換え(こちらが一般的)
SELECT o.*, c.name
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.customer_id;

両者は全く同じ結果を返しますが、可読性の観点からLEFT JOINへの統一を推奨します。

FULL OUTER JOIN(完全外部結合)

FULL OUTER JOINの基本

FULL OUTER JOIN(またはFULL JOIN)は、両方のテーブルの全ての行を結果に含めます。一致する行がない場合は、該当するカラムがNULLになります。

flowchart LR
    subgraph 左テーブル
        A1["データA"]
        A2["データB"]
        A3["データC"]
    end
    subgraph 右テーブル
        B1["データA"]
        B2["データB"]
        B3["データD"]
    end
    subgraph FULL JOIN結果
        R1["データA + 右A"]
        R2["データB + 右B"]
        R3["データC + NULL"]
        R4["NULL + データD"]
    end
    A1 --> R1
    A2 --> R2
    A3 --> R3
    B1 --> R1
    B2 --> R2
    B3 --> R4

顧客と注文の完全結合

全ての顧客と全ての注文を結合し、それぞれの関連を確認します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
-- 顧客と注文の完全結合
SELECT
    c.customer_id,
    c.name AS customer_name,
    o.order_id,
    o.product_id,
    o.order_date
FROM customers c
FULL OUTER JOIN orders o ON c.customer_id = o.customer_id
ORDER BY c.customer_id NULLS LAST, o.order_date;

実行結果:

customer_id customer_name order_id product_id order_date
1 田中太郎 1 1 2025-12-01
1 田中太郎 3 3 2025-12-02
1 田中太郎 6 6 2025-12-05
1 田中太郎 10 3 2025-12-10
2 山田花子 2 2 2025-12-01
2 山田花子 7 2 2025-12-05
3 佐藤次郎 4 1 2025-12-03
3 佐藤次郎 8 7 2025-12-06
4 鈴木一郎 5 4 2025-12-03
5 高橋美咲 NULL NULL NULL
6 伊藤健太 NULL NULL NULL
NULL NULL 9 5 2025-12-07

この結果には以下の3種類の行が含まれています。

  1. 両テーブルに一致する行(通常の注文)
  2. 左テーブルのみに存在する行(注文履歴がない顧客:高橋美咲、伊藤健太)
  3. 右テーブルのみに存在する行(顧客不明の注文:order_id = 9)

FULL OUTER JOINの実践的な活用

FULL OUTER JOINは、2つのデータセット間の差異を検出する際に有用です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
-- 両方に存在/片方のみに存在するデータを分類
SELECT
    c.customer_id,
    c.name AS customer_name,
    o.order_id,
    CASE
        WHEN c.customer_id IS NULL THEN '顧客不明の注文'
        WHEN o.order_id IS NULL THEN '注文なしの顧客'
        ELSE '正常な注文'
    END AS status
FROM customers c
FULL OUTER JOIN orders o ON c.customer_id = o.customer_id
ORDER BY status, c.customer_id;

CROSS JOIN(交差結合)

CROSS JOINの基本

CROSS JOINは、2つのテーブルの全ての組み合わせを生成します。結合条件を指定せず、左テーブルの各行と右テーブルの全ての行を組み合わせます。

結果の行数は「左テーブルの行数 × 右テーブルの行数」となるため、大きなテーブルに対しては慎重に使用する必要があります。

1
2
3
4
5
6
7
-- 地域と商品カテゴリの全組み合わせ
SELECT DISTINCT
    c.region,
    p.category
FROM customers c
CROSS JOIN products p
ORDER BY c.region, p.category;

実行結果:

region category
名古屋 家具
名古屋 電子機器
大阪 家具
大阪 電子機器
東京 家具
東京 電子機器
福岡 家具
福岡 電子機器

CROSS JOINの実践的な活用

CROSS JOINは、カレンダーテーブルの生成やレポート用の枠組み作成に活用できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
-- 日付と商品の売上レポート用枠組み
WITH date_range AS (
    SELECT generate_series(
        '2025-12-01'::DATE,
        '2025-12-10'::DATE,
        '1 day'::INTERVAL
    )::DATE AS report_date
)
SELECT
    d.report_date,
    p.product_id,
    p.name AS product_name,
    COALESCE(SUM(o.quantity), 0) AS daily_quantity
FROM date_range d
CROSS JOIN products p
LEFT JOIN orders o ON d.report_date = o.order_date AND p.product_id = o.product_id
GROUP BY d.report_date, p.product_id, p.name
ORDER BY d.report_date, p.product_id
LIMIT 16;

この例では、全日付と全商品の組み合わせを作成し、実際の注文データをLEFT JOINすることで、注文がない日・商品も含めた完全なレポートを生成しています。

自己結合(Self Join)

自己結合の基本

自己結合は、同じテーブルを複数回参照して結合する手法です。階層構造(上司-部下関係など)や、同一テーブル内のデータ比較に使用します。

1
2
3
4
5
6
7
8
9
-- 社員と上司の関係を表示
SELECT
    e.employee_id,
    e.name AS employee_name,
    e.department,
    m.name AS manager_name
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.employee_id
ORDER BY e.employee_id;

実行結果:

employee_id employee_name department manager_name
1 山本社長 経営 NULL
2 田中部長 開発部 山本社長
3 佐藤課長 開発部 田中部長
4 鈴木主任 開発部 佐藤課長
5 高橋社員 開発部 佐藤課長
6 伊藤部長 営業部 山本社長
7 渡辺課長 営業部 伊藤部長
8 中村社員 営業部 渡辺課長

自己結合では、同じテーブルに異なるエイリアス(em)を付けることで、2つの異なるテーブルとして扱います。

組織階層の可視化

再帰CTEと自己結合を組み合わせて、組織階層を可視化できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
-- 再帰CTEで組織階層を展開
WITH RECURSIVE org_tree AS (
    -- 基底ケース:トップレベル(manager_idがNULL)
    SELECT
        employee_id,
        name,
        department,
        manager_id,
        1 AS level,
        name AS path
    FROM employees
    WHERE manager_id IS NULL

    UNION ALL

    -- 再帰ケース:部下を追加
    SELECT
        e.employee_id,
        e.name,
        e.department,
        e.manager_id,
        t.level + 1,
        t.path || ' > ' || e.name
    FROM employees e
    INNER JOIN org_tree t ON e.manager_id = t.employee_id
)
SELECT
    level,
    REPEAT('  ', level - 1) || name AS hierarchy,
    department,
    path
FROM org_tree
ORDER BY path;

実行結果:

level hierarchy department path
1 山本社長 経営 山本社長
2 伊藤部長 営業部 山本社長 > 伊藤部長
3 渡辺課長 営業部 山本社長 > 伊藤部長 > 渡辺課長
4 中村社員 営業部 山本社長 > 伊藤部長 > 渡辺課長 > 中村社員
2 田中部長 開発部 山本社長 > 田中部長
3 佐藤課長 開発部 山本社長 > 田中部長 > 佐藤課長
4 鈴木主任 開発部 山本社長 > 田中部長 > 佐藤課長 > 鈴木主任
4 高橋社員 開発部 山本社長 > 田中部長 > 佐藤課長 > 高橋社員

同一テーブル内のデータ比較

自己結合を使って、同じテーブル内のデータを比較することもできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
-- 同じカテゴリで自分より高い商品を持つ商品を列挙
SELECT
    p1.name AS product_name,
    p1.category,
    p1.price,
    p2.name AS higher_priced_product,
    p2.price AS higher_price
FROM products p1
INNER JOIN products p2 ON p1.category = p2.category AND p1.price < p2.price
ORDER BY p1.category, p1.price;

サブクエリとJOINの組み合わせ

相関サブクエリの代替としてのJOIN

サブクエリで実現していた処理をJOINで書き換えることで、パフォーマンスが向上する場合があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
-- サブクエリ版:各顧客の最新注文を取得
SELECT
    c.customer_id,
    c.name,
    (
        SELECT MAX(o.order_date)
        FROM orders o
        WHERE o.customer_id = c.customer_id
    ) AS latest_order_date
FROM customers c;

-- JOIN版:同じ結果をJOINで取得(効率的)
SELECT
    c.customer_id,
    c.name,
    MAX(o.order_date) AS latest_order_date
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.name;

派生テーブル(インラインビュー)とのJOIN

サブクエリで作成した一時的なテーブル(派生テーブル)とJOINすることで、複雑な集計を行えます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
-- 顧客ごとの注文統計と詳細を結合
SELECT
    c.customer_id,
    c.name AS customer_name,
    c.region,
    stats.order_count,
    stats.total_quantity,
    stats.total_amount
FROM customers c
INNER JOIN (
    SELECT
        o.customer_id,
        COUNT(*) AS order_count,
        SUM(o.quantity) AS total_quantity,
        SUM(o.quantity * p.price) AS total_amount
    FROM orders o
    INNER JOIN products p ON o.product_id = p.product_id
    GROUP BY o.customer_id
) stats ON c.customer_id = stats.customer_id
ORDER BY stats.total_amount DESC;

実行結果:

customer_id customer_name region order_count total_quantity total_amount
1 田中太郎 東京 4 10 469000
3 佐藤次郎 東京 2 5 168000
2 山田花子 大阪 2 15 52500
4 鈴木一郎 名古屋 1 1 45000

CTEを使ったJOIN

WITH句(共通テーブル式、CTE)を使うことで、複雑なクエリをより読みやすく構造化できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-- CTEを使って顧客ランキングを作成
WITH customer_orders AS (
    SELECT
        o.customer_id,
        COUNT(*) AS order_count,
        SUM(o.quantity) AS total_quantity
    FROM orders o
    WHERE o.customer_id IS NOT NULL
    GROUP BY o.customer_id
),
customer_ranking AS (
    SELECT
        customer_id,
        order_count,
        total_quantity,
        RANK() OVER (ORDER BY total_quantity DESC) AS quantity_rank
    FROM customer_orders
)
SELECT
    c.name AS customer_name,
    c.region,
    cr.order_count,
    cr.total_quantity,
    cr.quantity_rank
FROM customer_ranking cr
INNER JOIN customers c ON cr.customer_id = c.customer_id
ORDER BY cr.quantity_rank;

実行結果:

customer_name region order_count total_quantity quantity_rank
山田花子 大阪 2 15 1
田中太郎 東京 4 10 2
佐藤次郎 東京 2 5 3
鈴木一郎 名古屋 1 1 4

EXISTS句とJOINの使い分け

関連データの存在確認には、JOINの代わりにEXISTS句を使用することもできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-- JOIN版:注文履歴がある顧客を取得
SELECT DISTINCT c.customer_id, c.name, c.region
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id;

-- EXISTS版:同じ結果(DISTINCTが不要で効率的な場合がある)
SELECT c.customer_id, c.name, c.region
FROM customers c
WHERE EXISTS (
    SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id
);

存在確認のみが目的の場合、EXISTSの方がセマンティクス(意図)が明確で、最適化されやすい傾向にあります。

JOINのパフォーマンス最適化

結合順序の考慮

JOINを複数行う場合、結果セットが小さくなる結合を先に行うとパフォーマンスが向上することがあります。PostgreSQLのオプティマイザは自動的に最適な結合順序を選択しますが、複雑なクエリでは明示的に順序を指定することも検討します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
-- WHERE句で絞り込んでから結合(効率的)
SELECT
    c.name AS customer_name,
    p.name AS product_name,
    o.quantity
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id
INNER JOIN products p ON o.product_id = p.product_id
WHERE o.order_date >= '2025-12-05'
  AND c.region = '東京';

インデックスの活用

JOIN条件で使用するカラムにインデックスを作成することで、結合処理が高速化されます。

1
2
3
4
5
6
-- 外部キーにインデックスを作成
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
CREATE INDEX idx_orders_product_id ON orders(product_id);

-- 複合インデックスの例
CREATE INDEX idx_orders_date_customer ON orders(order_date, customer_id);

実行計画の確認

EXPLAIN ANALYZEを使用して、JOINクエリの実行計画を確認し、ボトルネックを特定できます。

1
2
3
4
5
6
7
8
9
EXPLAIN ANALYZE
SELECT
    c.name AS customer_name,
    p.name AS product_name,
    o.quantity
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id
INNER JOIN products p ON o.product_id = p.product_id
WHERE o.order_date >= '2025-12-05';

実行計画の読み方については、本シリーズの後続記事「EXPLAIN ANALYZE入門」で詳しく解説します。

まとめ

本記事では、PostgreSQLにおけるJOIN構文の基本から応用までを解説しました。

JOINを使いこなすためのポイントをまとめます。

JOIN種類 用途 結果の特徴
INNER JOIN 両テーブルに存在するデータの取得 一致する行のみ
LEFT OUTER JOIN 左テーブルを基準にした結合 左テーブルの全行を保持
RIGHT OUTER JOIN 右テーブルを基準にした結合 右テーブルの全行を保持
FULL OUTER JOIN 両テーブルの完全結合 全ての行を保持
CROSS JOIN 全組み合わせの生成 行数 = 左 × 右
自己結合 同一テーブル内の関係表現 階層構造、データ比較

適切なJOINを選択し、サブクエリやCTEと組み合わせることで、複雑なデータ取得要件にも対応できます。次のステップとして、テーブル設計やインデックスについて学ぶことで、より効率的なデータベース活用が可能になります。

参考リンク