SQL 統計分析 — 集計関数・標準偏差・移動平均の基礎

基礎統計分析集計関数パーセンタイル標準偏差移動平均PostgreSQL対応全5問
1 / 5 · 学習中 0 / 5 問完了
QUESTION 1

基本統計量 — COUNT/SUM/AVG/MIN/MAX で売上データの全体像を把握する

COUNT/SUM/AVGMIN/MAX基本統計量売上分析
前提知識

基本統計量(Basic Statistics)は、データ全体の規模・中心・範囲を素早く把握するための最初の分析ステップです。SQLの集計関数を組み合わせることで1クエリで取得できます。

COUNT(*)          -- NULL を含む全行数(テーブルのレコード数)
COUNT(col)        -- NULL を除いた行数(注意: * との差異に注意)
SUM(col)          -- 合計(NULL は自動スキップ)
AVG(col)          -- 平均(NULL 行を除いた行数で割る)
MIN(col) / MAX(col)  -- 最小値 / 最大値
ROUND(value, n) で出力精度を制御:AVG は小数を返すため ROUND(AVG(amount), 0) で整数丸めするのが実務の定番です。ROUND は四捨五入、TRUNC は切り捨てと挙動が異なります。レポート用途では必ず ROUND を指定して、桁数の意図を明示しましょう。
問題

orders テーブルから、注文データ全体の基本統計量を1行で算出してください。取得列は total_orders, total_revenue, avg_amount, min_amount, max_amount(avg_amount は整数丸め)。

使用テーブル
▸ orders(6行)
order_iduser_idamountorder_date
1112002024-01-03
2235002024-01-05
318002024-01-08
4352002024-01-10
5224002024-01-12
6418002024-01-15
期待出力
total_orderstotal_revenueavg_amountmin_amountmax_amount
61490024838005200
模範解答コード
SELECT
  COUNT(*)                        AS total_orders,   -- NULL に無関係に全行をカウント
  SUM(amount)                     AS total_revenue,  -- NULL は自動スキップして合計
  ROUND(AVG(amount), 0)          AS avg_amount,     -- 平均を整数に丸め(四捨五入)
  MIN(amount)                     AS min_amount,     -- 最小注文額
  MAX(amount)                     AS max_amount      -- 最大注文額
FROM orders;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM orders            → 6行を読込
  2. COUNT/SUM/AVG/MIN/MAX  → 集計関数を評価
  3. ROUND(AVG(amount), 0)  → 小数第0位で四捨五入
  4. SELECT                 → 集計結果を1行で出力
  */
