PostgreSQL: PostgreSQLの集合演算と複合クエリ

最終更新:2026-08-26

1. 学習目標


2. ストーリー

BobはECプラットフォームのデータアナリストです。CEOから3つの質問を受けました。

  1. Q1、Q2、Q3で最も売れた商品は?(UNION ALLでマージ)
  2. すべての四半期で売れ続けた商品は?(INTERSECT = 常緑商品)
  3. 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(重複カウントあり)
100%
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四半期のベストセラーをマージ

SQL
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:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

UNIONは自動的に重複を除去します。��品がQ1とQ2の両方でベストセラーの場合、1回だけ表示されます。

▶ サンプル: UNION ALLで重複を保持

SQL
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;
TEXT 📖 参照専用
 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で複数テーブルの統計をマージ

SQL
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:

TEXT 📖 参照専用
 count 
-------
     5
(1 row)

(3) INTERSECTとINTERSECT ALL

▶ サンプル: 3四半期すべてで売れた常緑商品を検索

SQL
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;
TEXT 📖 参照専用
 product_id | product_name
------------+--------------
        101 | Widget Pro

Widget Proが唯一、毎四半期ベストセラーだった常緑商品です。

▶ サンプル: INTERSECT ALLで重複カウントを保持

商品101がQ1のベストセラーリストに2回、Q2に1回、Q3に3回出現するとします。

SQL
SELECT product_id FROM hot_products_q1_detail
INTERSECT ALL
SELECT product_id FROM hot_products_q2_detail;

Output:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

INTERSECT ALLはMIN(出現回数)を返します。商品101はmin(2, 1) = 1行を返します。

演算 重複除去 重複行の処理 典型的な用途
INTERSECT はい 1行のみ保持 共通項目の検索
INTERSECT ALL いいえ 最小出現回数で保持 重複頻度の正確な一致

▶ サンプル: INTERSECTで複数年共通の顧客を検索

SQL
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:

TEXT 📖 参照専用
CREATE TABLE

3年連続で注文したロイヤル顧客です。

(4) EXCEPTとEXCEPT ALL

▶ サンプル: Q1ベストセラーでQ2に入らなかった商品(離脱)

SQL
SELECT product_id, product_name FROM hot_products_q1
EXCEPT
SELECT product_id, product_name FROM hot_products_q2;
TEXT 📖 参照専用
 product_id | product_name
------------+--------------
        102 | Gadget Mini

Gadget MiniはQ1のベストセラーでしたが、Q2リストには入らず——離脱しました。

▶ サンプル: Q2で新たにベストセラー入りした商品

SQL
SELECT product_id, product_name FROM hot_products_q2
EXCEPT
SELECT product_id, product_name FROM hot_products_q1;
TEXT 📖 参照専用
 product_id | product_name
------------+--------------
        103 | Server Rack

Server RackはQ2で新たにベストセラーリスト入りしました。

▶ サンプル: EXCEPT ALLで重複カウントを保持

SQL
SELECT product_id FROM order_items_2024
EXCEPT ALL
SELECT product_id FROM order_items_2025;

Output:

TEXT 📖 参照専用
 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: カラム数が同じである必要があります。

SQL
SELECT id, name FROM table_a
UNION
SELECT id, name, price FROM table_b;
TEXT 📖 参照専用
ERROR: each UNION query must have the same number of columns

ルール2: 対応するカラムの型が互換性を持つ必要があります。

ルール 要件 違反時の結果
カラム数 同じであること コンパイルエラー
カラム型 互換性があること 暗黙的キャストまたはエラー
カラム名 最初のクエリのカラム名が採用される エイリアスに注意

▶ サンプル: エイリアスでカラム名を統一

SQL
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:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

(2) 集合演算におけるORDER BYの位置

ORDER BYは最後のクエリの後ろにのみ記述でき、結果セット全体に適用されます。

▶ サンプル: 正しいORDER BY

SQL
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:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ サンプル: 括弧で優先順位を制御

