7.

SQL 集合演算子 (UNION/INTERSECT/EXCEPT) 完全ガイド

編集
この記事の要点
  • UNION = 和集合(重複排除)/ UNION ALL = 重複保持(高速)
  • INTERSECT = 積集合(両方にある行)/ EXCEPT または MINUS (Oracle) = 差集合
  • 必要条件: 各 SELECT のカラム数データ型が一致。NULL も同等扱い
  • ORDER BY は最後の SELECTの後にのみ書ける
  • NOT EXISTS / EXISTS との比較: 集合演算子は宣言的、EXISTS は手続き的。性能はオプティマイザ次第

SQL 集合演算子とは

SQL の集合演算子 (Set Operator) は、2 つ以上の SELECT 文の結果集合を、数学的な集合演算(和・積・差)で結合する演算子です。標準 SQL では以下が定義されています:

演算子意味重複
UNION和集合 (A ∪ B)排除
UNION ALL和集合(重複保持)保持(高速)
INTERSECT積集合 (A ∩ B)排除
INTERSECT ALL積集合(重複保持)保持
EXCEPT / MINUS差集合 (A - B)排除
EXCEPT ALL差集合(重複保持)保持

UNION と UNION ALL

-- 例: 社員テーブルと顧客テーブルから連絡先一覧
SELECT name, email FROM employees
UNION
SELECT name, email FROM customers
ORDER BY name;

-- UNION ALL: 重複行も含む(高速)
SELECT product_id FROM orders_2024
UNION ALL
SELECT product_id FROM orders_2025;

-- 性能差は大きい:
-- UNION     → 結果に DISTINCT 相当の重複排除(ソート or ハッシュ)
-- UNION ALL → 単純連結のみ、ソート不要
-- → 重複が無いことが分かっているなら必ず UNION ALL

INTERSECT (積集合)

-- 例: メルマガと SMS 両方を購読している顧客
SELECT user_id FROM mailing_list
INTERSECT
SELECT user_id FROM sms_subscribers;

-- IN サブクエリでも同じことが書ける
SELECT user_id FROM mailing_list
WHERE user_id IN (SELECT user_id FROM sms_subscribers);

-- EXISTS バージョン
SELECT m.user_id FROM mailing_list m
WHERE EXISTS (SELECT 1 FROM sms_subscribers s WHERE s.user_id = m.user_id);

-- NULL の扱い:
-- 集合演算子は NULL = NULL と「同じ値」扱い
-- WHERE 句の = NULL は false 扱い → 注意

EXCEPT / MINUS (差集合)

-- 標準 SQL / PostgreSQL / SQL Server / MariaDB 10.3+
SELECT email FROM customers
EXCEPT
SELECT email FROM unsubscribed;

-- Oracle / DB2 (一部) は MINUS
SELECT email FROM customers
MINUS
SELECT email FROM unsubscribed;

-- 「顧客にいるがメルマガ未登録の人」
SELECT user_id FROM all_users
EXCEPT
SELECT user_id FROM mailing_list;

-- NOT IN / NOT EXISTS で書き換え
SELECT user_id FROM all_users
WHERE user_id NOT IN (SELECT user_id FROM mailing_list);
-- ⚠️ NULL が含まれると NOT IN は意図しない結果に
-- → NOT EXISTS の方が安全
SELECT u.user_id FROM all_users u
WHERE NOT EXISTS (SELECT 1 FROM mailing_list m WHERE m.user_id = u.user_id);

必要条件

条件説明
カラム数一致各 SELECT のカラム数が同じ
データ型互換対応位置のカラム型が同じ or 暗黙キャスト可
カラム名最初の SELECT の名前が結果に使われる
ORDER BY最後にのみ書ける。途中の SELECT には書けない
LIMIT同上(一部 DB で例外あり)
-- ❌ カラム数不一致でエラー
SELECT id, name FROM a
UNION
SELECT id FROM b;
-- ORA-01789: query block has incorrect number of result columns

-- ❌ ORDER BY の位置エラー
SELECT name FROM a ORDER BY name
UNION
SELECT name FROM b;
-- → エラー

-- ✅ ORDER BY は最後
SELECT name FROM a
UNION
SELECT name FROM b
ORDER BY name;

-- ✅ 個別にソートしたい場合はサブクエリ
SELECT * FROM (SELECT name FROM a ORDER BY name LIMIT 10) sub1
UNION
SELECT * FROM (SELECT name FROM b ORDER BY name LIMIT 10) sub2;