解説(テーブル変化・ポイント)
SELECT COUNT(*) AS total_orders, SUM(amount) AS total_revenue, ROUND(AVG(amount), 0) AS avg_amount, MIN(amount) AS min_amount, MAX(amount) AS max_amount FROM orders;
LEGEND
データ取得・読込対象
① FROM orders — 注文データ全6行を読み込む
FROM ordersorders テーブルの全6行を読み込みます。GROUP BY のない集計クエリでは、テーブル全体が1つのグループとして扱われ、すべての行が集計関数の対象になります。
1 / 4
order_iduser_id▸ amountorder_date
1112002024-01-03
2235002024-01-05
318002024-01-08
4352002024-01-10
5224002024-01-12
6418002024-01-15
orders: 6行(全行が集計対象)
学習ポイント
COUNT(*) と COUNT(col) の挙動の違い:COUNT(*) は NULL を含む全行を数え、COUNT(amount) は amount が NULL の行を除いてカウントします。amount に NULL がなければ結果は同じですが、NULL の可能性がある列を COUNT する場合は意図を明確にするため COUNT(DISTINCT col) か IS NOT NULL の確認を先に行いましょう
SUM と AVG の NULL スキップ:SUM と AVG はどちらも NULL 行を自動的に除外して計算します。ただし AVG の分母(行数)も NULL 行を含まないため、NULL が多い列の AVG はデータ全体の平均ではなく「値が存在する行の平均」であることを意識してください。
ROUND vs TRUNC の使い分け:ROUND(2483.5, 0) = 2484(四捨五入)、TRUNC(2483.9, 0) = 2483(切り捨て)。レポート用途では ROUND が標準です。小数点以下の桁数は ROUND(value, 2) のように第2引数で制御します。
アンチパターン
COUNT(amount) で NULL 行を意図せず除外してしまう:amount に NULL が含まれる場合、COUNT(amount) は NULL 行を除いた件数を返します。「注文件数」を求めるなら COUNT(*) が正確です。COUNT(amount) は「金額が確定した注文件数」など意図的に NULL を除外したい場合にのみ使いましょう。
平均だけで代表値を判断する:avg_amount=2483 に対して min=800・max=5200 と幅が広い場合、平均は外れ値(5200の注文)に引き上げられています。基本統計量は常にセットで確認し、min/max の乖離が大きければ中央値(Q2)やヒストグラム(Q4)で分布を深掘りしましょう。
実務コラム:EDA(探索的データ分析)の出発点として
新規データを受け取ったとき、最初に実行するのがこの基本統計量クエリです。total_orders でデータ件数を把握し、min/max の差(レンジ)で外れ値の存在を確認し、avg と中央値の差で分布の歪みを診断する——この3ステップが分析の土台になります。ダッシュボードでは WHERE order_date BETWEEN ... AND ... を追加して期間フィルタと組み合わせることで、任意期間の統計量を動的に確認できます。
QUESTION 2

パーセンタイル分析 — PERCENTILE_CONT で中央値と分布の偏りを把握する

PERCENTILE_CONTWITHIN GROUP中央値外れ値検出
前提知識

パーセンタイルは「データを昇順に並べたときの位置」で表現する統計量です。中央値(p50)は外れ値の影響を受けない頑健な代表値として実務で多用されます。PostgreSQL では PERCENTILE_CONT(連続補間)と PERCENTILE_DISC(最近傍値)の2種類があります。

-- 連続補間:隣接する値を線形補間して返す(小数になり得る)
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount)

-- 離散値:実際に存在する値のうち最近傍を返す(常に元データの値)
PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY amount)
WITHIN GROUP の役割:PERCENTILE_CONT は順序集合集計関数(Ordered-Set Aggregate)であり、WITHIN GROUP (ORDER BY col) で「どの列のどの順番でパーセンタイルを求めるか」を指定します。この構文は BigQuery・Snowflake・Redshift でも同様に使えます。
問題

orders テーブルの amount 列から、平均・中央値・第3四分位(p75)・上位10%閾値(p90)を1行で算出してください。取得列は avg_amount, median_amount, p75_amount, p90_amount(avg_amount は整数丸め)。

使用テーブル
▸ orders(6行)
order_iduser_idamountorder_date
1112002024-01-03
2235002024-01-05
318002024-01-08
4352002024-01-10
5224002024-01-12
6418002024-01-15
期待出力
avg_amountmedian_amountp75_amountp90_amount
24832100.03225.04350.0
模範解答コード
SELECT
  ROUND(AVG(amount), 0)                                           AS avg_amount,
  PERCENTILE_CONT(0.5)  WITHIN GROUP (ORDER BY amount)           AS median_amount,  -- 中央値
  PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY amount)           AS p75_amount,     -- 第3四分位
  PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY amount)           AS p90_amount      -- 上位10%閾値
FROM orders;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM orders            → 6行読込
  2. AVG(amount)            → 平均を計算
  3. PERCENTILE_CONT(0.5)   → 中央値を補間
  4. PERCENTILE_CONT(0.75)  → 第3四分位を補間
  5. PERCENTILE_CONT(0.90)  → 90パーセンタイルを補間
  6. SELECT                 → 1行で出力
  */
