UNIONで複数SELECTの結果を結合したあとにORDER BYで並び替えるのは一見シンプルです。しかし「各SELECT内にORDER BYを書いたらエラーになった」「ORDER BYで参照できる列名が制限される」「特定のテーブルのデータを先に表示したい」といった疑問にぶつかるケースが多くあります。
本記事ではUNIONとORDER BYの組み合わせに関するルール・よくあるエラーの原因と対策・ソース別の優先ソートやピン留めといった実務パターン・各UNION部分にORDER BYをかけたい場合の回避策・DBMS別の挙動差まで徹底解説します。
基本ルール:ORDER BYはUNION全体の末尾に1つだけ
UNIONを使うとき、ORDER BYに関して守るべきルールが2つあります。
- ルール1:ORDER BYはUNION全体の末尾に1つだけ書く
- ルール2:ORDER BYで参照できる列名は最初のSELECT文の列名(エイリアス含む)に限られる
-- OK: ORDER BY は UNION 全体の末尾に1つ SELECT id, name, 'users' AS source FROM users UNION SELECT id, name, 'admins' AS source FROM admins ORDER BY name ASC; -- ← ここに1つだけ書く -- OK: UNION ALL(重複を残す)+ ORDER BY SELECT id, name FROM employees UNION ALL SELECT id, name FROM contractors ORDER BY id;
UNION結合する各SELECTの末尾にORDER BYを書くと、MySQL・PostgreSQL・SQL Serverではエラーになります。
個別SELECTにORDER BYをかけたい場合はサブクエリまたはCTEを使います(後述)。
ORDER BYで参照できる列名の制約
ORDER BYで使える列名は最初のSELECT文の列名・エイリアスに限られます。2番目以降のSELECTの列名が違う場合でも、最初のSELECTの名前で参照します。
-- テーブルによって列名が異なる場合 -- employees テーブル: emp_name -- contractors テーブル: contractor_name -- OK: 最初のSELECTの列名(または列番号)を使う SELECT emp_id AS id, emp_name AS name FROM employees UNION SELECT contractor_id, contractor_name FROM contractors ORDER BY name; -- 最初のSELECTのエイリアス "name" を参照 -- OK: 列番号でも参照できる(可読性は下がる) SELECT emp_id, emp_name FROM employees UNION SELECT contractor_id, contractor_name FROM contractors ORDER BY 2; -- 2番目の列(emp_name / contractor_name)でソート -- NG: 2番目のSELECTの列名は使えない SELECT emp_id, emp_name FROM employees UNION SELECT contractor_id, contractor_name FROM contractors ORDER BY contractor_name; -- エラー! contractor_name は最初のSELECTにない
各UNION部分に個別のORDER BYまたはLIMITをかけたい場合
「各テーブルの上位N件ずつをUNIONして表示したい」など、個別SELECTにORDER BYやLIMITをかけたい場合は、サブクエリ(または括弧付きSELECT)を使います。
-- MySQL / PostgreSQL: 括弧でくくることで個別LIMITが使える (SELECT id, name, score FROM game_rankings ORDER BY score DESC LIMIT 3) UNION ALL (SELECT id, name, score FROM mobile_rankings ORDER BY score DESC LIMIT 3) ORDER BY score DESC; -- ※ UNION(重複排除)を使う場合も同様 (SELECT emp_id AS id, emp_name AS name FROM employees ORDER BY emp_name LIMIT 5) UNION (SELECT contractor_id, contractor_name FROM contractors ORDER BY contractor_name LIMIT 5) ORDER BY name;
-- Oracle: 括弧内のORDER BYは許可されていない(エラー)
-- → サブクエリ + ROWNUM / FETCH FIRST を使う
SELECT id, name, score
FROM (
SELECT id, name, score FROM game_rankings ORDER BY score DESC FETCH FIRST 3 ROWS ONLY
)
UNION ALL
SELECT id, name, score
FROM (
SELECT id, name, score FROM mobile_rankings ORDER BY score DESC FETCH FIRST 3 ROWS ONLY
)
ORDER BY score DESC;
-- SQL Server: サブクエリ + TOP を使う SELECT id, name, score FROM (SELECT TOP 3 id, name, score FROM game_rankings ORDER BY score DESC) AS g UNION ALL SELECT id, name, score FROM (SELECT TOP 3 id, name, score FROM mobile_rankings ORDER BY score DESC) AS m ORDER BY score DESC;
-- CTEを使った書き方(推奨:可読性が高い)
WITH top_employees AS (
SELECT emp_id AS id, emp_name AS name, 'employee' AS source
FROM employees
ORDER BY emp_name
LIMIT 5 -- MySQL / PostgreSQL
-- FETCH FIRST 5 ROWS ONLY -- Oracle
-- TOP 5 を先頭に書く -- SQL Server
),
top_contractors AS (
SELECT contractor_id, contractor_name, 'contractor' AS source
FROM contractors
ORDER BY contractor_name
LIMIT 5
)
SELECT id, name, source FROM top_employees
UNION ALL
SELECT id, name, source FROM top_contractors
ORDER BY name;
ソース別の優先ソート(テーブルAを先に、テーブルBを後に)
「社員を先に表示して、次に業務委託を表示したい」など、特定のSELECTのデータを別のSELECTより優先して並べたい場合は、ソート用の識別子カラムをSELECT句に追加します。
-- source_order 列で優先順位を指定(1が先、2が後)
SELECT emp_id AS id, emp_name AS name,
1 AS source_order -- 社員を先
FROM employees
UNION ALL
SELECT contractor_id, contractor_name,
2 AS source_order -- 業務委託を後
FROM contractors
ORDER BY source_order, name; -- まずテーブル順、次に名前順
-- 結果のイメージ:
-- id | name | source_order
-- 1 | 田中 | 1(社員)
-- 2 | 鈴木 | 1(社員)
-- 3 | 佐藤 | 2(業務委託)
-- 4 | 渡辺 | 2(業務委託)
-- ソート用の列を表示させたくない場合: サブクエリでラップして除外
SELECT id, name
FROM (
SELECT emp_id AS id, emp_name AS name, 1 AS sort_key FROM employees
UNION ALL
SELECT contractor_id, contractor_name, 2 AS sort_key FROM contractors
) AS combined
ORDER BY sort_key, name; -- sort_key でソートしつつ、外側のSELECTには含めない
ピン留め:特定のレコードを常に先頭に固定する
「「おすすめ」ラベルのついた商品を常に先頭に表示し、残りは通常ソート」といったUI要件は、UNIONとソート用カラムの組み合わせで実現できます。
-- おすすめ商品(is_featured = 1)を先頭に、残りを名前順で表示
SELECT product_id, name, price, 1 AS pin_order FROM products WHERE is_featured = 1
UNION ALL
SELECT product_id, name, price, 2 AS pin_order FROM products WHERE is_featured = 0
ORDER BY pin_order, name;
-- 同じ結果をCASE WHENで1クエリにする方法(UNIONなし)
SELECT product_id, name, price
FROM products
ORDER BY
CASE WHEN is_featured = 1 THEN 0 ELSE 1 END, -- おすすめを先頭
name; -- 同じグループ内は名前順
-- 管理者(admins)→ 一般ユーザー(users)の順に表示 SELECT user_id AS id, username AS name, email, 'admin' AS role, 1 AS sort_key FROM admins UNION ALL SELECT user_id, username, email, 'user' AS role, 2 AS sort_key FROM users ORDER BY sort_key, name;
各UNION部分の行の順序を保持する
「テーブルAはスコア降順、テーブルBは日付順に並べてからUNION」のように、各部分の内部順序を保ったまま結合したい場合は、行番号列(ROW_NUMBER)を使います。
-- ROW_NUMBER() を使って各部分の内部順序を保持
SELECT id, name, score, source, part_rank
FROM (
SELECT
game_id AS id,
game_name AS name,
high_score AS score,
'game' AS source,
ROW_NUMBER() OVER (ORDER BY high_score DESC) AS part_rank,
1 AS source_order
FROM game_scores
UNION ALL
SELECT
user_id,
username,
level_score,
'user',
ROW_NUMBER() OVER (ORDER BY level_score DESC),
2
FROM user_scores
) ranked
ORDER BY source_order, part_rank;
-- game_scores: スコア降順の順序を保持
-- user_scores: スコア降順の順序を保持
-- 2つのブロックを順番に表示
UNION vs UNION ALL + ORDER BYのパフォーマンス
ORDER BYを加えるとソートコストが発生しますが、それに加えてUNION(重複排除)はUNION ALLよりも重いです。理由は重複排除のためにソートまたはハッシュ処理が必要なためです。
| 組み合わせ | 動作 | コスト | 推奨場面 |
|---|---|---|---|
| UNION ALL + ORDER BY | 全行マージ → ソート | 低(ソートのみ) | 重複がない・または重複を残したい場合 |
| UNION + ORDER BY | 全行マージ → 重複排除 → ソート | 高(重複排除+ソート) | 重複排除が必要な場合のみ |
-- UNION + ORDER BY の実行計画を確認 EXPLAIN SELECT id, name FROM employees UNION SELECT id, name FROM contractors ORDER BY name; -- UNION ALL + ORDER BY と比較 EXPLAIN SELECT id, name FROM employees UNION ALL SELECT id, name FROM contractors ORDER BY name; -- UNION ALLの方がExtra列に "Using filesort" のみ(重複排除コストなし)
DBMS別の注意点
-- MySQL: 括弧付きSELECTに個別のORDER BY + LIMIT が使える (SELECT id, name FROM users ORDER BY name LIMIT 5) UNION ALL (SELECT id, name FROM admins ORDER BY name LIMIT 5) ORDER BY name; -- ただし括弧なしのSELECTにORDER BYを書くとエラー -- SELECT id, name FROM users ORDER BY name ← エラー -- UNION ALL -- SELECT id, name FROM admins -- ORDER BY name; -- MySQL: ORDER BY にエイリアスが使える(拡張) SELECT id, emp_name AS name FROM employees UNION ALL SELECT id, contractor_name FROM contractors ORDER BY name; -- エイリアス "name" が使える
-- PostgreSQL: 括弧付きSELECT内のORDER BYは許可される (SELECT id, name FROM users ORDER BY name LIMIT 5) UNION ALL (SELECT id, name FROM admins ORDER BY name LIMIT 5) ORDER BY name; -- PostgreSQL: SQL標準に厳密で、ORDER BYの列名は最初のSELECTを参照 SELECT emp_id AS id, emp_name AS name FROM employees UNION SELECT contractor_id, contractor_name FROM contractors ORDER BY name; -- 最初のSELECTのエイリアス "name" を使う
-- Oracle: UNION内の各SELECTにORDER BYは書けない
-- → サブクエリ + ROWNUM / FETCH FIRST を使う
-- OK: サブクエリでORDER BY + FETCH FIRST
SELECT id, name FROM (
SELECT emp_id AS id, emp_name AS name FROM employees
ORDER BY emp_name FETCH FIRST 5 ROWS ONLY
)
UNION ALL
SELECT id, name FROM (
SELECT contractor_id, contractor_name FROM contractors
ORDER BY contractor_name FETCH FIRST 5 ROWS ONLY
)
ORDER BY name;
-- Oracle: UNION全体のORDER BYでエイリアスが使える
SELECT emp_id AS id, emp_name AS name FROM employees
UNION
SELECT contractor_id, contractor_name FROM contractors
ORDER BY name; -- Oracle もエイリアス参照可
-- SQL Server: 括弧付きSELECT内のORDER BYは使えない(エラー)
-- → サブクエリ + TOP を使う
SELECT id, name FROM (
SELECT TOP 5 emp_id AS id, emp_name AS name
FROM employees ORDER BY emp_name
) AS e
UNION ALL
SELECT id, name FROM (
SELECT TOP 5 contractor_id, contractor_name
FROM contractors ORDER BY contractor_name
) AS c
ORDER BY name;
-- SQL Server: ORDER BY のエイリアスも使える
SELECT emp_id AS id, emp_name AS name FROM employees
UNION
SELECT contractor_id, contractor_name FROM contractors
ORDER BY name;
| 機能 | MySQL | PostgreSQL | Oracle | SQL Server |
|---|---|---|---|---|
| 括弧内の個別ORDER BY | ○ | ○ | × (サブクエリ必要) | × (サブクエリ必要) |
| 括弧内の個別LIMIT | ○ | ○ (LIMIT) | ○ (FETCH FIRST) | ○ (TOP) |
| ORDER BY でエイリアス参照 | ○ | ○ | ○ | ○ |
| ORDER BY で列番号参照 | ○ | ○ | ○ | ○ |
よくあるエラーと原因・対処法
| エラー内容 | 原因 | 対処法 |
|---|---|---|
| ORDER BY is not allowed here | UNION内のSELECTにORDER BYを書いた | ORDER BYはUNION末尾に1つだけ書く。個別にかけたいならサブクエリを使う |
| Column not found in ORDER BY | 2番目以降のSELECTの列名をORDER BYで参照した | 最初のSELECTの列名またはエイリアスを使う |
| Incorrect usage of UNION and ORDER BY | MySQLで括弧なしSELECTにORDER BYを書いた | 括弧でSELECTをくくる: (SELECT ... ORDER BY ...) |
| SELECT列数が一致しない | UNIONする各SELECTの列数が違う | SELECT句の列数をすべてのSELECTで一致させる |
よくある質問
1 AS sort_key、2 AS sort_key)を追加して、ORDER BY sort_key, name と並べます。sort_keyを表示させたくない場合はサブクエリでラップして外側のSELECTからsort_keyを除きます。WHERE condition1 OR condition2 やWHERE condition IN (...)で1クエリにまとめられる場合があります。異なるテーブルをUNIONしている場合は代替が難しいですが、FULL OUTER JOIN(JOIN後にフィルタ)での代替も検討できます。まとめ
UNIONとORDER BYの組み合わせには独自のルールがあります。ポイントをまとめます。
- ORDER BYの位置:UNION全体の末尾に1つだけ。各SELECTには書けない
- 列名の制約:ORDER BYで参照できるのは最初のSELECTの列名(エイリアス)のみ
- 個別ORDER BY:MySQL/PostgreSQLは括弧でくくれば可。Oracle/SQL Serverはサブクエリ使用
- ソース別優先ソート:ソート用数値カラム(sort_key)をSELECTに追加してORDER BYで利用
- ピン留め:CASE WHEN(同一テーブル)またはUNION + sort_keyで実現
- パフォーマンス:重複がなければUNION ALLを使う。UNIONは重複排除コストが発生
関連記事:UNION完全ガイド(UNION ALL・INTERSECT・EXCEPT/MINUSの違いと実務パターン)、ORDER BYの使い方完全解説、WITH句(CTE)を使って副問合せを再利用する方法
