はじめに
リレーショナルデータベースでは、データの重複を排除し整合性を保つために、情報を複数のテーブルに分割して管理します。顧客情報、商品情報、注文情報がそれぞれ別のテーブルに格納されている場合、これらを組み合わせて「誰が何を注文したか」といった情報を取得するにはテーブル結合が必要です。
本記事では、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を実行してください。
|
|
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句の基本構文は以下の通りです。
|
|
結合条件には通常、外部キーと主キーの等価条件を指定します。
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 --> R22つのテーブルを結合する
注文テーブルと顧客テーブルをINNER JOINで結合し、顧客名を含む注文情報を取得します。
|
|
実行結果:
| 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)は、顧客テーブルに一致する行がないため結果から除外されています。
テーブルエイリアスの活用
上記の例ではoとcというエイリアス(別名)を使用しています。JOINを使用する際はエイリアスを活用することで、クエリの可読性が向上します。
|
|
JOINとWHERE句の組み合わせ
JOINとWHERE句を組み合わせることで、結合後の結果をさらに絞り込めます。
|
|
実行結果:
| 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つを結合して、完全な注文情報を取得します。
|
|
実行結果:
| 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全顧客と注文情報を取得
全ての顧客を取得し、注文がある場合はその情報も表示します。
|
|
実行結果:
| 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句を組み合わせることで、右テーブルに一致する行がないレコードを抽出できます。
|
|
実行結果:
| customer_id | customer_name | region | |
|---|---|---|---|
| 5 | 高橋美咲 | takahashi@example.com | 福岡 |
| 6 | 伊藤健太 | ito@example.com | 東京 |
この手法は、未使用のマスタデータの検出、関連データがないレコードの特定など、データ整合性のチェックに有用です。
全商品と注文状況を確認
全商品を一覧表示し、注文された商品には注文数の合計を表示します。
|
|
実行結果:
| 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の逆の動作をします。
|
|
実行結果:
| 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がよく使用されます。これは、クエリを左から右に読む際に「メインとなるテーブル」を最初に書く方が直感的だからです。
|
|
両者は全く同じ結果を返しますが、可読性の観点から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顧客と注文の完全結合
全ての顧客と全ての注文を結合し、それぞれの関連を確認します。
|
|
実行結果:
| 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種類の行が含まれています。
- 両テーブルに一致する行(通常の注文)
- 左テーブルのみに存在する行(注文履歴がない顧客:高橋美咲、伊藤健太)
- 右テーブルのみに存在する行(顧客不明の注文:order_id = 9)
FULL OUTER JOINの実践的な活用
FULL OUTER JOINは、2つのデータセット間の差異を検出する際に有用です。
|
|
CROSS JOIN(交差結合)
CROSS JOINの基本
CROSS JOINは、2つのテーブルの全ての組み合わせを生成します。結合条件を指定せず、左テーブルの各行と右テーブルの全ての行を組み合わせます。
結果の行数は「左テーブルの行数 × 右テーブルの行数」となるため、大きなテーブルに対しては慎重に使用する必要があります。
|
|
実行結果:
| region | category |
|---|---|
| 名古屋 | 家具 |
| 名古屋 | 電子機器 |
| 大阪 | 家具 |
| 大阪 | 電子機器 |
| 東京 | 家具 |
| 東京 | 電子機器 |
| 福岡 | 家具 |
| 福岡 | 電子機器 |
CROSS JOINの実践的な活用
CROSS JOINは、カレンダーテーブルの生成やレポート用の枠組み作成に活用できます。
|
|
この例では、全日付と全商品の組み合わせを作成し、実際の注文データをLEFT JOINすることで、注文がない日・商品も含めた完全なレポートを生成しています。
自己結合(Self Join)
自己結合の基本
自己結合は、同じテーブルを複数回参照して結合する手法です。階層構造(上司-部下関係など)や、同一テーブル内のデータ比較に使用します。
|
|
実行結果:
| employee_id | employee_name | department | manager_name |
|---|---|---|---|
| 1 | 山本社長 | 経営 | NULL |
| 2 | 田中部長 | 開発部 | 山本社長 |
| 3 | 佐藤課長 | 開発部 | 田中部長 |
| 4 | 鈴木主任 | 開発部 | 佐藤課長 |
| 5 | 高橋社員 | 開発部 | 佐藤課長 |
| 6 | 伊藤部長 | 営業部 | 山本社長 |
| 7 | 渡辺課長 | 営業部 | 伊藤部長 |
| 8 | 中村社員 | 営業部 | 渡辺課長 |
自己結合では、同じテーブルに異なるエイリアス(eとm)を付けることで、2つの異なるテーブルとして扱います。
組織階層の可視化
再帰CTEと自己結合を組み合わせて、組織階層を可視化できます。
|
|
実行結果:
| level | hierarchy | department | path |
|---|---|---|---|
| 1 | 山本社長 | 経営 | 山本社長 |
| 2 | 伊藤部長 | 営業部 | 山本社長 > 伊藤部長 |
| 3 | 渡辺課長 | 営業部 | 山本社長 > 伊藤部長 > 渡辺課長 |
| 4 | 中村社員 | 営業部 | 山本社長 > 伊藤部長 > 渡辺課長 > 中村社員 |
| 2 | 田中部長 | 開発部 | 山本社長 > 田中部長 |
| 3 | 佐藤課長 | 開発部 | 山本社長 > 田中部長 > 佐藤課長 |
| 4 | 鈴木主任 | 開発部 | 山本社長 > 田中部長 > 佐藤課長 > 鈴木主任 |
| 4 | 高橋社員 | 開発部 | 山本社長 > 田中部長 > 佐藤課長 > 高橋社員 |
同一テーブル内のデータ比較
自己結合を使って、同じテーブル内のデータを比較することもできます。
|
|
サブクエリとJOINの組み合わせ
相関サブクエリの代替としてのJOIN
サブクエリで実現していた処理をJOINで書き換えることで、パフォーマンスが向上する場合があります。
|
|
派生テーブル(インラインビュー)とのJOIN
サブクエリで作成した一時的なテーブル(派生テーブル)とJOINすることで、複雑な集計を行えます。
|
|
実行結果:
| 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)を使うことで、複雑なクエリをより読みやすく構造化できます。
|
|
実行結果:
| 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句を使用することもできます。
|
|
存在確認のみが目的の場合、EXISTSの方がセマンティクス(意図)が明確で、最適化されやすい傾向にあります。
JOINのパフォーマンス最適化
結合順序の考慮
JOINを複数行う場合、結果セットが小さくなる結合を先に行うとパフォーマンスが向上することがあります。PostgreSQLのオプティマイザは自動的に最適な結合順序を選択しますが、複雑なクエリでは明示的に順序を指定することも検討します。
|
|
インデックスの活用
JOIN条件で使用するカラムにインデックスを作成することで、結合処理が高速化されます。
|
|
実行計画の確認
EXPLAIN ANALYZEを使用して、JOINクエリの実行計画を確認し、ボトルネックを特定できます。
|
|
実行計画の読み方については、本シリーズの後続記事「EXPLAIN ANALYZE入門」で詳しく解説します。
まとめ
本記事では、PostgreSQLにおけるJOIN構文の基本から応用までを解説しました。
JOINを使いこなすためのポイントをまとめます。
| JOIN種類 | 用途 | 結果の特徴 |
|---|---|---|
| INNER JOIN | 両テーブルに存在するデータの取得 | 一致する行のみ |
| LEFT OUTER JOIN | 左テーブルを基準にした結合 | 左テーブルの全行を保持 |
| RIGHT OUTER JOIN | 右テーブルを基準にした結合 | 右テーブルの全行を保持 |
| FULL OUTER JOIN | 両テーブルの完全結合 | 全ての行を保持 |
| CROSS JOIN | 全組み合わせの生成 | 行数 = 左 × 右 |
| 自己結合 | 同一テーブル内の関係表現 | 階層構造、データ比較 |
適切なJOINを選択し、サブクエリやCTEと組み合わせることで、複雑なデータ取得要件にも対応できます。次のステップとして、テーブル設計やインデックスについて学ぶことで、より効率的なデータベース活用が可能になります。