SQLで複数のSELECT結果を縦に結合するには、UNIONかUNION ALLを使います。ふたつは似ていますが、重複行の扱いと実行速度に明確な差があります。重複を除きたいときはUNION、そのまま全行欲しいときはUNION ALL——と覚えている方も多いでしょう。ただし、性能差が生じる理由や、ORDER BY・LIMITとの組み合わせには見落としがちな注意点があります。この記事では、基本構文から動作の違い、一時テーブルが生じる理由、各SELECT内でのORDER BYの扱いまで、実例とともに整理します。
このシナリオで考える
商品販売システムで、現行の商品テーブル(products)と廃番商品テーブル(discontinued_products)の2つがあるとします。
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(100),
category VARCHAR(50),
price INT
);
INSERT INTO products VALUES
(1, 'ノートPC', 'electronics', 80000),
(2, 'マウス', 'electronics', 3000),
(3, 'デスク', 'furniture', 45000);
CREATE TABLE discontinued_products (
id INT PRIMARY KEY,
name VARCHAR(100),
category VARCHAR(50),
price INT
);
INSERT INTO discontinued_products VALUES
(2, 'マウス', 'electronics', 3000), -- productsと重複
(4, 'キーボード', 'electronics', 7000),
(5, 'チェア', 'furniture', 60000);
「マウス」(id=2)は両テーブルに存在します。この2テーブルを縦につなぐとき、UNIONとUNION ALLでは結果が変わります。これ以降の例はこのデータをもとに話を進めます。
UNIONの基本:重複を排除して縦につなぐ
UNIONは複数のSELECT結果を縦に連結し、全列の値が一致する行を重複とみなして1行に絞ります。
SELECT id, name, category, price FROM products
UNION
SELECT id, name, category, price FROM discontinued_products;
| id | name | category | price |
|---|---|---|---|
| 1 | ノートPC | electronics | 80000 |
| 2 | マウス | electronics | 3000 |
| 3 | デスク | furniture | 45000 |
| 4 | キーボード | electronics | 7000 |
| 5 | チェア | furniture | 60000 |
「マウス」は両テーブルに存在しますが、UNIONでは1行だけになります。このとき列名は最初のSELECTのものが採用されます。また、各SELECTの列数とデータ型が一致していなければなりません。
UNION ALLの基本:全行をそのまま返す
UNION ALLは重複排除を行わず、全テーブルのすべての行をそのまま返します。
SELECT id, name, category, price FROM products
UNION ALL
SELECT id, name, category, price FROM discontinued_products;
| id | name | category | price |
|---|---|---|---|
| 1 | ノートPC | electronics | 80000 |
| 2 | マウス | electronics | 3000 |
| 3 | デスク | furniture | 45000 |
| 2 | マウス | electronics | 3000 |
| 4 | キーボード | electronics | 7000 |
| 5 | チェア | furniture | 60000 |
「マウス」が2行存在します。重複排除の処理を省くため、UNIONより高速です。
性能差の正体:一時テーブルとDISTINCT処理
UNIONが遅くなる理由は、内部で一時テーブルを作成してDISTINCT相当の処理を行うからです。処理の流れは次のとおりです。
- 各
SELECTの結果を一時テーブルに書き込む - 全列に対してソートまたはハッシュで重複行を除去する
- 最終結果を返す
UNION ALLはステップ2を省略し、直接結果を返します。データ件数が増えるほど差が顕著になります。
| 演算子 | 重複排除 | 一時テーブル | 処理速度の目安 |
|---|---|---|---|
| UNION | あり(全列一致で除去) | 常に使用 | 遅め |
| UNION ALL | なし | 回避できる場合あり | 速い |
重複がないと分かっている場面(異なる月のログを縦に並べる、テーブルをシャーディングして分割したデータをまとめるなど)では、UNION ALLを使うのが原則です。重複を排除する必要がある場合にのみUNIONを選びましょう。
ORDER BYとLIMITの正しい書き方
UNION/UNION ALLとORDER BY・LIMITを組み合わせるとき、スコープの理解が重要です。
全体結果に対してORDER BYとLIMITをかける
最終結果全体を並べ替えたいときは、UNION/UNION ALLの後ろに1度だけ書きます。
SELECT id, name, price FROM products
UNION ALL
SELECT id, name, price FROM discontinued_products
ORDER BY price DESC
LIMIT 3;
これは全6行を価格の降順に並べ、上位3件を返します。ORDER BYは常に結合後の全結果に適用されます。
各SELECT内でORDER BYとLIMITをかける
各SELECTの結果のみにORDER BY・LIMITを適用したいときは、括弧で囲む必要があります。
(SELECT id, name, price FROM products ORDER BY price DESC LIMIT 2)
UNION ALL
(SELECT id, name, price FROM discontinued_products ORDER BY price DESC LIMIT 2);
この書き方で、各テーブルの上位2件ずつ(計4件)を取得できます。括弧なしで各SELECT内にORDER BYを書いてもMySQL 8.0では無視されることがあるため、括弧は必須です。
よくある落とし穴・注意点
NULLも重複として扱われる
UNIONはNULL同士も重複とみなし、1行に絞ります。これはSQL標準の仕様です。
SELECT NULL AS val
UNION
SELECT NULL AS val;
-- 結果: NULLが1行だけ返る
一方、UNION ALLはNULL同士でも2行返します。意図に合わせて使い分けてください。
列数・データ型の不一致はエラーになる
各SELECTの列数が異なるとエラーになります。型が違う場合はMySQL側で暗黙変換されますが、予期しない結果になることがあります。
-- エラー: 列数不一致
SELECT id, name FROM products
UNION ALL
SELECT id FROM discontinued_products;
-- ERROR 1222: The used SELECT statements have a different number of columns
「重複がないから大丈夫」と思って UNION を使い続ける
データ設計上、重複が発生しないことが明らかな場合でも、UNIONはDISTINCT処理のコストを払います。UNION ALLに変えるだけで、数百万行規模では数秒単位の改善が見込めるケースがあります。まずUNION ALLを書き、重複排除が必要なときだけUNIONに切り替える習慣をつけましょう。
まとめ
UNIONは重複を除いて縦結合、UNION ALLは全行そのまま縦結合です。UNION ALLは一時テーブルのDISTINCT処理を省けるため高速で、重複が存在しない場合や重複を残してよい場合は積極的に選びます。ORDER BY・LIMITを各SELECT内に適用したいときは括弧が必須で、最終結果全体への適用は一番外側に書きます。まずはUNION ALLを書き、本当に重複排除が必要なときだけUNIONに切り替える、という判断基準を持つと迷いがなくなります。
参考リンク
- MySQL 8.0 リファレンスマニュアル: UNION 句
- MySQL 8.0 Reference Manual: Set Operations with UNION, INTERSECT, and EXCEPT
アイキャッチ画像: Photo by Jantine Doornbos on Unsplash