解説(テーブル変化・ポイント)
SELECT ROUND(AVG(amount), 0) AS avg_amount, PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount) AS median_amount, PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY amount) AS p75_amount, PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY amount) AS p90_amount FROM orders;
LEGEND
データ取得・読込対象
① FROM orders — 元データ6行
FROM ordersPERCENTILE_CONT は ORDER BY を指定した内部ソートを行うため、FROM の順序は問いません。6行の amount 列すべてが対象になります。
1 / 4
order_id▸ amountorder_date
112002024-01-03
235002024-01-05
38002024-01-08
452002024-01-10
524002024-01-12
618002024-01-15
orders: 6行
学習ポイント
avg と median の乖離が外れ値のシグナル:avg(2483) と median(2100) の差が 383 ある場合、右裾(高額)に外れ値が存在することを示します。逆に median > avg なら左裾に外れ値があります。まず両者を比較することが分布の歪み検出の最速手法です。
PERCENTILE_CONT vs PERCENTILE_DISC の使い分け:PERCENTILE_CONT は補間で小数を返し、PERCENTILE_DISC は元データに存在する値を返します。評価スコア(1〜5の整数)の中央値には DISC が適切で、金額・時間などの連続値には CONT が適切です。
p75 と p90 の実務活用:p90(上位10%の閾値)はSLA設計でよく使われます。「APIレスポンスタイムのp90が200ms以下」のような指標は平均では捉えられない「遅いリクエスト体験」を数値化します。ユーザー体験の観点では p95・p99 まで確認するのが実務の標準です。
アンチパターン
平均値だけで「典型的なユーザー」を定義する:avg_amount=2483 を「標準的な注文額」として施策設計すると、実際には 2100円付近(中央値)の注文が多いのに 5200円の高額注文が平均を引き上げているケースを見逃します。セグメント施策には中央値・パーセンタイルを必ず確認してください。
PERCENTILE_DISC を使うべき場面で CONT を使う:5段階評価(1・2・3・4・5)の中央値を PERCENTILE_CONT(0.5) で求めると 3.5 のような「実在しない値」が返ります。整数スコアの場合は PERCENTILE_DISC(0.5) を使って実際のスコア値を取得してください。
実務コラム:SLAとパーセンタイルの関係
Web APIのレイテンシ監視では p50(中央値)・p95・p99 がSLAの標準指標です。平均レイテンシが 100ms でも p99 が 3000ms なら 1% のユーザーが深刻な遅延を体験しています。p99 の改善は p50 を見るだけでは気づけないのがポイントです。SQLで PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY response_time_ms) を定期的にモニタリングするクエリをダッシュボードに組み込むことで、エンジニアリングチームがユーザー体験の悪化を早期検出できます。
QUESTION 3

グループ別標準偏差 — STDDEV で評価スコアのばらつきを数値化する

STDDEVGROUP BY標準偏差商品評価分析
前提知識

標準偏差(Standard Deviation)は「平均からのばらつきの大きさ」を表す統計量です。平均が同じでも標準偏差が大きい場合は評価が二極化(賛否両論)しており、小さい場合は評価が安定しています。

STDDEV(col)      -- 標本標準偏差(n-1 で割る, SQLのデフォルト)
STDDEV_POP(col)  -- 母標準偏差(n で割る)
VARIANCE(col)    -- 標本分散(標準偏差の二乗)
標本 vs 母の選び方:分析対象が「全ユーザーの一部サンプル」なら標本(STDDEV)、「全ユーザー全件」なら母集団(STDDEV_POP)を使います。実務のデータ分析では STDDEV が一般的です(サンプルであることが多いため)。標準偏差は平均と同じ単位(例: 評価スコアなら 0〜5 の範囲)なので直感的に解釈できます。
問題

product_ratings テーブルから、商品ごとの評価件数・平均評価・標準偏差を算出してください。取得列は product_id, rating_count, avg_rating, stddev_rating(各2桁丸め)、product_id 昇順で返してください。

