PostgreSQL: PostgreSQLの集合演算と複合クエリ
最終更新:2026-08-26
1. 学習目標
- UNION(重複除去マージ)
- UNION ALL(重複保持マージ)
- INTERSECT / INTERSECT ALL(積集合)
- EXCEPT / EXCEPT ALL(差集合)
- 集合演算でのORDER BY
- 集合演算でのNULL処理
- 集合演算とJOINの選択
2. ストーリー
BobはECプラットフォームのデータアナリストです。CEOから3つの質問を受けました。
- Q1、Q2、Q3で最も売れた商品は?(UNION ALLでマージ)
- すべての四半期で売れ続けた商品は?(INTERSECT = 常緑商品)
- Q1では売れたがQ2では売れなかった商品は?(EXCEPT = 離脱商品)
Bobはこれらの質問がSQLの3つの集合演算——UNION、INTERSECT、EXCEPTに正確に対応していることに気づきました。
3. 概念
(1) 集合演算の概要
| 演算 | 意味 | 重複除去 | 類推 |
|---|---|---|---|
| UNION | 結果セットのマージ | はい | A ∪ B |
| UNION ALL | 結果セットのマージ | いいえ | A ∪ B(重複あり) |
| INTERSECT | 積集合 | はい | A ∩ B |
| INTERSECT ALL | 積集合 | いいえ | A ∩ B(重複カウントあり) |
| EXCEPT | AにあってBにないもの | はい | A - B |
| EXCEPT ALL | AにあってBにないもの | いいえ | A - B(重複カウントあり) |
flowchart TD
subgraph Union
U1((A)) --- U2((B))
U1 & U2 --> U3["A ∪ B"]
end
subgraph Intersect
I1((A)) --- I2((B))
I1 ∩ I2 --> I3["A ∩ B"]
end
subgraph Except
E1((A)) --- E2((B))
E1 - E2 --> E3["A - B"]
end
style U3 fill:#c8e6c9
style I3 fill:#e1f5fe
style E3 fill:#fff9c4
(2) UNIONとUNION ALL
▶ サンプル: UNIONで3四半期のベストセラーをマージ
SELECT product_id, product_name FROM hot_products_q1
UNION
SELECT product_id, product_name FROM hot_products_q2
UNION
SELECT product_id, product_name FROM hot_products_q3
ORDER BY product_id;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
UNIONは自動的に重複を除去します。��品がQ1とQ2の両方でベストセラーの場合、1回だけ表示されます。
▶ サンプル: UNION ALLで重複を保持
SELECT product_id, product_name, 'Q1' AS quarter FROM hot_products_q1
UNION ALL
SELECT product_id, product_name, 'Q2' AS quarter FROM hot_products_q2
UNION ALL
SELECT product_id, product_name, 'Q3' AS quarter FROM hot_products_q3
ORDER BY quarter, product_id;
product_id | product_name | quarter
------------+--------------+---------
101 | Widget Pro | Q1
102 | Gadget Mini | Q1
101 | Widget Pro | Q2
103 | Server Rack | Q2
101 | Widget Pro | Q3
104 | Cable Max | Q3
| シナリオ | 推奨 | 理由 |
|---|---|---|
| 重複のない異なるソースをマージ | UNION ALL | 重複除去不要で高速 |
| 重複の可能性があり除去が必要 | UNION | 自動重複除去 |
| ソースをタグ付けしてマージ | UNION ALL + タグカラム | 重複を保持しソースを区別 |
▶ サンプル: UNION ALLで複数テーブルの統計をマージ
SELECT 'NA' AS region, COUNT(*) AS order_count, SUM(amount) AS total FROM orders_na
UNION ALL
SELECT 'EU', COUNT(*), SUM(amount) FROM orders_eu
UNION ALL
SELECT 'APAC', COUNT(*), SUM(amount) FROM orders_apac;
Output:
count
-------
5
(1 row)
(3) INTERSECTとINTERSECT ALL
▶ サンプル: 3四半期すべてで売れた常緑商品を検索
SELECT product_id, product_name FROM hot_products_q1
INTERSECT
SELECT product_id, product_name FROM hot_products_q2
INTERSECT
SELECT product_id, product_name FROM hot_products_q3;
product_id | product_name
------------+--------------
101 | Widget Pro
Widget Proが唯一、毎四半期ベストセラーだった常緑商品です。
▶ サンプル: INTERSECT ALLで重複カウントを保持
商品101がQ1のベストセラーリストに2回、Q2に1回、Q3に3回出現するとします。
SELECT product_id FROM hot_products_q1_detail
INTERSECT ALL
SELECT product_id FROM hot_products_q2_detail;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
INTERSECT ALLはMIN(出現回数)を返します。商品101はmin(2, 1) = 1行を返します。
| 演算 | 重複除去 | 重複行の処理 | 典型的な用途 |
|---|---|---|---|
| INTERSECT | はい | 1行のみ保持 | 共通項目の検索 |
| INTERSECT ALL | いいえ | 最小出現回数で保持 | 重複頻度の正確な一致 |
▶ サンプル: INTERSECTで複数年共通の顧客を検索
SELECT customer_id FROM orders WHERE EXTRACT(YEAR FROM created_at) = 2023
INTERSECT
SELECT customer_id FROM orders WHERE EXTRACT(YEAR FROM created_at) = 2024
INTERSECT
SELECT customer_id FROM orders WHERE EXTRACT(YEAR FROM created_at) = 2025;
Output:
CREATE TABLE
3年連続で注文したロイヤル顧客です。
(4) EXCEPTとEXCEPT ALL
▶ サンプル: Q1ベストセラーでQ2に入らなかった商品(離脱)
SELECT product_id, product_name FROM hot_products_q1
EXCEPT
SELECT product_id, product_name FROM hot_products_q2;
product_id | product_name
------------+--------------
102 | Gadget Mini
Gadget MiniはQ1のベストセラーでしたが、Q2リストには入らず——離脱しました。
▶ サンプル: Q2で新たにベストセラー入りした商品
SELECT product_id, product_name FROM hot_products_q2
EXCEPT
SELECT product_id, product_name FROM hot_products_q1;
product_id | product_name
------------+--------------
103 | Server Rack
Server RackはQ2で新たにベストセラーリスト入りしました。
▶ サンプル: EXCEPT ALLで重複カウントを保持
SELECT product_id FROM order_items_2024
EXCEPT ALL
SELECT product_id FROM order_items_2025;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
商品101が2024年に5回、2025年に3回出現する場合、EXCEPT ALLは5 - 3 = 2行を返します。
| 演算 | 重複除去 | 重複行の処理 | 典型的な用途 |
|---|---|---|---|
| EXCEPT | はい | 1行のみ保持 | 差分の検索 |
| EXCEPT ALL | いいえ | 出現回数の差で保持 | 正確な余剰カウントの計算 |
4. 重要なポイント
(1) 集合演算のルール
ルール1: カラム数が同じである必要があります。
SELECT id, name FROM table_a
UNION
SELECT id, name, price FROM table_b;
ERROR: each UNION query must have the same number of columns
ルール2: 対応するカラムの型が互換性を持つ必要があります。
| ルール | 要件 | 違反時の結果 |
|---|---|---|
| カラム数 | 同じであること | コンパイルエラー |
| カラム型 | 互換性があること | 暗黙的キャストまたはエラー |
| カラム名 | 最初のクエリのカラム名が採用される | エイリアスに注意 |
▶ サンプル: エイリアスでカラム名を統一
SELECT product_id, product_name AS name FROM products_active
UNION ALL
SELECT sku AS product_id, title AS name FROM products_legacy
ORDER BY product_id;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(2) 集合演算におけるORDER BYの位置
ORDER BYは最後のクエリの後ろにのみ記述でき、結果セット全体に適用されます。
▶ サンプル: 正しいORDER BY
SELECT product_id, product_name FROM hot_products_q1
UNION ALL
SELECT product_id, product_name FROM hot_products_q2
ORDER BY product_id;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ サンプル: 括弧で優先順位を制御
(SELECT product_id FROM hot_products_q1
EXCEPT
SELECT product_id FROM hot_products_q2)
UNION ALL
(SELECT product_id FROM hot_products_q2
EXCEPT
SELECT product_id FROM hot_products_q3)
ORDER BY product_id;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
最初に差分を計算し、その後マージします。括弧がない場合、UNIONはINTERSECT/EXCEPTよりも優先順位が低くなります。
| 演算 | 優先順位 | 結合性 |
|---|---|---|
| INTERSECT | 最高 | 左から右 |
| EXCEPT | 中 | 左から右 |
| UNION / UNION ALL | 中 | 左から右 |
(3) 集合演算におけるNULL処理
集合演算ではNULLを等しいものとして扱います(通常の比較ではNULL <> NULLとなるのとは異なります)。
▶ サンプル: UNIONでのNULL重複除去
SELECT NULL AS val
UNION
SELECT NULL AS val;
val
-----
(1 row)
2つのNULLは同じものとして扱われ、UNIONは1行に重複除去します。
▶ サンプル: INTERSECTでのNULL一致
SELECT NULL AS val
INTERSECT
SELECT NULL AS val;
val
-----
(1 row)
NULLはNULLと一致し、INTERSECTは1行を返します。
| シナリオ | NULLの動作 | 通常の比較との違い |
|---|---|---|
| UNION | 2つのNULLは等しいとみなされ、重複除去 | 通常のNULL = NULLはUNKNOWN |
| INTERSECT | 2つのNULLは等しいとみなされ、一致 | 通常のNULL = NULLはUNKNOWN |
| EXCEPT | 2つのNULLは等しいとみなされ、相殺 | 通常のNULL <> NULLはUNKNOWN |
(4) 集合演算とJOIN
▶ サンプル: INTERSECTとINNER JOINの同等性
SELECT a.product_id
FROM hot_products_q1 a
INNER JOIN hot_products_q2 b ON a.product_id = b.product_id;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
以下と同等です。
SELECT product_id FROM hot_products_q1
INTERSECT
SELECT product_id FROM hot_products_q2;
| 観点 | 集合演算 | JOIN |
|---|---|---|
| 意味論 | 行に対する集合演算 | カラムの組み合わせ |
| 出力カラム | 左側のカラムを採用 | 両方のテーブルのカラムが利用可能 |
| 重複除去 | UNION/INTERSECT/EXCEPTは自動 | 手動DISTINCTが必要 |
| NULLマッチング | NULL = NULL | NULL <> NULL |
| パフォーマンス | 大規模データではソートして重複除去 | インデックス付きHash Joinの方が高速な場合あり |
| 最適な用途 | 同じ構造の結果セットのマージ/積/差分 | 異種テーブルの結合によるカラム取得 |
5. 実践
▶ サンプル: UNION ALLで注文と返金台帳をマージ
SELECT
order_id AS transaction_id,
amount AS credit,
0 AS debit,
'order' AS type,
created_at
FROM orders
UNION ALL
SELECT
refund_id,
0,
refund_amount,
'refund',
created_at
FROM refunds
ORDER BY created_at;
Output:
CREATE TABLE
▶ サンプル: EXCEPTで登録済みだが未アクティベーションのユーザーを検索
SELECT user_id, email FROM registered_users
EXCEPT
SELECT user_id, email FROM activated_users;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ サンプル: INTERSECTでAとBの両方を購入した顧客を検索
SELECT customer_id FROM order_items WHERE product_id = 101
INTERSECT
SELECT customer_id FROM order_items WHERE product_id = 102;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ サンプル: 3年間の顧客維持分析
SELECT 'retained' AS status, COUNT(*) AS cnt FROM (
SELECT customer_id FROM orders WHERE EXTRACT(YEAR FROM created_at) = 2024
INTERSECT
SELECT customer_id FROM orders WHERE EXTRACT(YEAR FROM created_at) = 2025
) t
UNION ALL
SELECT 'churned', COUNT(*) FROM (
SELECT customer_id FROM orders WHERE EXTRACT(YEAR FROM created_at) = 2024
EXCEPT
SELECT customer_id FROM orders WHERE EXTRACT(YEAR FROM created_at) = 2025
) t;
Output:
count
-------
5
(1 row)
▶ サンプル: UNION ALL + GROUP BYでトレンド集計
SELECT
product_id,
SUM(CASE WHEN quarter = 'Q1' THEN 1 ELSE 0 END) AS q1_count,
SUM(CASE WHEN quarter = 'Q2' THEN 1 ELSE 0 END) AS q2_count,
SUM(CASE WHEN quarter = 'Q3' THEN 1 ELSE 0 END) AS q3_count
FROM (
SELECT product_id, 'Q1' AS quarter FROM hot_products_q1
UNION ALL
SELECT product_id, 'Q2' FROM hot_products_q2
UNION ALL
SELECT product_id, 'Q3' FROM hot_products_q3
) combined
GROUP BY product_id
ORDER BY product_id;
Output:
count
-------
5
(1 row)
6. 総合サンプル
Bobのクロスクォーターベストセラー分��——常緑、新規、離脱商品の一括出力。
WITH q1 AS (
SELECT product_id, product_name FROM hot_products WHERE quarter = 'Q1'
),
q2 AS (
SELECT product_id, product_name FROM hot_products WHERE quarter = 'Q2'
),
q3 AS (
SELECT product_id, product_name FROM hot_products WHERE quarter = 'Q3'
),
evergreen AS (
SELECT product_id, product_name, 'evergreen' AS trend FROM q1
INTERSECT
SELECT product_id, product_name, 'evergreen' FROM q2
INTERSECT
SELECT product_id, product_name, 'evergreen' FROM q3
),
new_q2 AS (
SELECT product_id, product_name, 'new_in_q2' AS trend FROM q2
EXCEPT
SELECT product_id, product_name, 'new_in_q2' FROM q1
),
new_q3 AS (
SELECT product_id, product_name, 'new_in_q3' AS trend FROM q3
EXCEPT
SELECT product_id, product_name, 'new_in_q3' FROM q2
),
churned_q2 AS (
SELECT product_id, product_name, 'churned_in_q2' AS trend FROM q1
EXCEPT
SELECT product_id, product_name, 'churned_in_q2' FROM q2
),
churned_q3 AS (
SELECT product_id, product_name, 'churned_in_q3' AS trend FROM q2
EXCEPT
SELECT product_id, product_name, 'churned_in_q3' FROM q3
)
SELECT * FROM evergreen
UNION ALL
SELECT * FROM new_q2
UNION ALL
SELECT * FROM new_q3
UNION ALL
SELECT * FROM churned_q2
UNION ALL
SELECT * FROM churned_q3
ORDER BY trend, product_id;
product_id | product_name | trend
------------+--------------+---------------
101 | Widget Pro | evergreen
103 | Server Rack | new_in_q2
104 | Cable Max | new_in_q3
102 | Gadget Mini | churned_in_q2
103 | Server Rack | churned_in_q3
7. 集合演算の実行フロー
flowchart TD
A["クエリA"] --> C{演算}
B["クエリB"] --> C
C -->|UNION| D["結合 + 重複除去"]
C -->|UNION ALL| E["結合(重複保持)"]
C -->|INTERSECT| F["一致 + 重複除去"]
C -->|EXCEPT| G["A - B + 重複除去"]
D --> H["ORDER BY(オプション)"]
E --> H
F --> H
G --> H
H --> I["最終結果"]
style D fill:#c8e6c9
style F fill:#e1f5fe
style G fill:#fff9c4
| ステップ | 操作 | 説明 |
|---|---|---|
| 1 | 各サブクエリを実行 | 独立して実行。結果セットは構造を共有する必要あり |
| 2 | 集合演算 | UNION/INTERSECT/EXCEPT |
| 3 | 重複除去(必要な場合) | UNION/INTERSECT/EXCEPTはデフォルトで重複除去 |
| 4 | ORDER BY | 最終結果セットに適用 |
| 5 | LIMIT | 最終出力行の制限 |
❓ よくある質問
SELECT * FROM (A UNION B) AS tは有効です。括弧で囲んでエイリアスを付けてください。📖 まとめ
- UNIONはマージして重複除去、UNION ALLはマージして重複を保持(パフォーマンスが良い)
- INTERSECTは積集合、EXCEPTは差集合
- ALL接尾辞は重複カウントを保持: INTERSECT ALL / EXCEPT ALL
- 集合演算はカラム数が同じで、型に互換性が必要
- ORDER BYは文の最後にのみ記述可能
- 集合演算ではNULLは等しいものとして扱われる(通常の比較とは異なる)
- 優先順位: INTERSECT = EXCEPT > UNION。括弧で制御
- 集合演算は同じ構造の結果セットのマージ/積/差分に適し、JOINは異種テーブルの結合に適する
📝 練習問題
- ⭐ UNION ALLを使用して2024年と2025年の注文テーブルをマージし、年タグカラムを追加して金額降順でソートしてください。
- ⭐ EXCEPTを使用して、customersテーブルには存在するがactive_usersテーブルには存在しないユーザーを検索してください。
- ⭐⭐ INTERSECTを使用して全3四半期に出現するベストセラー商品IDを検索し、productsテーブルと結合して商品名を出力してください。
- ⭐⭐ CTE + EXCEPTで顧客離脱分析: 2024年に注文があったが2025年にはなかった顧客。
- ⭐⭐⭐ UNION ALL + INTERSECT + EXCEPTを組み合わせた1つのSQL文を書き、3つの商品カテゴリ(常緑/新規/離脱)をそれぞれタグカラム付きで出力し、最後にGROUP BYで各カテゴリの商品数をカウントしてください。