概要
ORDER BY 句は、クエリ結果の並び順を指定するために使用される。
SELECT 文の末尾に記述し、指定した列の値に基づいて結果セットをソートする。
昇順 (ASC) または降順 (DESC) を指定でき、複数列による多段階ソートも可能である。
ORDER BY 句は、単純な列名の指定だけでなく、関数の結果、式、CASE式、列番号、エイリアス等、柔軟な指定方法をサポートしている。
また、LIMIT 句と組み合わせることにより、上位N件の取得やページネーションの実装が可能である。
パフォーマンスの観点では、インデックスの有無が実行速度に大きく影響する。
インデックスを使用できる場合、MySQLはファイルソート (filesort) を回避し、インデックス順で直接データを読み取ることができる。
インデックスを使用できない場合は、ソートバッファを使用した filesort が発生し、大量のデータに対してはディスクソートが必要になる場合がある。
MySQL 8.0以降では、ウィンドウ関数と組み合わせた高度なソート処理や、インクリメンタルソートバッファ割り当て等の最適化が導入されている。
基本構文
ORDER BY句の基本形式
ORDER BY句は、SELECT文の末尾に記述する。
# 基本構文
SELECT column1, column2, ...
FROM table_name
ORDER BY column [ASC|DESC];
# 昇順ソート (ASC、デフォルト)
SELECT name, salary FROM employees
ORDER BY salary ASC;
# 降順ソート (DESC)
SELECT name, salary FROM employees
ORDER BY salary DESC;
# ASCは省略可能
SELECT name, salary FROM employees
ORDER BY salary;
ASC は昇順 (Ascending) を意味し、小さい値から大きい値の順にソートされる。
DESC は降順 (Descending) を意味し、大きい値から小さい値の順にソートされる。
ASC を指定しない場合、デフォルトで昇順ソートとなる。
複数列によるソート
複数の列を指定することで、多段階のソートが可能である。
# 複数列によるソート
SELECT name, department_id, salary FROM employees
ORDER BY department_id ASC, salary DESC;
# 最初の列で並び替え、同値の場合は2番目の列で並び替え
SELECT product_name, category_id, price FROM products
ORDER BY category_id, price DESC;
# 3つ以上の列でソート
SELECT name, department_id, hire_date, salary FROM employees
ORDER BY department_id, hire_date DESC, salary DESC;
複数列を指定した場合、最初の列でソートし、同値の行については2番目の列でソート、さらに同値の場合は3番目の列でソート、という処理が行われる。
各列ごとに ASC または DESC を個別に指定できる。
# 各列ごとにソート順を指定
SELECT name, department_id, salary FROM employees
ORDER BY department_id ASC, salary DESC;
# department_idは昇順、salaryは降順
# 混在も可能
SELECT name, hire_date, salary FROM employees
ORDER BY hire_date DESC, salary ASC;
# hire_dateは降順、salaryは昇順
列番号とエイリアスによるソート
列番号によるソート
ORDER BY句では、SELECT句に記述した列の順序を示す番号 (1から始まる) を指定できる。
# 列番号によるソート
SELECT name, salary FROM employees
ORDER BY 2 DESC;
# 2番目の列 (salary) で降順ソート
# 複数列の指定も可能
SELECT name, department_id, salary FROM employees
ORDER BY 2 ASC, 3 DESC;
# 2番目の列 (department_id) で昇順、3番目の列 (salary) で降順
ただし、列番号によるソートは非推奨である。
理由は、SELECT句の列順序が変更されると、ソート対象も意図せず変わってしまうためである。
# 列番号は非推奨
SELECT name, salary FROM employees
ORDER BY 1 DESC;
# nameで降順ソート
# SELECT句が変更されると、ソート対象も変わる
SELECT salary, name FROM employees
ORDER BY 1 DESC;
# salaryで降順ソート (意図と異なる可能性)
列名を直接指定する方が、保守性と可読性が高い。
エイリアスによるソート
SELECT句で定義した列のエイリアス (別名) をORDER BY句で使用できる。
# エイリアスによるソート
SELECT name, salary * 12 AS annual_salary FROM employees
ORDER BY annual_salary DESC;
# CONCAT関数の結果にエイリアスを付けてソート
SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM employees
ORDER BY full_name;
# 複数のエイリアスを使用
SELECT
name,
salary * 12 AS annual_salary,
YEAR(hire_date) AS hire_year
FROM employees
ORDER BY hire_year DESC, annual_salary DESC;
エイリアスにバッククォート (`) を使用すると、予約語や特殊文字を含む名前も使用できる。
# バッククォートを使用したエイリアス
SELECT salary * 12 AS `年収` FROM employees
ORDER BY `年収` DESC;
# 予約語をエイリアスとして使用
SELECT name AS `order` FROM employees
ORDER BY `order`;
エイリアスを使用することで、複雑な式をORDER BY句で再記述する必要がなくなり、可読性が向上する。
NULLの扱い
NULLの基本的な動作
ORDER BY句では、NULL 値は最小値として扱われる。
# NULLは最小値扱い
SELECT name, salary FROM employees
ORDER BY salary ASC;
# NULLが最初、次に小さい値から大きい値の順
SELECT name, salary FROM employees
ORDER BY salary DESC;
# 大きい値から小さい値の順、最後にNULL
ASC (昇順) では、NULL が最初に表示される。
DESC (降順) では、NULL が最後に表示される。
NULLを最後に配置する方法
NULLを最後に配置したい場合、ISNULL() 関数または IS NULL を使用する方法がある。
# ISNULL()関数を使用してNULLを最後に配置
SELECT name, salary FROM employees
ORDER BY ISNULL(salary) ASC, salary ASC;
# ISNULL(salary)は、NULLの場合1、それ以外は0を返す
# 0 (非NULL) が最初、1 (NULL) が最後になる
# IS NULLを使用した方法
SELECT name, salary FROM employees
ORDER BY salary IS NULL, salary ASC;
# salary IS NULLは、NULLの場合1、それ以外は0を返す
# 降順でNULLを最後に配置
SELECT name, salary FROM employees
ORDER BY ISNULL(salary) ASC, salary DESC;
ISNULL(column) は、NULLの場合に 1、それ以外の場合に 0 を返す。
この特性を利用して、非NULL値を先にソートし、NULLを最後に配置できる。
COALESCE()関数による代替値指定
COALESCE() 関数を使用すると、NULL に代替値を指定してソートできる。
# COALESCE()で代替値を指定
SELECT name, salary FROM employees
ORDER BY COALESCE(salary, 9999999) ASC;
# salaryがNULLの場合、9999999として扱われる
# NULLを最小値として扱う
SELECT name, salary FROM employees
ORDER BY COALESCE(salary, 0) DESC;
# salaryがNULLの場合、0として扱われる
# 文字列の場合
SELECT name, department_name FROM employees
ORDER BY COALESCE(department_name, 'ZZZZZ');
# department_nameがNULLの場合、'ZZZZZ'として扱われ、最後に配置される
COALESCE(column, default_value) は、column がNULLの場合に default_value を返す。
これにより、NULLを任意の値として扱ってソートできる。
式と関数によるソート
関数の結果によるソート
ORDER BY句では、列名だけでなく、関数の結果でソートすることができる。
# LENGTH()関数で文字列の長さでソート
SELECT name FROM employees
ORDER BY LENGTH(name) DESC;
# YEAR()関数で年だけを抽出してソート
SELECT name, hire_date FROM employees
ORDER BY YEAR(hire_date) DESC;
# MONTH()関数で月だけを抽出してソート
SELECT name, hire_date FROM employees
ORDER BY MONTH(hire_date), DAY(hire_date);
# UPPER()関数で大文字小文字を無視してソート
SELECT name FROM employees
ORDER BY UPPER(name);
# ABS()関数で絶対値でソート
SELECT name, balance FROM accounts
ORDER BY ABS(balance) DESC;
関数を使用することで、データの加工結果に基づいたソートが可能になる。
式によるソート
ORDER BY句では、算術演算やその他の式を使用できる。
# 算術演算の結果でソート
SELECT name, salary, bonus FROM employees
ORDER BY salary + bonus DESC;
# 複数列の演算結果でソート
SELECT product_name, price, quantity FROM products
ORDER BY price * quantity DESC;
# 条件式と組み合わせたソート
SELECT name, salary FROM employees
ORDER BY salary / 12 DESC;
式によるソートは、計算結果に基づいた柔軟なソートを実現する。
CASE式によるカスタムソート
CASE 式を使用すると、カスタムの優先順位でソートできる。
# CASE式でステータスの優先順位を指定
SELECT order_id, status FROM orders
ORDER BY
CASE status
WHEN 'urgent' THEN 1
WHEN 'processing' THEN 2
WHEN 'pending' THEN 3
WHEN 'completed' THEN 4
ELSE 5
END;
# 複数条件での優先順位指定
SELECT name, department_id, salary FROM employees
ORDER BY
CASE
WHEN department_id = 10 THEN 1
WHEN department_id = 20 THEN 2
ELSE 3
END,
salary DESC;
# NULL値を特定の位置に配置
SELECT name, manager_id FROM employees
ORDER BY
CASE
WHEN manager_id IS NULL THEN 0
ELSE 1
END,
manager_id;
CASE 式を使用することで、ビジネスロジックに基づいた任意の順序でソートできる。
FIELD()関数による任意順序指定
FIELD() 関数を使用すると、任意の順序でソートできる。
# FIELD()関数で任意順序を指定
SELECT name, status FROM orders
ORDER BY FIELD(status, 'urgent', 'processing', 'pending', 'completed');
# 特定の値を優先的にソート
SELECT product_name, category FROM products
ORDER BY FIELD(category, 'Electronics', 'Books', 'Clothing'), price;
# 任意の値リストでソート
SELECT name FROM employees
ORDER BY FIELD(department_id, 30, 10, 20);
# department_idが30、10、20の順で表示される
FIELD(column, value1, value2, value3, ...) は、column が value1 の場合に1、value2 の場合に2、という順序番号を返す。
これにより、任意の順序でソートできる。
LIMITとの組み合わせ
上位N件の取得
ORDER BY句とLIMIT句を組み合わせることにより、ソート後の上位N件を取得できる。
# 上位10件の給与を取得
SELECT name, salary FROM employees
ORDER BY salary DESC
LIMIT 10;
# 最新の5件の注文を取得
SELECT order_id, ordered_at FROM orders
ORDER BY ordered_at DESC
LIMIT 5;
# 価格が最も高い商品を取得
SELECT product_name, price FROM products
ORDER BY price DESC
LIMIT 1;
# 最も古い3件のレコードを取得
SELECT name, hire_date FROM employees
ORDER BY hire_date ASC
LIMIT 3;
LIMIT 句は、ソート後の結果から指定した件数のみを返す。
詳細は、MySQL - LIMIT句のページを参照すること。
ページネーション
LIMIT 句と OFFSET を組み合わせることにより、ページネーション (ページ分割) を実装できる。
# 最初の10件 (ページ1)
SELECT name, salary FROM employees
ORDER BY salary DESC
LIMIT 10 OFFSET 0;
# 11件目から20件目 (ページ2)
SELECT name, salary FROM employees
ORDER BY salary DESC
LIMIT 10 OFFSET 10;
# 21件目から30件目 (ページ3)
SELECT name, salary FROM employees
ORDER BY salary DESC
LIMIT 10 OFFSET 20;
# 一般的な形式
# LIMIT rows OFFSET (page - 1) * rows
ただし、OFFSET を使用したページネーションは、大きなオフセット値に対してパフォーマンスが低下する。
MySQLは、オフセット分の行を読み飛ばす必要があるためである。
シーク法 (キーセットページネーション)
大量のデータに対するページネーションでは、シーク法 (キーセットページネーション) が推奨される。
# 最初のページ (通常通り)
SELECT id, name, created_at FROM posts
ORDER BY id DESC
LIMIT 10;
# 結果: id = 100, 99, 98, ..., 91 (最後のidは91)
# 次のページ (WHERE句で前ページの最後のidより小さいものを取得)
SELECT id, name, created_at FROM posts
WHERE id < 91
ORDER BY id DESC
LIMIT 10;
# 結果: id = 90, 89, 88, ..., 81
# さらに次のページ
SELECT id, name, created_at FROM posts
WHERE id < 81
ORDER BY id DESC
LIMIT 10;
シーク法は、インデックスを効率的に使用できるため、大量のデータに対しても高速である。
# 昇順ソートの場合
SELECT id, name FROM employees
WHERE id > 100
ORDER BY id ASC
LIMIT 10;
# 複合ソートの場合
SELECT id, name, created_at FROM posts
WHERE (created_at, id) < ('2024-01-01', 100)
ORDER BY created_at DESC, id DESC
LIMIT 10;
パフォーマンス最適化
インデックスによるORDER BY最適化
ORDER BY句で指定した列にインデックスが存在する場合、MySQLはインデックス順でデータを読み取ることができる。
これにより、ファイルソート (filesort) を回避し、高速にソートを実行できる。
# インデックスが使用される例
CREATE INDEX idx_salary ON employees(salary);
SELECT name, salary FROM employees
ORDER BY salary DESC;
# インデックスが使用され、filesortは発生しない
# WHERE句とORDER BY句の列が同一インデックスに含まれる場合
CREATE INDEX idx_dept_salary ON employees(department_id, salary);
SELECT name, salary FROM employees
WHERE department_id = 10
ORDER BY salary DESC;
# インデックスが使用され、filesortは発生しない
インデックスを使用できる条件は以下の通りである。
ORDER BY句の列にインデックスが存在する- 単一列または複合インデックスの先頭列から順に使用されている。
WHERE句とORDER BY句の列が同一インデックスに含まれるWHERE句の列とソート列が同一のインデックスに含まれている場合、インデックスのみで処理できる。
- ソート順がインデックスの順序と一致する
- インデックスは昇順で作成されるため、昇順ソートが最も効率的。
filesort (ファイルソート)
インデックスを使用できない場合、MySQLはファイルソート (filesort) を実行する。
# filesortが発生する例
SELECT name, salary FROM employees
ORDER BY salary DESC;
# salaryにインデックスがない場合、filesortが発生
# 複数列のソートでインデックスが使用できない場合
SELECT name, salary FROM employees
ORDER BY department_id, hire_date DESC;
# (department_id, hire_date)のインデックスがない場合、filesortが発生
# 関数を使用したソート
SELECT name, hire_date FROM employees
ORDER BY YEAR(hire_date);
# 関数を使用しているため、インデックスが使用できず、filesortが発生
ファイルソートには、以下に示す3つのアルゴリズムがある。
<sort_key, rowid>アルゴリズム- ソートキーと行IDをソートし、ソート後に再度テーブルを読み取る。
<sort_key, additional_fields>アルゴリズム- ソートキーと必要な列を全てソートバッファに格納し、再読み取りを回避する。
<sort_key, packed_additional_fields>アルゴリズム- ソートキーと必要な列を圧縮形式で格納する。最も効率的。
MySQLは、データの特性とソートバッファのサイズに基づいて、最適なアルゴリズムを自動的に選択する。
sort_buffer_sizeの調整
sort_buffer_size は、ファイルソート用のメモリバッファサイズを指定する。
# 現在の設定を確認
SHOW VARIABLES LIKE 'sort_buffer_size';
# デフォルト: 256[KB] (262144バイト)
# セッション単位で変更
SET SESSION sort_buffer_size = 1048576;
# 1[MB]に変更
# グローバルで変更
SET GLOBAL sort_buffer_size = 2097152;
# 2[MB]に変更
sort_buffer_size を増やすと、大量のデータをメモリ内でソートできるため、ディスクI/Oを削減できる。
ただし、各セッションごとにバッファが確保されるため、過度に大きくするとメモリ不足になる可能性がある。
MySQL 8.0.12以降では、ソートバッファは増分割り当て (incremental allocation) されるため、実際に必要なメモリのみが使用される。
EXPLAINでの確認
EXPLAIN を使用して、ORDER BYがインデックスを使用しているか、filesortが発生しているかを確認できる。
# EXPLAINで実行計画を確認
EXPLAIN SELECT name, salary FROM employees
ORDER BY salary DESC;
# Extra列に "Using filesort" が表示される場合、filesortが発生している
# Extra列に何も表示されない場合、インデックスが使用されている
Extra 列の内容を確認することで、ソート処理の詳細がわかる。
Using filesort- ファイルソートが発生している。インデックスを使用していない。
Using index- インデックスのみで処理が完了している (カバリングインデックス)。
Using temporary; Using filesort- 一時テーブルを作成してからファイルソートを実行している。
最適化のベストプラクティス
ORDER BY句のパフォーマンスを最適化するためのベストプラクティスを以下に示す。
- ソート列にインデックスを作成する
- 頻繁にソートされる列にはインデックスを作成すべきである。
- 複合インデックスの順序に注意する
WHERE句とソート列を含む複合インデックスは、(filter_column, sort_column)の順序で作成する。
SELECT *を避ける- 必要な列のみを取得することで、ソートバッファの使用量を削減できる。
LIMIT句を使用する- 上位N件のみが必要な場合、
LIMIT句を使用することで、優先キューソートが使用され、全件ソートを回避できる。
- 上位N件のみが必要な場合、
- 大量のデータに対するページネーションではシーク法を使用する
OFFSETを避け、WHERE句とインデックスを活用したシーク法を使用する。
# 良い例: 必要な列のみを取得
SELECT id, name, salary FROM employees
ORDER BY salary DESC
LIMIT 10;
# 悪い例: SELECT *を使用
SELECT * FROM employees
ORDER BY salary DESC
LIMIT 10;
# ソートバッファに不要な列も格納される
# 複合インデックスの作成例
CREATE INDEX idx_dept_salary ON employees(department_id, salary);
# インデックスが効率的に使用される
SELECT name, salary FROM employees
WHERE department_id = 10
ORDER BY salary DESC;
MySQL 8.0以降の機能
ウィンドウ関数でのORDER BY
MySQL 8.0以降では、ウィンドウ関数内で ORDER BYを使用できる。
# ROW_NUMBER()でランキング付け
SELECT
name,
salary,
ROW_NUMBER() OVER (ORDER BY salary DESC) AS salary_rank
FROM employees;
# RANK()で同順位を考慮したランキング
SELECT
name,
salary,
RANK() OVER (ORDER BY salary DESC) AS salary_rank
FROM employees;
# DENSE_RANK()で連続したランキング
SELECT
name,
salary,
DENSE_RANK() OVER (ORDER BY salary DESC) AS salary_rank
FROM employees;
# パーティション内でのソート
SELECT
name,
department_id,
salary,
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS dept_rank
FROM employees;
ウィンドウ関数を使用することにより、グループごとのランキングや累積計算等が可能になる。
GROUP BY ASC/DESC構文の廃止
MySQL 8.0.13以降、GROUP BY句でのASC / DESC構文は廃止された。
# MySQL 8.0.12以前 (非推奨)
SELECT department_id, COUNT(*) FROM employees
GROUP BY department_id ASC;
# MySQL 8.0.13以降 (エラー)
# ERROR: 'GROUP BY ASC/DESC' is not supported
# 代わりにORDER BY句を使用
SELECT department_id, COUNT(*) FROM employees
GROUP BY department_id
ORDER BY department_id ASC;
GROUP BY 句でソートが必要な場合は、明示的に ORDER BY 句を使用する必要がある。
インクリメンタルソートバッファ割り当て
MySQL 8.0.12以降、ソートバッファは増分割り当て (incremental allocation) される。
以前のバージョンでは、sort_buffer_size で指定されたサイズのバッファが最初に確保されていた。
MySQL 8.0.12以降では、実際に必要なサイズのみが段階的に割り当てられるため、メモリ使用量が削減される。
これにより、sort_buffer_size を大きな値に設定しても、メモリの無駄が発生しにくくなった。