使用テーブル
▸ product_ratings(10行)
product_iduser_idrating
A14
A25
A34
A45
A54
B11
B25
B33
B45
B51
期待出力
product_idrating_countavg_ratingstddev_rating
A54.400.55
B53.002.00
模範解答コード
SELECT
  product_id,
  COUNT(*)                                 AS rating_count,
  ROUND(AVG(rating)::numeric, 2)           AS avg_rating,    -- numeric にキャストして ROUND
  ROUND(STDDEV(rating)::numeric, 2)        AS stddev_rating  -- 標本標準偏差(n-1 で割る)
FROM   product_ratings
GROUP BY product_id
ORDER BY product_id;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM product_ratings    → 10行読込
  2. GROUP BY product_id     → 2グループに分割
  3. COUNT/AVG/STDDEV        → 集計関数を評価(標本標準偏差)
  4. ROUND(...::numeric, 2)  → 小数2桁に丸め
  5. ORDER BY product_id     → 昇順
  */
解説(テーブル変化・ポイント)
SELECT product_id, COUNT(*) AS rating_count, ROUND(AVG(rating)::numeric, 2) AS avg_rating, ROUND(STDDEV(rating)::numeric, 2) AS stddev_rating FROM product_ratings GROUP BY product_id ORDER BY product_id;
LEGEND
データ取得・読込対象
① FROM product_ratings — 評価データ10行を読み込む
FROM product_ratingsproduct_ratings テーブルの全10行を読み込みます。product_id が A と B の2種類あります。次のステップで GROUP BY によりグループに分割されます。
1 / 4
product_iduser_id▸ rating
A14
A25
A34
A45
A54
B11
B25
B33
B45
B51
product_ratings: 10行
学習ポイント
標本標準偏差(STDDEV)と母標準偏差(STDDEV_POP)の違い:STDDEV は n-1 で割る(ベッセルの補正)、STDDEV_POP は n で割ります。商品Aの場合: STDDEV=sqrt(0.3)≈0.55、STDDEV_POP=sqrt(0.24)≈0.49 と若干異なります。全データを保持している場合は STDDEV_POP、ランダムサンプルの場合は STDDEV を使います。
::numeric キャストが必要な理由:PostgreSQL の STDDEV は double precision を返しますが、ROUND の第1引数は numeric 型を期待します。::numeric でキャストしないと型エラーになる場合があります。AVG も同様に double precision を返すため、ROUND と組み合わせる際は ::numeric キャストを付ける習慣を身につけましょう。
変動係数(CV)への応用:stddev を avg で割った「変動係数 CV = STDDEV / AVG × 100」を使うと、平均の大きさが異なる複数の商品のばらつきを相対比較できます。商品AのCV≈12.5%、商品BのCV≈66.7%——CVが高い商品は品質改善や説明文の見直しが有効です。
アンチパターン
平均評価だけで商品の質を判断する:商品Bの avg_rating=3.0 を見て「中程度の評価」と判断すると、実態(1点が2件・5点が2件・3点が1件という二極化)を見落とします。必ず stddev をセットで確認し、平均が高くても stddev が大きい商品は「一部ユーザーに刺さるニッチ商品」として別施策を検討しましょう。
VARIANCE(分散)を直接ユーザーに見せる:VARIANCE は標準偏差の二乗であり、単位が元データと異なります(評価スコアなら「点²」)。ユーザー向けレポートには必ず STDDEV(元データと同じ単位)を使ってください。VARIANCE は計算の中間値としてのみ使用する認識で問題ありません。
実務コラム:評価ばらつきが示すビジネスインサイト
標準偏差が高い商品は「改善余地がある」サインです。低評価レビューを分析することで、何が満足度を下げているか(配送・品質・説明文の乖離など)を特定できます。一方で stddev が低く avg が高い商品は「コア顧客向けの安定した優良商品」として広告予算を集中させる判断材料になります。実務では CASE WHEN stddev_rating > 1.5 THEN '要注目' ELSE '安定' END のようなフラグを追加してダッシュボード上で優先度を可視化するのが定番手法です。
QUESTION 4

