ウィンドウ関数とRANGEとROWS

こんにちは、@knsk765 です。ラーメン食べるとすぐ太るのなんなんですかね?勘弁してほしいですよね。

SQL のウィンドウ関数好きなのでこのブログの記事が毎回それなんですけど、今回もです。

blog.ingage.jp

目次

前回の記事の中で ROWS ... というのに触れたんですが、そこをもうちょい説明します。
というか自分もよく分かってなかったので、Grok が教えてくれたことを共有したいと思っています。 プライベートではなんでも Grok に聞くマンです。

ROWS ... ってなんのこと言ってんの?っていうとこれです。

AVG(amount) OVER (
  PARTITION BY customer_id
  ORDER BY ordered_at
  ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)

今回は OVER (...) の中でも少し分かりにくい Frame 句 の話、特に RANGEROWS の違いに絞って整理します。

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 で見たときに、同じ値の行が複数あります。ここで RANGEROWS の違いが見えてきます。

まずは 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行の平均」です。同じ日付が並んでいても、同値の行をまとめて扱ったりはしません。

実務ではどう書くと安全か

個人的には、以下を意識しておくと事故りにくいです。

  1. まず、ORDER BYを書いたら Frame 句も明示する
    PostgreSQL では Frame 句を省略すると RANGE が使われるので、「何も書かなければ普通の累積和」と思っているとずれます。

  2. 次に、「直近N行」や「1行ずつ増える累積」がほしいなら ROWS を使う
    多くの "行ベースの累積" は、こちらのほうが意図がそのまま伝わります。

  3. 逆に、「同じ値の行をまとめたい」「値の範囲で見たい」ときだけ RANGE

この整理だけでもかなり分かりやすくなります。

まとめ

PostgreSQL のウィンドウ関数で RANGEROWS の違いは、まず意味の違いとして理解するのがいちばん分かりやすいです。

  • ROWS は行数ベース
  • RANGE は値ベース

特に ORDER BY の値が重複していると、結果ははっきり変わります。
しかも Frame 句を省略したときのデフォルトは RANGE なので、累積和を1行ずつ素直に増やしたいなら
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW を明示しておくほうが安全です。

なんとなく省略して書くより、Frame 句まで書いておく。
それだけで、だいぶ読みやすくて、誤解しにくい SQL になります。

おまけ

ウィンドウ関数に興味を持たれた方はこちらの記事もぜひ。

blog.ingage.jp

blog.ingage.jp

blog.ingage.jp