SQL
(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:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

最初に差分を計算し、その後マージします。括弧がない場合、UNIONはINTERSECT/EXCEPTよりも優先順位が低くなります。

演算 優先順位 結合性
INTERSECT 最高 左から右
EXCEPT 左から右
UNION / UNION ALL 左から右

(3) 集合演算におけるNULL処理

集合演算ではNULLを等しいものとして扱います(通常の比較ではNULL <> NULLとなるのとは異なります)。

▶ サンプル: UNIONでのNULL重複除去

SQL
SELECT NULL AS val
UNION
SELECT NULL AS val;
TEXT 📖 参照専用
 val
-----

(1 row)

2つのNULLは同じものとして扱われ、UNIONは1行に重複除去します。

▶ サンプル: INTERSECTでのNULL一致

SQL
SELECT NULL AS val
INTERSECT
SELECT NULL AS val;
TEXT 📖 参照専用
 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の同等性

SQL
SELECT a.product_id
FROM hot_products_q1 a
INNER JOIN hot_products_q2 b ON a.product_id = b.product_id;

Output:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

以下と同等です。

SQL
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で注文と返金台帳をマージ

SQL
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:

TEXT 📖 参照専用
CREATE TABLE

▶ サンプル: EXCEPTで登録済みだが未アクティベーションのユーザーを検索

SQL
SELECT user_id, email FROM registered_users
EXCEPT
SELECT user_id, email FROM activated_users;

Output:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ サンプル: INTERSECTでAとBの両方を購入した顧客を検索

SQL
SELECT customer_id FROM order_items WHERE product_id = 101
INTERSECT
SELECT customer_id FROM order_items WHERE product_id = 102;

Output:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ サンプル: 3年間の顧客維持分析

SQL
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:

TEXT 📖 参照専用
 count 
-------
     5
(1 row)

▶ サンプル: UNION ALL + GROUP BYでトレンド集計

SQL
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:

TEXT 📖 参照専用
 count 
-------
     5
(1 row)

6. 総合サンプル

Bobのクロスクォーターベストセラー分��——常緑、新規、離脱商品の一括出力。

SQL
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;
TEXT 📖 参照専用
 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. 集合演算の実行フロー

100%
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 最終出力行の制限

❓ よくある質問

Q UNIONとUNION ALL、どちらが速いですか?
A UNION ALLの方が重複除去を行わないため高��です。重複がないことが確実な場合、または重複除去が不要な場合はUNION ALLを優先してください。
Q 集合演算のカラム名はどちらのものが使われますか?
A 最初のクエリのカラム名(またはエイリアス)が採用されます。統一するには最初のクエリでエイリアスを記述してください。
Q 集合演算はサブクエリ内で使用できますか?
A はい。SELECT * FROM (A UNION B) AS tは有効です。括弧で囲んでエイリアスを付けてください。
Q 複数の集合演算の優先順位は?
A INTERSECT > (EXCEPT = UNION)。INTERSECTの優先順位が最も高く、EXCEPTとUNIONは同じレベルです。括弧で優先順位を明示してください。
Q 集合演算ではNULLは本当にNULLと等しいのですか?
A はい。集合演算では2つのNULLは等しいものとして扱われます。これは標準SQLの動作であり、通常の比較でNULL = NULLがUNKNOWNとなるのとは異なります。
Q 集合演算を使うべき時とJOINを使うべき時は?
A 「存在する/存在しない」のテストのみが必要で結合カラムが不要な場合は集合演算を使用します。両方のテーブルのカラムを結合する必要がある場合はJOINを使用します。集合演算は直感的に読めますが、JOINの方が柔軟です。

📖 まとめ


📝 練習問題

  1. ⭐ UNION ALLを使用して2024年と2025年の注文テーブルをマージし、年タグカラムを追加して金額降順でソートしてください。
  2. ⭐ EXCEPTを使用して、customersテーブルには存在するがactive_usersテーブルには存在しないユーザーを検索してください。
  3. ⭐⭐ INTERSECTを使用して全3四半期に出現するベストセラー商品IDを検索し、productsテーブルと結合して商品名を出力してください。
  4. ⭐⭐ CTE + EXCEPTで顧客離脱分析: 2024年に注文があったが2025年にはなかった顧客。
  5. ⭐⭐⭐ UNION ALL + INTERSECT + EXCEPTを組み合わせた1つのSQL文を書き、3つの商品カテゴリ(常緑/新規/離脱)をそれぞれタグカラム付きで出力し、最後にGROUP BYで各カテゴリの商品数をカウントしてください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%