こんにちは、@knsk765 です。ラーメン食べるとすぐ太るのなんなんですかね?勘弁してほしいですよね。
SQL のウィンドウ関数好きなのでこのブログの記事が毎回それなんですけど、今回もです。
目次
- Frame 句って何?
- あんばうんでぃっど ぷれ...??
- RANGE と ROWS の違いを一言でいうと
- PostgreSQL では、Frame 句を省略すると RANGE になる
- 次に ROWS を明示した場合
- つまり何が違うのか
- RANGE が向いている場面
- ROWS が向いている場面
- 実務ではどう書くと安全か
- まとめ
前回の記事の中で ROWS ... というのに触れたんですが、そこをもうちょい説明します。
というか自分もよく分かってなかったので、Grok が教えてくれたことを共有したいと思っています。
プライベートではなんでも Grok に聞くマンです。
ROWS ... ってなんのこと言ってんの?っていうとこれです。
AVG(amount) OVER ( PARTITION BY customer_id ORDER BY ordered_at ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW )
今回は OVER (...) の中でも少し分かりにくい Frame 句 の話、特に RANGE と ROWS の違いに絞って整理します。
Frame 句って何?
ウィンドウ関数では、まず PARTITION BY でグループを分けて、ORDER BY でその中の順番を決めます。
そのうえで、現在の行を基準にどこからどこまでを計算対象にするかを決めるのが Frame 句です。
たとえば、累積和なら「先頭から今の行まで」を対象にしたいとか、移動平均なら「直近3行」を対象にしたいとかです。
この「どこからどこまでを含めるか」を決めるのが Frame 句です。
あんばうんでぃっど ぷれ...??
Frame 句の説明で、よくこんな書き方が出てきます。
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
ちょっと何言ってんのかわかんない... って僕は思ったんですが、意味はそんなに難しくありません。
UNBOUNDED PRECEDINGは「いちばん先頭」CURRENT ROWは「現在の行」
つまり「先頭から現在行まで」を対象にするという意味です。累積和でよく使うのは、まさにこの形です。
そして、冒頭の ROWS のところは RANGE と指定することもできます。
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
RANGE と ROWS の違いを一言でいうと
ここでややこしいのが、ROWS と RANGE では、同じように
BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
と書いていても、"現在の行" の考え方が違うことです。
ざっくり言うとこう。
ROWSは 行数ベースRANGEは 値ベース
ROWS では、CURRENT ROW は本当に今その1行です。
一方 RANGE では、ORDER BY に使った値を基準に範囲を考えます。
🤔🤔🤔🤔🤔
この違いは、ORDER BY の値が重複しているときにはっきり出ます。
PostgreSQL では、Frame 句を省略すると RANGE になる
結構大事なことです(僕は知りませんでした)。
PostgreSQL では、Frame 句を省略すると、デフォルトで
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
になります。
つまり、何も書かなくても RANGE が使われます。
ここを知らないと、
「何も書いてないし、なんとなく1行ずつ累積していくはず」
と思っていたのに、結果が違って見えて混乱しやすいです。
サンプルデータ
まずは試してみましょう。
WITH orders(id, customer_id, ordered_at, amount) AS ( VALUES (1, 1, date '2026-01-01', 100), (2, 1, date '2026-01-02', 300), (3, 1, date '2026-01-02', 600), (4, 1, date '2026-01-02', 400), (5, 1, date '2026-01-05', 350), (6, 1, date '2026-01-05', 800), (7, 2, date '2026-01-01', 50), (8, 2, date '2026-01-03', 150), (9, 2, date '2026-01-04', 250) ) SELECT * FROM orders ORDER BY customer_id, ordered_at, id;
このデータのポイントは、customer_id = 1 の中で
- 2026-01-02 が3行ある
- 2026-01-05 が2行ある
ことです。
つまり、ORDER BY ordered_at で見たときに、同じ値の行が複数あります。ここで RANGE と ROWS の違いが見えてきます。
まずは Frame句を省略した場合
よくある書き方から見てみます。
WITH orders(id, customer_id, ordered_at, amount) AS ( VALUES (1, 1, date '2026-01-01', 100), (2, 1, date '2026-01-02', 300), (3, 1, date '2026-01-02', 600), (4, 1, date '2026-01-02', 400), (5, 1, date '2026-01-05', 350), (6, 1, date '2026-01-05', 800), (7, 2, date '2026-01-01', 50), (8, 2, date '2026-01-03', 150), (9, 2, date '2026-01-04', 250) ) SELECT *, SUM(amount) OVER ( PARTITION BY customer_id ORDER BY ordered_at ) AS sum_frame_default FROM orders ORDER BY customer_id, ordered_at, id;
SUM(amount) OVER ( PARTITION BY customer_id ORDER BY ordered_at RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW )
結果はこうなります。
| id | customer_id | ordered_at | amount | sum_frame_default |
|---|---|---|---|---|
| 1 | 1 | 2026-01-01 | 100 | 100 |
| 2 | 1 | 2026-01-02 | 300 | 1400 |
| 3 | 1 | 2026-01-02 | 600 | 1400 |
| 4 | 1 | 2026-01-02 | 400 | 1400 |
| 5 | 1 | 2026-01-05 | 350 | 2550 |
| 6 | 1 | 2026-01-05 | 800 | 2550 |
| 7 | 2 | 2026-01-01 | 50 | 50 |
| 8 | 2 | 2026-01-03 | 150 | 200 |
| 9 | 2 | 2026-01-04 | 250 | 450 |
ここで気になるのは、2026-01-02 の3行が全部 1400 になっていることです。
素直な累積和っぽく考えると、
- 300
- 900
- 1300
みたいに増えていきそうです。でもそうなっていません。
これは RANGE だからです。
次に ROWS を明示した場合
今度は ROWS を明示してみます。
WITH orders(id, customer_id, ordered_at, amount) AS ( VALUES (1, 1, date '2026-01-01', 100), (2, 1, date '2026-01-02', 300), (3, 1, date '2026-01-02', 600), (4, 1, date '2026-01-02', 400), (5, 1, date '2026-01-05', 350), (6, 1, date '2026-01-05', 800), (7, 2, date '2026-01-01', 50), (8, 2, date '2026-01-03', 150), (9, 2, date '2026-01-04', 250) ) SELECT *, SUM(amount) OVER ( PARTITION BY customer_id ORDER BY ordered_at ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS sum_frame_rows FROM orders ORDER BY customer_id, ordered_at, id;
結果はこうなります。
| id | customer_id | ordered_at | amount | sum_frame_rows |
|---|---|---|---|---|
| 1 | 1 | 2026-01-01 | 100 | 100 |
| 2 | 1 | 2026-01-02 | 300 | 400 |
| 3 | 1 | 2026-01-02 | 600 | 1000 |
| 4 | 1 | 2026-01-02 | 400 | 1400 |
| 5 | 1 | 2026-01-05 | 350 | 1750 |
| 6 | 1 | 2026-01-05 | 800 | 2550 |
| 7 | 2 | 2026-01-01 | 50 | 50 |
| 8 | 2 | 2026-01-03 | 150 | 200 |
| 9 | 2 | 2026-01-04 | 250 | 450 |
今度は、かなり直感どおりです。1行ずつ足し込まれていきます。
これは ROWS では CURRENT ROW が本当に今その1行だからです。
同じ ordered_at の行が複数あっても、まとめて同じ結果にはなりません。
つまり何が違うのか
ここまでをかなり雑に言い切ると、こうです。
RANGEは、同じ並び順値の行をまとめて扱うROWSは、物理的な行単位で扱う
だから、
- 同じ日付の行をまとめて同じ累積値にしたいなら
RANGE - 1行ずつ順番に累積させたいなら
ROWS
という違いになります。
この違いは SUM だけでなく、AVG などの集計でも同じです。
ウィンドウ集計関数はフレームに入る行を対象に計算するので、Frame 句の意味がそのまま結果の違いになります。
RANGE が向いている場面
RANGE は、「何行前か」ではなく「どの値の範囲か」で考えたいときに向いています。
たとえば、
- 現在行から過去7日以内
- 現在値から差が100以内
のような考え方です。
SUM(amount) OVER ( ORDER BY ordered_at RANGE BETWEEN INTERVAL '7 days' PRECEDING AND CURRENT ROW )
これは「直近7行」ではなく、「値として7日以内の範囲」です。ROWS とは発想が違います。
ROWS が向いている場面
ROWS は、「直近3件」「前後2行」といった、件数ベースで考えたいときに向いています。
たとえばこんな形です。
AVG(amount) OVER ( ORDER BY ordered_at, id ROWS BETWEEN 2 PRECEDING AND CURRENT ROW )
これは「今の行を含めた直近3行の平均」です。同じ日付が並んでいても、同値の行をまとめて扱ったりはしません。
実務ではどう書くと安全か
個人的には、以下を意識しておくと事故りにくいです。
まず、
ORDER BYを書いたら Frame 句も明示する
PostgreSQL では Frame 句を省略するとRANGEが使われるので、「何も書かなければ普通の累積和」と思っているとずれます。次に、「直近N行」や「1行ずつ増える累積」がほしいなら
ROWSを使う
多くの "行ベースの累積" は、こちらのほうが意図がそのまま伝わります。逆に、「同じ値の行をまとめたい」「値の範囲で見たい」ときだけ
RANGE
この整理だけでもかなり分かりやすくなります。
まとめ
PostgreSQL のウィンドウ関数で RANGE と ROWS の違いは、まず意味の違いとして理解するのがいちばん分かりやすいです。
ROWSは行数ベースRANGEは値ベース
特に ORDER BY の値が重複していると、結果ははっきり変わります。
しかも Frame 句を省略したときのデフォルトは RANGE なので、累積和を1行ずつ素直に増やしたいなら
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW を明示しておくほうが安全です。
なんとなく省略して書くより、Frame 句まで書いておく。
それだけで、だいぶ読みやすくて、誤解しにくい SQL になります。
おまけ
ウィンドウ関数に興味を持たれた方はこちらの記事もぜひ。