2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GROUP BY で泣いた夜に

「部署ごとの最高給与を取りたいだけなのに、なんで社員の名前も一緒に返ってこないの……?」

SQLを書いていて、こんな“理不尽”に出会ったことはありませんか。GROUP BY で集約すると、集計に使った列以外の情報は容赦なく消し飛びます。集約は便利ですが、たとえるなら「クラス全員の平均点は分かるけど、誰が一番だったかは教えない先生」みたいなものです。気になるところだけ隠される。

そんなときに登場するのが 分析関数(Analytic Functions) です。分析関数は、行をまとめて消してしまう GROUP BYと違い、1行1行を残したまま、その行から見た「順位」や「前の行の値」を計算して列として付け足してくれる、気の利いた相棒です。

この記事では、現場で出番の多い 3 兄弟、

  • ROW_NUMBER(とにかく連番)
  • RANK(同点は仲良く同じ順位)
  • LAG(ひとつ前の行をのぞき見)

を、実際に動くSQLとともに紹介します。
対象はOracle Database 19cです。

分析関数のキホン:OVER句

分析関数の構文は、ざっくり言うとこうです。

関数名(引数) OVER (
  [PARTITION BY 列リスト]   -- グループ分け(任意)
  [ORDER BY 列リスト]       -- 並び順
  [ウィンドウ句]             -- 集計範囲(任意)
)

ポイントは OVER 句。これが「どの範囲で計算するか」を指定するものです。

  • PARTITION BYGROUP BY の親戚。ただし行は消えない。「部署ごとに」「顧客ごとに」といった仕切りを作ります。書かなければ全行が1つのグループ。
  • ORDER BY:その仕切りの中での並び順。順位付けや「前の行」の概念は、並び順があって初めて意味を持ちます。

用語補足:「ウィンドウ(window)」とは、計算対象になる行の範囲のこと。
注目している行の周辺だけを見る、というイメージです。

では、以下のような社員テーブルを例に進めます。

CREATE TABLE employees (
  emp_id   NUMBER,
  emp_name VARCHAR2(20),
  dept     VARCHAR2(10),
  salary   NUMBER
);

INSERT INTO employees VALUES (1, '田中', '営業', 500000);
INSERT INTO employees VALUES (2, '鈴木', '営業', 600000);
INSERT INTO employees VALUES (3, '佐藤', '営業', 600000);
INSERT INTO employees VALUES (4, '高橋', '開発', 700000);
INSERT INTO employees VALUES (5, '伊藤', '開発', 550000);
INSERT INTO employees VALUES (6, '渡辺', '開発', 550000);
COMMIT;

失敗例:やりがちなSQL

まず部署ごとの最高給与のみ見てみましょう。

SELECT dept,
       MAX(salary)
FROM employees
GROUP BY dept;

結果:

DEPT  MAX(SALARY)
営業  600000
開発  700000

SQL触りたての多くの人がこう思うかもしれません。

「営業部の最高給与は 60万円なんだから、鈴木さんの名前も一緒に出せばいいじゃん!」

そこで次のように書いてみます。

SELECT dept,
       emp_name,
       MAX(salary)
FROM employees
GROUP BY dept;

すると、エラーが出ます。

ORA-00979: "EMP_NAME": must appear in the GROUP BY clause or be used in an aggregate function

なぜならOracleにとって、

営業部 → 600000円

という情報は分かっても、

600000円の人 → 鈴木さん?
600000円の人 → 佐藤さん?

が決められないからです。

実際、サンプルデータを見ると営業部には最高給与の社員が2人います。

EMP_NAME  DEPT  SALARY
鈴木      営業  600000
佐藤      営業  600000

こんな時に分析関数を使用して解決することができます。
ここからは、分析関数の実践をしていきましょう。

① ROW_NUMBER:とにかく重複なしの連番が欲しい

ROW_NUMBER は、並び順に沿って 1, 2, 3, ... と重複なしの連番を振ります。同じ値が並んでいても容赦なく別番号です。整理券を配るおじさんだと思ってください。給与が同じでも「はい次の人、次の人」と機械的に番号を渡します。

SELECT
  emp_name,
  dept,
  salary,
  ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
FROM employees;

結果(営業・開発それぞれで連番):

EMP_NAME  DEPT  SALARY   RN
鈴木      営業  600000   1
佐藤      営業  600000   2   ← 同額でも番号は別
田中      営業  500000   3
高橋      開発  700000   1
伊藤      開発  550000   2
渡辺      開発  550000   3

使い方:各グループの「1位だけ」を取り出す

冒頭の悩み「部署ごとの最高給与の人を、名前付きで」はこう解決します。

SELECT emp_name, dept, salary
FROM (
  SELECT
    emp_name, dept, salary,
    ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
  FROM employees
)
WHERE rn = 1;

結果:

EMP_NAME  DEPT  SALARY
鈴木      営業  600000
高橋      開発  700000

