SQLで中央値を求める方法
SQLでは AVG()
や SUM()
は簡単に使えますが、**中央値(メディアン)**を求めるのは少し工夫が必要です。
この記事では、PostgreSQLのウィンドウ関数を使って、効率的に中央値を求める方法を紹介します。
毎度のことながら、ミックさん著の「SQL実践入門 高速でわかりやすいクエリの書き方」から、理解できなかった箇所を噛み砕いて記事にしています。
💡 発想のポイント
- 中央値とは「ソートしたときに真ん中に来る値」
- データ件数が奇数:真ん中の1件
- データ件数が偶数:真ん中の2件の平均
🛠 アプローチの仕組み
以下の工夫を使います:
-
ROW_NUMBER()
を使って、ソート後の順位を取得 -
COUNT(*) OVER ()
で全体の件数を取得 -
diff = 2 × ROW_NUMBER - COUNT(*)
という式で中央値候補を判定 -
diff BETWEEN 0 AND 2
の行だけを抽出し、AVG(salary)
で中央値にする
📊 例:salary テーブル
SELECT AVG(salary)
FROM (
SELECT salary,
2 * ROW_NUMBER() OVER (ORDER BY salary)
- COUNT(*) OVER () AS diff
FROM salary
) TMP
WHERE diff BETWEEN 0 AND 2;
🤓 なぜこれで中央値になるのか?
まず、この式を見てください:
2 * ROW_NUMBER() - COUNT(*) OVER ()
この diff
の値を使って、中央値に該当する行だけを抽出しています。
たとえば、3行のデータが salary
テーブルにあった場合:
ROW_NUMBER | salary | COUNT(*) | diff = 2×ROW - COUNT |
---|---|---|---|
1 | 300 | 3 | -1 |
2 | 400 | 3 | 1 ✅ 中央値! |
3 | 500 | 3 | 3 |
この中で diff BETWEEN 0 AND 2
に該当するのは2行目だけです。つまり 奇数件の場合は真ん中の1件を正確に取得できます。
偶数件の場合は?
例えばデータが4件のとき:
ROW_NUMBER | salary | COUNT(*) | diff |
---|---|---|---|
1 | 300 | 4 | -2 |
2 | 400 | 4 | 0 ✅ |
3 | 500 | 4 | 2 ✅ |
4 | 600 | 4 | 4 |
このときは diff BETWEEN 0 AND 2
に該当するのは 2行目と3行目。これら2つの平均を取ることで、偶数件でも正しい中央値になります。
このように、diff
式を使うことで、奇数・偶数の両方に対応した中央値抽出が実現できます。
🚀 パフォーマンスも良好
この方法はパフォーマンス面でも優れています。
✅ ソートは1回だけで済む
ROW_NUMBER()
と COUNT(*) OVER ()
は、どちらも同じ順序(ORDER BY salary
)を使うため、1回のソートで両方の処理が済みます。これにより無駄な処理が発生しません。
✅ 複雑なJOINやサブクエリが不要
他の中央値の求め方では、自己結合やネストの深いサブクエリが必要になることがありますが、今回の方法は非常にシンプルな1段のクエリで完結します。
✅ インデックスが使える可能性あり
ORDER BY salary
にインデックスが貼られていれば、ROW_NUMBER()
のパフォーマンスも向上します。
✅ まとめ
- 中央値は SQL で直接求めるのが難しいが、ウィンドウ関数と
diff
式を使えば効率的に計算可能 - データ件数が奇数・偶数どちらでも対応できる
- ソート回数が1回で済むので高速
- シンプルな構文で読みやすく、保守性も高い