ヒストグラム分析 — WIDTH_BUCKET で注文金額の分布をバケット化する

WIDTH_BUCKETCTEヒストグラム分布分析
前提知識

ヒストグラムは、連続値データを等幅の区間(バケット)に分割し、各区間の件数を集計することで分布の形を可視化します。PostgreSQL の WIDTH_BUCKET 関数を使うと、CASE WHEN を多数並べることなく動的にバケットを生成できます。

WIDTH_BUCKET(value, lo, hi, count)
-- value: 対象の値
-- lo:    範囲の下限(バケット1の開始)
-- hi:    範囲の上限(バケット外の境界)
-- count: バケットの分割数
-- 戻り値: バケット番号(1〜count)/ 範囲外は 0 または count+1

: WIDTH_BUCKET(1800, 0, 6000, 3)
→ バケット幅 = 6000/3 = 2000 → [0,2000) [2000,4000) [4000,6000)
→ 1800 は [0,2000) なので bucket=1
バケット番号からラベルへの変換:WIDTH_BUCKET が返すのは整数のバケット番号です。((bucket-1)*width)::text || '〜' || (bucket*width-1)::text のように文字列演算で人間が読めるラベルを動的に生成できます。バケット幅が変わっても1か所の修正で済むのがポイントです。
問題

orders テーブルの amount 列を 2,000円幅で3分割(0〜5,999円)し、バケット番号・金額レンジ・件数を算出してください。CTE でバケット番号を付与してから外側クエリで集計してください。取得列は bucket, bucket_range, order_count、bucket 昇順で返してください。

使用テーブル
▸ orders(6行)
order_idamount
11200
23500
3800
45200
52400
61800
期待出力
bucketbucket_rangeorder_count
10〜19993
22000〜39992
34000〜59991
模範解答コード
WITH bucketed AS (
  -- 各注文にバケット番号を付与(6000円を3等分: 幅2000円/バケット)
  SELECT
    WIDTH_BUCKET(amount, 0, 6000, 3)  AS bucket,
    amount
  FROM orders
)
SELECT
  bucket,
  ((bucket - 1) * 2000)::text || '〜' ||
    (bucket * 2000 - 1)::text    AS bucket_range,  -- 動的ラベル生成
  COUNT(*)                        AS order_count
FROM   bucketed
GROUP BY bucket
ORDER BY bucket;

/*
  実行順序(SQLの論理的な評価順):
  1. CTE bucketed
  2. 外側クエリ
  3. ORDER BY bucket  → バケット番号昇順
  */
