UNIONとUNION ALLの違いと使い分け:重複排除と性能を実例で解説

black laptop computer turned-on displaying source code on table SQL

UNIONとUNION ALLは、どちらも複数のSELECTの結果を縦につなぎます。違いは重複した行の扱いで、UNIONは全列の値が同じ行を1行にまとめ、UNION ALLは全部の行をそのまま返します。重複を消す必要がなければ、UNION ALLのほうが速く済みます。この記事では MySQL 8.0 で実際に動かした結果をもとに、速さが変わる理由と、ORDER BY・LIMITと組み合わせるときの書き方を説明します。

このシナリオで考える

商品販売システムで、現行の商品テーブル(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相当の処理を行うからです。処理の流れは次のとおりです。

  1. 各SELECTの結果を一時テーブルに書き込む
  2. 全列に対してソートまたはハッシュで重複行を除去する
  3. 最終結果を返す

UNION ALLはステップ2を省略し、直接結果を返します。EXPLAINで比べると違いが見えます。

EXPLAIN SELECT id FROM products UNION SELECT id FROM discontinued_products;
+----+--------------+-----------------------+-------+---------+------+-----------------+
| id | select_type  | table                 | type  | key     | rows | Extra           |
+----+--------------+-----------------------+-------+---------+------+-----------------+
|  1 | PRIMARY      | products              | index | PRIMARY |    3 | Using index     |
|  2 | UNION        | discontinued_products | index | PRIMARY |    3 | Using index     |
|  3 | UNION RESULT | <union1,2>            | ALL   | NULL    | NULL | Using temporary |
+----+--------------+-----------------------+-------+---------+------+-----------------+
EXPLAIN SELECT id FROM products UNION ALL SELECT id FROM discontinued_products;
+----+-------------+-----------------------+-------+---------+------+-------------+
| id | select_type | table                 | type  | key     | rows | Extra       |
+----+-------------+-----------------------+-------+---------+------+-------------+
|  1 | PRIMARY     | products              | index | PRIMARY |    3 | Using index |
|  2 | UNION       | discontinued_products | index | PRIMARY |    3 | Using index |
+----+-------------+-----------------------+-------+---------+------+-------------+

(表示を見やすくするため、一部の列を省いています)

UNIONには結果をまとめるUNION RESULTの行があり、Using temporary(一時テーブルを使う)と出ています。UNION ALLにはこの行がありません。データ件数が増えるほど、この差が大きくなります。

演算子 重複排除 一時テーブル 処理速度の目安
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;
+----+-------------+-------+
| id | name        | price |
+----+-------------+-------+
|  1 | ノートPC    | 80000 |
|  5 | チェア      | 60000 |
|  3 | デスク      | 45000 |
+----+-------------+-------+

全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);
+----+-----------------+-------+
| id | name            | price |
+----+-----------------+-------+
|  1 | ノートPC        | 80000 |
|  3 | デスク          | 45000 |
|  5 | チェア          | 60000 |
|  4 | キーボード      |  7000 |
+----+-----------------+-------+

各テーブルの上位2件ずつ、計4件が返りました。括弧を付けずに書くと、最初のSELECTのORDER BY・LIMITのところで構文エラー(ERROR 1064)になります。

また、括弧の中のORDER BYはLIMITと一緒に書いたときだけ効きます。LIMITがないと MySQL は並べ替えを省きます(MySQL 8.0 リファレンスマニュアル)。つないだあとの並び順を決めたいときは、一番外側にORDER BYを書いてください。

よくある落とし穴・注意点

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は一時テーブルを作って重複を探す手間をかけます。行数が多いほど、この手間は大きくなります。まずUNION ALLを書き、重複排除が必要なときだけUNIONに切り替える習慣をつけましょう。

まとめ

UNIONは重複を除いて縦結合、UNION ALLは全行そのまま縦結合です。UNION ALLは一時テーブルのDISTINCT処理を省けるため高速で、重複が存在しない場合や重複を残してよい場合は積極的に選びます。ORDER BY・LIMITを各SELECT内に適用したいときは括弧が必須で、最終結果全体への適用は一番外側に書きます。まずはUNION ALLを書き、本当に重複排除が必要なときだけUNIONに切り替える、という判断基準を持つと迷いがなくなります。

もっと学ぶなら【PR】

※Amazonのアソシエイトとして、当サイトは適格販売により収入を得ています。

参考リンク

アイキャッチ画像: Photo by Jantine Doornbos on Unsplash

タイトルとURLをコピーしました