日次の売上を 7 日移動平均でならし、累計も並べます。WITH で日次に畳んでからウィンドウ関数をかける手順と、フレーム指定の意味を練習します。
まず日別の売上を出してみます。
SELECT sale_date, SUM(amount) AS sales
FROM daily_sales
GROUP BY sale_date
ORDER BY sale_date;
90 行返りますが、 11 万の日もあれば 40 万の日もあり、 上がっているのか
下がっているのかが分かりません。 曜日の影響 (週末が高い) が乗っているためです。
その日を含む直近 7 日の平均に置き換えると、 曜日の波が消えて傾向だけが残ります。
7 日にするのは、 1 週間ぶんを必ず 1 回ずつ含むので曜日の偏りが打ち消えるからです。
WITH daily AS (
SELECT sale_date, SUM(amount) AS sales
FROM daily_sales
GROUP BY sale_date
)
SELECT sale_date, sales,
ROUND(AVG(sales) OVER (
ORDER BY sale_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
), 1) AS ma7
FROM daily
ORDER BY sale_date;
まず WITH で日次に畳んでから、 その結果にウィンドウ関数をかけるのが型です。
daily_sales に直接 AVG(...) OVER を書くと、 1 日に複数行あるので
「直近 7 行」 が 「直近 7 日」 になりません。
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
これは 「6 行前から今の行まで」 = 自分を入れて 7 行という範囲の指定です。
この範囲を フレーム と呼びます。
| 書き方 | 範囲 |
|---|---|
| INLINE2 | 直近 7 行 (移動平均) |
| INLINE3 | 先頭から今まで (累計) |
| INLINE4 | 前後 3 行ずつ (中心化移動平均) |
⚠ 最初の 6 行は 7 日ぶん揃いません。 SQLite は足りないぶんを無視して
あるだけで平均するので、 1 日目の ma7 は その日の値そのものになります。
グラフにするときは最初の数日を捨てるか、 揃っていないことを明示します。
フレームを UNBOUNDED PRECEDING に変えるだけです。
SUM(sales) OVER (ORDER BY sale_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cum
最終日の累計は、 期間全体の合計 (23,029,800) と一致します。
一致するかどうかは、 集計が正しいかを確かめる手軽な検算になります。
ここは事故が起きやすいところです。
-- ✕ 3 月だけの移動平均になってしまう
WITH daily AS (...)
SELECT sale_date, AVG(sales) OVER (...) FROM daily
WHERE sale_date >= '2026-03-01';
WHERE で先に行を捨ててからウィンドウを計算するので、 2 月末の 6 日ぶんが
平均に入りません。 全期間で計算してから絞りたいなら、 もう一段包みます。
WITH daily AS (...),
windowed AS (SELECT sale_date, sales, AVG(sales) OVER (...) AS ma7 FROM daily)
SELECT * FROM windowed WHERE sale_date >= '2026-03-01';
- 移動平均が階段状になる — ORDER BY を書き忘れると範囲がパーティション全体になり、
全部同じ値 (全期間の平均) が並びます。
- 日付が飛んでいる日がある — 売上のない日は行そのものが存在しないため、
「7 行前」 が 「7 日前」 とずれます。 厳密にやるなら日付の一覧を作って
LEFT JOIN しますが、 このデータは 90 日すべてに売上があるので気にせず進めて大丈夫です。
- ROUND を掛ける位置を間違える — ROUND(AVG(...) OVER (...), 1) です。
OVER 句の内側に ROUND は書けません。
日次のダッシュボードは、 生の値と移動平均を重ねて描くのが定番です。
生の値で 「今日どうだったか」、 移動平均で 「流れが変わったか」 を同時に見ます。
次のレッスン: 前月比と店舗比較 — 縦の並びを横に変える