解説(テーブル変化・ポイント)
WITH bucketed AS ( SELECT WIDTH_BUCKET(amount, 0, 6000, 3) AS bucket, amount FROM orders ) SELECT bucket, ((bucket - 1) * 2000)::text || '〜' || (bucket * 2000 - 1)::text AS bucket_range, COUNT(*) AS order_count FROM bucketed GROUP BY bucket ORDER BY bucket;
LEGEND
データ取得・読込対象
① CTE: FROM orders — 注文データ6行
FROM orders (in CTE)まず CTE(共通テーブル式)内で orders テーブルを読み込みます。この段階ではまだバケットは付与されていません。
1 / 5
order_id▸ amount
11200
23500
3800
45200
52400
61800
orders: 6行
学習ポイント
WIDTH_BUCKET の境界処理(範囲外の挙動):WIDTH_BUCKET(6000, 0, 6000, 3) は hi=6000 が上限境界のため bucket=4(オーバーフロー)を返します。同様に 0 未満の値は bucket=0 になります。実務では WHERE amount BETWEEN 0 AND 5999 などで事前にフィルタするか、GREATEST(1, LEAST(bucket, 3)) でクランプするのが安全です。
等幅 vs 等頻度バケット:WIDTH_BUCKET は等幅バケット(各区間の幅が同じ)を生成します。データが偏っているとき(例: 低価格帯に集中)は特定バケットに件数が偏ります。件数を均等にしたい場合は等頻度バケットとして NTILE(3) OVER (ORDER BY amount) を使います。
CTEで段階的に処理する設計の価値:CTE bucketed でバケット番号付与 → 外側クエリでラベル生成・集計と2段階に分けることで、デバッグが容易になります。まず SELECT * FROM bucketed で各行のバケット番号を確認してから集計クエリを書くのが実務での安全な進め方です。
アンチパターン
ハードコードした CASE WHEN でバケットを定義する:CASE WHEN amount < 2000 THEN '0〜1999' WHEN amount < 4000 THEN '2000〜3999' ELSE '4000〜5999' END と書くと、バケット数や幅を変えるたびに条件分岐を全部書き直す必要があります。WIDTH_BUCKET なら 35 に変えるだけで5分割に対応でき、ラベル計算式も1か所の修正で完結します。
上限境界に最大値をそのまま使う:WIDTH_BUCKET(amount, MIN(amount), MAX(amount), 3) とすると、MAX値がちょうど上限境界になりオーバーフローバケット(bucket=4)に分類されます。上限は MAX(amount)+1 以上に設定するか、集計後に bucket=count+1 の行を除外するフィルタを追加してください。
実務コラム:A/Bテスト結果の金額セグメント別分析
ヒストグラム分析の実務活用例として、A/Bテストの結果を金額セグメント別に分解するケースがあります。「全体の平均購入額は施策Aが高い」としても、実は高額帯ユーザーにだけ効果があり低額帯では逆効果だった——というパターンはよく起きます。WIDTH_BUCKET で金額を3〜5分割し、セグメント別のコンバージョン率や購入額平均を比較することで、施策の効果が刺さるユーザー層を特定できます。このセグメント分析は GROUP BY bucket, ab_variant に拡張するだけで実現できます。
QUESTION 5

移動平均 — AVG() OVER (ROWS BETWEEN) で日次売上トレンドを平滑化する

AVG OVERROWS BETWEEN移動平均ウィンドウ関数
前提知識

移動平均(Moving Average)は、日次データの短期的なノイズを除去してトレンドを把握するための統計手法です。PostgreSQL のウィンドウ関数 AVG() OVER (... ROWS BETWEEN) を使うと、1クエリで全行の移動平均を算出できます。

AVG(col) OVER (
  ORDER BY date_col
  ROWS BETWEEN 2 PRECEDING AND CURRENT ROW   -- 直前2行+現在行 = 3日移動平均
)
ROWS BETWEEN のフレーム指定:N PRECEDING は「N行前まで」、CURRENT ROW は「現在行」を意味します。フレーム内の行数が足りない先頭行は利用可能な行のみで平均します(例: 1日目は1行のみ→その1行の値が移動平均になる)。RANGE BETWEEN(値ベース)と異なり ROWS BETWEEN(行数ベース)は常に正確な行数を使うため移動平均計算に適しています。
問題

daily_sales テーブルから、日次売上と3日移動平均(ma3)を算出してください。ma3 は「当日 + 直前2日」の平均(整数丸め)です。取得列は sale_date, revenue, ma3、sale_date 昇順で返してください。

使用テーブル
▸ daily_sales(7行)
sale_daterevenue
2024-01-011000
2024-01-021200
2024-01-03900
2024-01-041500
2024-01-051300
2024-01-061100
2024-01-071600
期待出力
sale_daterevenuema3
2024-01-0110001000
2024-01-0212001100
2024-01-039001033
2024-01-0415001200
2024-01-0513001233
2024-01-0611001300
2024-01-0716001333
模範解答コード
SELECT
  sale_date,
  revenue,
  ROUND(
    AVG(revenue) OVER (
      ORDER BY sale_date
      ROWS BETWEEN 2 PRECEDING AND CURRENT ROW  -- 直近3日(自分含む)
    ), 0
  ) AS ma3
