順番に読み進めながら学べます

ウィンドウ関数 ② — ORDER BY とフレーム(ROWS / RANGE)

この記事は、基礎から複雑なSQL,SQLチューニングまでSQLの実践的なスキルを1からマスターする「SQL入門講座の一部」です。
OVER (... ORDER BY ...) で累積和、ROWS BETWEEN 2 PRECEDING の移動合計、同値行で結果が変わる ROWS と RANGE の差を sales データを表で確かめます。

ORDER BY 付きのウィンドウ — 累積和(running total)を出す

OVERの中にORDER BYを足すと、集計の対象が「ウィンドウの先頭から今の行まで」に変わり、行ごとに値が増えていく累積和(running total = 先頭からその行までの合計)を計算できます。

書き方はSUM(amount) OVER (PARTITION BY emp_id ORDER BY sale_date)です。

PARTITION BY emp_idで社員ごとにウィンドウを区切り、ORDER BY sale_dateでその社員の売上を日付順に並べます。そして各社員に対して累計を計算できます。

下の図で、emp_id=29 の 5 件の売上を日付順に見たとき、累計がどう増えていくかを確認してください。

ORDER BY を足すと「先頭から現在行まで」の累計になる
sale_dateamountrunning_total(SUM OVER ORDER BY date)01-07310000310000(= 310000)02-14295000605000(= 310 + 295)03-21280000885000(= 605 + 280)04-052650001150000(= 885 + 265)06-233250001475000(= 1150 + 325)最終行 = 社員の総売上+295000+280000+265000+325000
emp_id=29 の売上 5 件を sale_date 順に並べると、running_total は前の行までの累計に今の amount を足した値になります。310000 → 605000 → 885000 → 1150000 → 1475000 と増えていき、最後の行が社員の総売上に一致します。
-- ORDER BY なし: 社員ごとの「変わらない合計」が各行に付く
SELECT emp_id, sale_date, amount,
  SUM(amount) OVER (PARTITION BY emp_id) AS emp_total
FROM sales
ORDER BY emp_id, sale_date;

-- ORDER BY あり: 社員ごとの「先頭から今の行まで」の累計
SELECT emp_id, sale_date, amount,
  SUM(amount) OVER (PARTITION BY emp_id ORDER BY sale_date) AS running_total
FROM sales
ORDER BY emp_id, sale_date;

「社員ごとに、売上を日付順に並べて、その時点までの累計売上を出したい」という要件を想定します。(正しく実行できれば解説が表示されます)

salesテーブルからemp_idsale_dateamountの 3 列を取り出してください。

SUM(amount)を社員ごとに区切り、日付順に並べたウィンドウで計算し、running_totalという別名で 4 列目に追加してください。

③ 判定は行の並び順を見るため、emp_idの昇順、同じ社員内はsale_dateの昇順で並べてください。

SQL エディタ

クエリを実行してください

フレーム — ウィンドウ内で実際に集計される行の範囲

ウィンドウ関数でORDER BYを付けたとき、各行が集計する範囲をフレーム(frame)と呼びます。

フレーム — ウィンドウの中で「実際に集計する行」の範囲
ウィンドウ(PARTITION BY emp_id= 1 社員分の行)フレーム(現在行のための集計範囲)row 1row 2row 3 ← 現在行row 4row 5row 1row 2row 3 (現在行)1 社員の全行row 3 を計算するときの集計対象(既定: 先頭〜現在行)ウィンドウ (5 行)フレーム (集計対象 3 行)
1 つのウィンドウ(例: emp_id=14 の 5 行)の中で、現在行ごとに「ここからここまでを合計する」と決める範囲がフレームです。フレームの取り方を変えると、累計・移動合計・前後の平均など、計算の意味が変わります。

フレームを明示しないと、既定でRANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(ウィンドウの先頭から現在行まで)が適用されます。

前の演習で累計になったのは、この既定フレームが効いていたからです。