CORRESPONDING (SQL 標準、実装少)

SQL 標準には UNION CORRESPONDING という構文があり、列名が一致する列だけで集合演算するものですが、多くの DB で未実装です(一部 DB2 のみ)。実用上はカラム順を揃えるのが定石。

NULL の扱い

-- 集合演算子では NULL = NULL として「同じ値」扱い
SELECT NULL FROM dual
INTERSECT
SELECT NULL FROM dual;
-- → 1 行 (NULL)
-- 通常の WHERE 句では NULL = NULL は UNKNOWN なので 0 行

-- これは set operator が DISTINCT 相当を内部実行する際の挙動
-- NULL を含むカラムは要注意

性能と Index

演算子内部処理性能
UNION ALL連結のみ最速
UNION連結 + Sort/Hash で重複排除
INTERSECTHash Match or Sort Merge
EXCEPT / MINUSHash Match or Sort Merge

各 SELECT 内で適切な Indexが効くようにすることが重要。集合演算子自体は最適化されにくいので、各 SELECT が高速に絞り込めるかが性能の鍵です。

各 DB の対応状況

DBUNIONINTERSECTEXCEPTMINUS
Oracle×
PostgreSQL×
SQL Server×
MySQL 8.0+○ (8.0.31+)○ (8.0.31+)×
MySQL 5.7× (NOT EXISTS で代用)××
MariaDB 10.3+○ (10.6+ で同義)
SQLite×
DB2×

サブクエリ + EXISTS / NOT EXISTS との比較

-- (1) 集合演算子: 宣言的
SELECT user_id FROM a
INTERSECT
SELECT user_id FROM b;

-- (2) EXISTS: 手続き的
SELECT a.user_id FROM a
WHERE EXISTS (SELECT 1 FROM b WHERE b.user_id = a.user_id);

-- (3) JOIN: 直接結合
SELECT DISTINCT a.user_id FROM a
INNER JOIN b ON a.user_id = b.user_id;

-- 性能はオプティマイザ次第。多くの場合 (2) or (3) が速い
-- (1) は重複排除のソートコストがある
-- → 巨大データには (2) or (3) を検討

Materialized View での集合演算

-- 大規模 UNION ALL を Materialized View 化
CREATE MATERIALIZED VIEW all_orders AS
SELECT 'A' AS region, order_id, amount FROM orders_asia
UNION ALL
SELECT 'E' AS region, order_id, amount FROM orders_europe
UNION ALL
SELECT 'N' AS region, order_id, amount FROM orders_namerica;

-- リフレッシュ
REFRESH MATERIALIZED VIEW all_orders;       -- PostgreSQL
EXEC DBMS_MVIEW.REFRESH('ALL_ORDERS');      -- Oracle

-- 統合ビューとして検索高速化
SELECT region, SUM(amount) FROM all_orders GROUP BY region;

実用例

パーティション分割テーブルの集約

-- 月別テーブルから 1 年分集計
SELECT order_id FROM orders_202501
UNION ALL SELECT order_id FROM orders_202502
UNION ALL SELECT order_id FROM orders_202503
...
UNION ALL SELECT order_id FROM orders_202512;

差分検出

-- 元テーブルと退避テーブルの差分
(SELECT * FROM master EXCEPT SELECT * FROM backup)  -- 追加分
UNION ALL
(SELECT * FROM backup EXCEPT SELECT * FROM master); -- 削除分

FAQ

Q: UNION と JOIN の違いは?
A: UNION は縦方向の連結(行を増やす)、JOIN は横方向の結合(列を増やす)。全く別の操作です。

Q: UNION で「重複していない」と分かっていても UNION ALL に置き換えるべき?
A: はい。UNION は内部で DISTINCT 処理(ソート / ハッシュ)が走るため、不要なら UNION ALL の方が大幅に速いです。

Q: ORACLE の MINUS は ANSI SQL ?
A: いいえ、Oracle 独自構文です。標準 SQL は EXCEPT。Oracle 19c でも EXCEPT は未サポート(21c で追加予定)。

Q: MySQL 5.7 で INTERSECT が使いたい
A: MySQL 5.7 は未対応。INNER JOIN または EXISTS で書き換えてください。MySQL 8.0.31+ で正式対応しました。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. SELECT文
  2. INSERT
  3. UPDATE文
  4. DELETE文
  5. WHERE句
  6. JOIN句
  7. 集合演算子
  8. インラインビュー
  9. 副問い合わせ (サブクエリ)

最近更新/作成されたページ