rn = 1 で各部署のトップだけが残ります。注意点として、WHERE 句では分析関数を直接使えないWHERE ROW_NUMBER() = 1 はエラー)ので、いったんサブクエリで列にしてから絞り込みます。分析関数は SELECTORDER BY でしか書けない、と覚えておきましょう。

② RANK:同点は同じ順位、でも番号は飛ぶ

スポーツの順位表を思い出してください。1位タイが2人いたら、次は3位ですよね(2位はいない)。これが RANK の挙動です。同じ値には同じ順位を与え、その分だけ次の順位を飛ばします。

SELECT
  emp_name,
  dept,
  salary,
  RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS rnk
FROM employees;

結果:

EMP_NAME  DEPT  SALARY   RNK
鈴木      営業  600000   1
佐藤      営業  600000   1   ← 同額なので同じ1位
田中      営業  500000   3   ← 2位は飛んで3位
高橋      開発  700000   1
伊藤      開発  550000   2
渡辺      開発  550000   2

営業の田中さんが「3位」になっているのがポイント。1位が2人いたので2位は欠番です。

「順位を飛ばしたくない」なら DENSE_RANK

「同点でもいいけど、次は2位にしてほしい(欠番イヤ)」という場合は DENSE_RANK を使います。denseは「密な」という意味で、その名のとおり順位がギッシリ詰まって飛びません。

SELECT
  emp_name,
  salary,
  RANK() OVER (ORDER BY salary DESC) AS rnk,
  DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rnk
FROM employees;

結果:

EMP_NAME  DEPT  SALARY   RNK  DESNSE_RNK
高橋      開発  700000   1     1
鈴木      営業  600000   2     2
佐藤      営業  600000   2     2
伊藤      開発  550000   4     3
渡辺      開発  550000   4     3
田中      営業  500000   6     4

3兄弟(ROW_NUMBER / RANK / DENSE_RANK)の違いを一言でまとめると:

  • ROW_NUMBER:同点を認めない。常に連番。
  • RANK:同点を認める。順位は飛ぶ。
  • DENSE_RANK:同点を認める。順位は飛ばない。

「TOP3を取りたい」場合、ROW_NUMBER なら同点を無視して必ず3行、RANK/DENSE_RANK なら同点を全部拾うので3行を超えることがあります。要件次第で使い分けましょう。

③ LAG:ひとつ前の行をこっそりのぞく

LAG は、並び順における「ひとつ前の行の値」を今の行に持ってきてくれる関数です。

月別売上の「前月比」を出す、という定番ケースで使います。

CREATE TABLE monthly_sales (
  ym     VARCHAR2(6),
  amount NUMBER
);
INSERT INTO monthly_sales VALUES ('202401', 1000);
INSERT INTO monthly_sales VALUES ('202402', 1500);
INSERT INTO monthly_sales VALUES ('202403', 1200);
INSERT INTO monthly_sales VALUES ('202404', 1800);
COMMIT;
SELECT
  ym,
  amount,
  LAG(amount) OVER (ORDER BY ym) AS prev_amount,
  amount - LAG(amount) OVER (ORDER BY ym) AS diff
FROM monthly_sales;

結果:

YM      AMOUNT  PREV_AMOUNT  DIFF
202401  1000    (null)       (null)
202402  1500    1000         500
202403  1200    1500         -300
202404  1800    1200         600

一番最初の行は「前の行」が存在しないので NULL になります。先頭の NULL が気になるなら、LAG の引数で何個前かデフォルト値を指定できます。

-- LAG(対象列, ずらす行数, 該当なし時のデフォルト値)
LAG(amount, 1, 0) OVER (ORDER BY ym)

こうすると先頭は 0 になり、差分の計算で NULL に悩まされません。

LEAD は LAG の双子

逆に「次の行」を見たいときは LEAD を使います。引数の使い方は LAG と同じです。「翌月の売上」を見たいなら LEAD(amount) OVER (ORDER BY ym)

まとめ:迷ったらこの早見表

やりたいこと 使う関数
グループごとに重複なしの連番/各グループの1件抽出 ROW_NUMBER
順位付け(同点同順位・順位は飛ぶ) RANK
順位付け(同点同順位・順位は飛ばない) DENSE_RANK
前の行の値(前月比など) LAG
次の行の値(翌月予測など) LEAD

分析関数のキモは、結局のところ OVER (PARTITION BY ... ORDER BY ...) という「どの仕切りで、どんな順番に」を表現する部分に尽きます。ここさえ押さえれば、これまで GROUP BY とサブクエリの森をさまよっていたクエリが、驚くほどスッキリします。

そして繰り返しになりますが、大事なルール:

  • 分析関数は SELECTORDER BY 句でしか書けない
  • WHERE で絞りたいときは、いったんサブクエリで列にしてから

この2点を忘れなければ、もう「平均点は教えるけど誰が1位かは秘密」の先生に泣かされることはありません。

参考

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?