-- A: フレーム省略(既定 = RANGE UNBOUNDED PRECEDING 〜 CURRENT ROW)
SELECT emp_id, sale_date, amount,
  SUM(amount) OVER (PARTITION BY emp_id ORDER BY sale_date) AS run_default
FROM sales WHERE emp_id = 14
ORDER BY sale_date;

-- B: 既定フレームを明示的に書いたもの(A と同じ結果)
SELECT emp_id, sale_date, amount,
  SUM(amount) OVER (
    PARTITION BY emp_id ORDER BY sale_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS run_explicit
FROM sales WHERE emp_id = 14
ORDER BY sale_date;

フレームに使うキーワードは次の 5 つです。現在行を基準に「どこから / どこまで」を指定します。

キーワード意味
UNBOUNDED PRECEDINGウィンドウの先頭まで遡る
n PRECEDING現在行より n 行前
CURRENT ROW現在の行(自分自身)
n FOLLOWING現在行より n 行後
UNBOUNDED FOLLOWINGウィンドウの末尾まで進む
既定(ORDER BYあり)RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

「累積和の挙動を、フレームを明示的に書いて確認したい」という要件を想定します。

salesテーブルからemp_idsale_dateamountの 3 列を取り出してください。

社員ごとに区切り、sale_date順に並べたウィンドウに、フレームとしてROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWを明示したSUM(amount)を計算し、running_totalという別名で 4 列目に追加してください。

③ 判定は行の並び順を見るため、emp_idの昇順、同じ社員内はsale_dateの昇順で並べてください。

SQL エディタ

クエリを実行してください

ROWS BETWEEN n PRECEDING — 移動合計・移動平均

フレームをROWS BETWEEN n PRECEDING AND CURRENT ROWにすると、集計対象が「手前 n 行 + 現在行」に絞られます。

ROWS BETWEEN 2 PRECEDING AND CURRENT ROWなら、現在行を含めて直近 3 行が対象になり、SUMなら移動合計AVGなら移動平均になります。

直近の傾向をならして見たいときの基本形です。

先頭付近は手前の行が足りないぶん、対象行数が少なくなります(1 行目は 1 行のみ、2 行目は 2 行、3 行目以降が 3 行)。

下の図で、emp_id=14 の 5 件を例に、各「現在行」のときフレームに何が入るかを 1 行ずつ確認してください。

ROWS BETWEEN 2 PRECEDING AND CURRENT ROW — 現在行ごとに「直近 3 行」が動く
現在行 (amount)フレーム対象 (直近 3 行)移動合計 (SUM)1 行目490000[490000] 1 行のみ4900002 行目100000[490000, 100000] 2 行5900003 行目85000[490000, 100000, 85000] 3 行6750004 行目70000[100000, 85000, 70000] 3 行(490000 が脱落)2550005 行目505000[85000, 70000, 505000] 3 行(100000 が脱落)660000フレームが「直近 3 行」を動いていく
emp_id=14 の 5 件(金額 490000 / 100000 / 85000 / 70000 / 505000)に対し、現在行ごとに集計対象が手前 2 行+現在行に動きます。先頭付近は対象が足りずに 1 行・2 行のフレームになります。各行の「現在行 → フレーム対象 → 移動合計」を矢印でたどってください。
-- 直近 3 件の移動平均(手前 2 行 + 現在行)
SELECT emp_id, sale_date, amount,
  AVG(amount) OVER (
    PARTITION BY emp_id ORDER BY sale_date
    ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
  ) AS moving_avg3
FROM sales WHERE emp_id = 2
ORDER BY sale_date;

-- 直近 2 件の移動合計(手前 1 行 + 現在行)も同じ書き方で作れる
SELECT emp_id, sale_date, amount,
  SUM(amount) OVER (
    PARTITION BY emp_id ORDER BY sale_date
    ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
  ) AS moving_sum2
FROM sales WHERE emp_id = 2
ORDER BY sale_date;

「社員ごとに、売上を日付順に並べて、直近 3 件(手前 2 件と現在)の移動合計を見たい」という要件を想定します。

salesテーブルからemp_idsale_dateamountの 3 列を取り出してください。

社員ごとに区切り、sale_date順に並べたウィンドウに、フレームROWS BETWEEN 2 PRECEDING AND CURRENT ROWを指定したSUM(amount)を計算し、moving_sum3という別名で 4 列目に追加してください。

③ 判定は行の並び順を見るため、emp_idの昇順、同じ社員内はsale_dateの昇順で並べてください。

SQL エディタ

クエリを実行してください

ROWS と RANGE の違い — 同じ値の行をどう扱うか

フレームにはROWSRANGEの 2 つの数え方があります。

ROWS物理的な行数で数えます(手前 2 行ならきっかり 2 行)。同点があっても 1 行ずつ別に数えるので、累計値が 1 行ごとに増えます。

RANGEORDER BYの値が同じ行(同点)をひとまとめに扱います。同点の行はすべて同じフレームに入り、同じ累計値になります。

下の 2 つの図で、amount = 100000が 3 件あるとき、ROWS と RANGE がそれぞれどう動くかを確認してください。

ROWS の動き — 同値でも 1 行ずつ別に数えて累計が増える
現在行ROWS フレーム (先頭〜現在行)rows_runsale_id 21100000[21]1 行100000sale_id 24100000[21, 24]2 行200000sale_id 37100000[21, 24, 37]3 行300000
amount = 100000 の 3 件を ORDER BY amount で並べたとき、ROWS フレームは現在行までを 1 行ずつ物理的に数えます。1 行目は [21] の 1 件、2 行目は [21,24] の 2 件、3 行目は [21,24,37] の 3 件と、累計が 100000 → 200000 → 300000 と増えていきます。
RANGE の動き — 同値 3 行を 1 フレームに束ねて全行で同じ累計
現在行RANGE フレーム (同値を 1 まとめ)range_runsale_id 21100000[21, 24, 37]同値 3 行300000sale_id 24100000[21, 24, 37]同値 3 行300000sale_id 37100000[21, 24, 37]同値 3 行300000同値 3 行が 1 つのフレーム
RANGE フレームは ORDER BY amount の値が同じ行(同点)をひとまとめに扱います。amount = 100000 の 3 件はすべて同じフレーム [21, 24, 37] に入り、累計はどの行でも 300000 で同じ値になります。
-- amount を ORDER BY すると 100000 が 3 件あり ROWS と RANGE で差が出る
SELECT sale_id, amount,
  SUM(amount) OVER (ORDER BY amount ROWS  UNBOUNDED PRECEDING) AS rows_run,
  SUM(amount) OVER (ORDER BY amount RANGE UNBOUNDED PRECEDING) AS range_run
FROM sales
WHERE amount = 100000
ORDER BY sale_id;
-- ROWS は 1 行ずつ、RANGE は同点 100000 の 3 件を 1 まとめに数える

「同じ金額の売上が複数あるとき、ROWS と RANGE で累計がどう変わるかを 1 つの結果で比較したい」という要件を想定します。amount = 100000の売上は 3 件あります。

salesテーブルからsale_idamountを取り出してください。

amountで並べたウィンドウで、フレームROWS UNBOUNDED PRECEDINGSUM(amount)rows_run、フレームRANGE UNBOUNDED PRECEDINGSUM(amount)range_runという別名で追加してください。

amount = 100000の行だけに絞り込んでください。

④ 判定は行の並び順を見るため、sale_idの昇順で並べてください。

SQL エディタ

クエリを実行してください
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1SUM(amount) OVER (PARTITION BY emp_id ORDER BY sale_date)のようにOVERの中にORDER BYを足すと、各行に付く値はどうなりますか。

Q2ウィンドウ関数でORDER BYを付けたとき、フレームを明示しなかった場合の既定の範囲はどれですか。

Q3ROWSフレームとRANGEフレームの違いとして正しいものはどれですか。