FROM   daily_sales
ORDER BY sale_date;

/*
  実行順序(SQLの論理的な評価順):
  1. FROM daily_sales         → 7行読込
  2. ORDER BY sale_date       → 日付昇順で行順を確定
  3. AVG(revenue) OVER (...)  → 自行+直前2行で移動平均
  4. ROUND(..., 0)            → 整数丸め
  5. SELECT + ORDER BY        → 日付昇順で出力
  */
解説(テーブル変化・ポイント)
SELECT sale_date, revenue, ROUND( AVG(revenue) OVER ( ORDER BY sale_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ), 0 ) AS ma3 FROM daily_sales ORDER BY sale_date;
LEGEND
データ取得・読込対象
① FROM daily_sales — 売上データ
FROM daily_salesdaily_sales テーブルの全7行を読み込みます。まだウィンドウ関数は適用されていません。
1 / 6
sale_daterevenue
2024-01-011000
2024-01-021200
2024-01-03900
2024-01-041500
2024-01-051300
2024-01-061100
2024-01-071600
daily_sales: 7行
学習ポイント
ROWS BETWEEN vs RANGE BETWEEN の違い:ROWS BETWEEN 2 PRECEDING AND CURRENT ROW は物理的な行数ベースでフレームを確定します。RANGE BETWEEN は値ベースのため、同じ日付が複数行ある場合に同一値の行をまとめて扱います。移動平均計算には ROWS BETWEEN が適切で、「同じ日付の行が複数あっても正確に N 行で計算できる」保証があります。
ORDER BY なしウィンドウはデフォルトフレームが変わる:OVER 句に ORDER BY を書かない場合、デフォルトのフレームは ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING(全行)になります。移動平均では必ず ORDER BY を指定してフレームを確定させてください。ORDER BY なしで AVG OVER () を書くと全行の平均が各行に繰り返される(CUM AVG ではなく全体 AVG)ので注意です。
移動平均の期間選択:3日移動平均は短期トレンドに反応しやすく、7日移動平均は週の曜日効果を除去するのに適しています。実務では ROWS BETWEEN 6 PRECEDING AND CURRENT ROW で7日移動平均に変更するだけです。複数の期間の移動平均を並べる場合は OVER 句を複数書いても問題ありません(SQL優化により1回のスキャンに最適化されます)。
アンチパターン
ORDER BY なしでウィンドウを定義する:AVG(revenue) OVER (ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) と ORDER BY を省略すると、行の物理順序が不定になり移動平均の結果が実行のたびに変わる可能性があります。OVER 句には必ず ORDER BY を指定してください。
ROUND を OVER 句の外に適用し忘れる:ROUND(AVG(revenue), 0) OVER (...) と書くと構文エラーになります。正しくは ROUND(AVG(revenue) OVER (...), 0)— OVER 句はウィンドウ関数(AVG など)に対して適用するもので、ROUND は外側でラップします。
実務コラム:移動平均のダッシュボード活用
日次売上ダッシュボードに移動平均を追加すると、実績値(棒グラフ)とトレンド線(折れ線)を1チャートで表現できます。月曜日に売上が落ちる曜日効果や、プロモーション直後のスパイクを移動平均が平滑化するため、本当の成長トレンドかノイズかを区別しやすくなります。実務では PARTITION BY product_category を OVER 句に追加することで、カテゴリ別の移動平均を1クエリで算出できます。さらに LAG(ma3, 7) OVER (ORDER BY sale_date) と組み合わせると「7日前の移動平均との差分」で成長率の動的な計算も実現できます。