基本統計量 — COUNT/SUM/AVG/MIN/MAX で売上データの全体像を把握する
基本統計量(Basic Statistics)は、データ全体の規模・中心・範囲を素早く把握するための最初の分析ステップです。SQLの集計関数を組み合わせることで1クエリで取得できます。
COUNT(*) -- NULL を含む全行数(テーブルのレコード数) COUNT(col) -- NULL を除いた行数(注意: * との差異に注意) SUM(col) -- 合計(NULL は自動スキップ) AVG(col) -- 平均(NULL 行を除いた行数で割る) MIN(col) / MAX(col) -- 最小値 / 最大値
ROUND(AVG(amount), 0) で整数丸めするのが実務の定番です。ROUND は四捨五入、TRUNC は切り捨てと挙動が異なります。レポート用途では必ず ROUND を指定して、桁数の意図を明示しましょう。orders テーブルから、注文データ全体の基本統計量を1行で算出してください。取得列は total_orders, total_revenue, avg_amount, min_amount, max_amount(avg_amount は整数丸め)。
| order_id | user_id | amount | order_date |
|---|---|---|---|
| 1 | 1 | 1200 | 2024-01-03 |
| 2 | 2 | 3500 | 2024-01-05 |
| 3 | 1 | 800 | 2024-01-08 |
| 4 | 3 | 5200 | 2024-01-10 |
| 5 | 2 | 2400 | 2024-01-12 |
| 6 | 4 | 1800 | 2024-01-15 |
| total_orders | total_revenue | avg_amount | min_amount | max_amount |
|---|---|---|---|---|
| 6 | 14900 | 2483 | 800 | 5200 |
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行で出力 */
LEGEND
① FROM orders — 注文データ全6行を読み込む
FROM ordersorders テーブルの全6行を読み込みます。GROUP BY のない集計クエリでは、テーブル全体が1つのグループとして扱われ、すべての行が集計関数の対象になります。| order_id | user_id | ▸ amount | order_date |
|---|---|---|---|
| 1 | 1 | 1200 | 2024-01-03 |
| 2 | 2 | 3500 | 2024-01-05 |
| 3 | 1 | 800 | 2024-01-08 |
| 4 | 3 | 5200 | 2024-01-10 |
| 5 | 2 | 2400 | 2024-01-12 |
| 6 | 4 | 1800 | 2024-01-15 |
COUNT(*) は NULL を含む全行を数え、COUNT(amount) は amount が NULL の行を除いてカウントします。amount に NULL がなければ結果は同じですが、NULL の可能性がある列を COUNT する場合は意図を明確にするため COUNT(DISTINCT col) か IS NOT NULL の確認を先に行いましょう。ROUND(2483.5, 0) = 2484(四捨五入)、TRUNC(2483.9, 0) = 2483(切り捨て)。レポート用途では ROUND が標準です。小数点以下の桁数は ROUND(value, 2) のように第2引数で制御します。COUNT(amount) は NULL 行を除いた件数を返します。「注文件数」を求めるなら COUNT(*) が正確です。COUNT(amount) は「金額が確定した注文件数」など意図的に NULL を除外したい場合にのみ使いましょう。WHERE order_date BETWEEN ... AND ... を追加して期間フィルタと組み合わせることで、任意期間の統計量を動的に確認できます。パーセンタイル分析 — PERCENTILE_CONT で中央値と分布の偏りを把握する
パーセンタイルは「データを昇順に並べたときの位置」で表現する統計量です。中央値(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)
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 は整数丸め)。
| order_id | user_id | amount | order_date |
|---|---|---|---|
| 1 | 1 | 1200 | 2024-01-03 |
| 2 | 2 | 3500 | 2024-01-05 |
| 3 | 1 | 800 | 2024-01-08 |
| 4 | 3 | 5200 | 2024-01-10 |
| 5 | 2 | 2400 | 2024-01-12 |
| 6 | 4 | 1800 | 2024-01-15 |
| avg_amount | median_amount | p75_amount | p90_amount |
|---|---|---|---|
| 2483 | 2100.0 | 3225.0 | 4350.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行で出力 */
LEGEND
① FROM orders — 元データ6行
FROM ordersPERCENTILE_CONT は ORDER BY を指定した内部ソートを行うため、FROM の順序は問いません。6行の amount 列すべてが対象になります。| order_id | ▸ amount | order_date |
|---|---|---|
| 1 | 1200 | 2024-01-03 |
| 2 | 3500 | 2024-01-05 |
| 3 | 800 | 2024-01-08 |
| 4 | 5200 | 2024-01-10 |
| 5 | 2400 | 2024-01-12 |
| 6 | 1800 | 2024-01-15 |
PERCENTILE_CONT は補間で小数を返し、PERCENTILE_DISC は元データに存在する値を返します。評価スコア(1〜5の整数)の中央値には DISC が適切で、金額・時間などの連続値には CONT が適切です。PERCENTILE_CONT(0.5) で求めると 3.5 のような「実在しない値」が返ります。整数スコアの場合は PERCENTILE_DISC(0.5) を使って実際のスコア値を取得してください。PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY response_time_ms) を定期的にモニタリングするクエリをダッシュボードに組み込むことで、エンジニアリングチームがユーザー体験の悪化を早期検出できます。グループ別標準偏差 — STDDEV で評価スコアのばらつきを数値化する
標準偏差(Standard Deviation)は「平均からのばらつきの大きさ」を表す統計量です。平均が同じでも標準偏差が大きい場合は評価が二極化(賛否両論)しており、小さい場合は評価が安定しています。
STDDEV(col) -- 標本標準偏差(n-1 で割る, SQLのデフォルト) STDDEV_POP(col) -- 母標準偏差(n で割る) VARIANCE(col) -- 標本分散(標準偏差の二乗)
product_ratings テーブルから、商品ごとの評価件数・平均評価・標準偏差を算出してください。取得列は product_id, rating_count, avg_rating, stddev_rating(各2桁丸め)、product_id 昇順で返してください。
| product_id | user_id | rating |
|---|---|---|
| A | 1 | 4 |
| A | 2 | 5 |
| A | 3 | 4 |
| A | 4 | 5 |
| A | 5 | 4 |
| B | 1 | 1 |
| B | 2 | 5 |
| B | 3 | 3 |
| B | 4 | 5 |
| B | 5 | 1 |
| product_id | rating_count | avg_rating | stddev_rating |
|---|---|---|---|
| A | 5 | 4.40 | 0.55 |
| B | 5 | 3.00 | 2.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 → 昇順 */
LEGEND
① FROM product_ratings — 評価データ10行を読み込む
FROM product_ratingsproduct_ratings テーブルの全10行を読み込みます。product_id が A と B の2種類あります。次のステップで GROUP BY によりグループに分割されます。| product_id | user_id | ▸ rating |
|---|---|---|
| A | 1 | 4 |
| A | 2 | 5 |
| A | 3 | 4 |
| A | 4 | 5 |
| A | 5 | 4 |
| B | 1 | 1 |
| B | 2 | 5 |
| B | 3 | 3 |
| B | 4 | 5 |
| B | 5 | 1 |
::numeric でキャストしないと型エラーになる場合があります。AVG も同様に double precision を返すため、ROUND と組み合わせる際は ::numeric キャストを付ける習慣を身につけましょう。CASE WHEN stddev_rating > 1.5 THEN '要注目' ELSE '安定' END のようなフラグを追加してダッシュボード上で優先度を可視化するのが定番手法です。ヒストグラム分析 — WIDTH_BUCKET で注文金額の分布をバケット化する
ヒストグラムは、連続値データを等幅の区間(バケット)に分割し、各区間の件数を集計することで分布の形を可視化します。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
((bucket-1)*width)::text || '〜' || (bucket*width-1)::text のように文字列演算で人間が読めるラベルを動的に生成できます。バケット幅が変わっても1か所の修正で済むのがポイントです。orders テーブルの amount 列を 2,000円幅で3分割(0〜5,999円)し、バケット番号・金額レンジ・件数を算出してください。CTE でバケット番号を付与してから外側クエリで集計してください。取得列は bucket, bucket_range, order_count、bucket 昇順で返してください。
| order_id | amount |
|---|---|
| 1 | 1200 |
| 2 | 3500 |
| 3 | 800 |
| 4 | 5200 |
| 5 | 2400 |
| 6 | 1800 |
| bucket | bucket_range | order_count |
|---|---|---|
| 1 | 0〜1999 | 3 |
| 2 | 2000〜3999 | 2 |
| 3 | 4000〜5999 | 1 |
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 → バケット番号昇順 */
LEGEND
① CTE: FROM orders — 注文データ6行
FROM orders (in CTE)まず CTE(共通テーブル式)内で orders テーブルを読み込みます。この段階ではまだバケットは付与されていません。| order_id | ▸ amount |
|---|---|
| 1 | 1200 |
| 2 | 3500 |
| 3 | 800 |
| 4 | 5200 |
| 5 | 2400 |
| 6 | 1800 |
WIDTH_BUCKET(6000, 0, 6000, 3) は hi=6000 が上限境界のため bucket=4(オーバーフロー)を返します。同様に 0 未満の値は bucket=0 になります。実務では WHERE amount BETWEEN 0 AND 5999 などで事前にフィルタするか、GREATEST(1, LEAST(bucket, 3)) でクランプするのが安全です。NTILE(3) OVER (ORDER BY amount) を使います。SELECT * FROM bucketed で各行のバケット番号を確認してから集計クエリを書くのが実務での安全な進め方です。CASE WHEN amount < 2000 THEN '0〜1999' WHEN amount < 4000 THEN '2000〜3999' ELSE '4000〜5999' END と書くと、バケット数や幅を変えるたびに条件分岐を全部書き直す必要があります。WIDTH_BUCKET なら 3 を 5 に変えるだけで5分割に対応でき、ラベル計算式も1か所の修正で完結します。WIDTH_BUCKET(amount, MIN(amount), MAX(amount), 3) とすると、MAX値がちょうど上限境界になりオーバーフローバケット(bucket=4)に分類されます。上限は MAX(amount)+1 以上に設定するか、集計後に bucket=count+1 の行を除外するフィルタを追加してください。GROUP BY bucket, ab_variant に拡張するだけで実現できます。移動平均 — AVG() OVER (ROWS 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日移動平均 )
N PRECEDING は「N行前まで」、CURRENT ROW は「現在行」を意味します。フレーム内の行数が足りない先頭行は利用可能な行のみで平均します(例: 1日目は1行のみ→その1行の値が移動平均になる)。RANGE BETWEEN(値ベース)と異なり ROWS BETWEEN(行数ベース)は常に正確な行数を使うため移動平均計算に適しています。daily_sales テーブルから、日次売上と3日移動平均(ma3)を算出してください。ma3 は「当日 + 直前2日」の平均(整数丸め)です。取得列は sale_date, revenue, ma3、sale_date 昇順で返してください。
| sale_date | revenue |
|---|---|
| 2024-01-01 | 1000 |
| 2024-01-02 | 1200 |
| 2024-01-03 | 900 |
| 2024-01-04 | 1500 |
| 2024-01-05 | 1300 |
| 2024-01-06 | 1100 |
| 2024-01-07 | 1600 |
| sale_date | revenue | ma3 |
|---|---|---|
| 2024-01-01 | 1000 | 1000 |
| 2024-01-02 | 1200 | 1100 |
| 2024-01-03 | 900 | 1033 |
| 2024-01-04 | 1500 | 1200 |
| 2024-01-05 | 1300 | 1233 |
| 2024-01-06 | 1100 | 1300 |
| 2024-01-07 | 1600 | 1333 |
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 → 日付昇順で出力 */
LEGEND
① FROM daily_sales — 売上データ
FROM daily_salesdaily_sales テーブルの全7行を読み込みます。まだウィンドウ関数は適用されていません。| sale_date | revenue |
|---|---|
| 2024-01-01 | 1000 |
| 2024-01-02 | 1200 |
| 2024-01-03 | 900 |
| 2024-01-04 | 1500 |
| 2024-01-05 | 1300 |
| 2024-01-06 | 1100 |
| 2024-01-07 | 1600 |
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW は物理的な行数ベースでフレームを確定します。RANGE BETWEEN は値ベースのため、同じ日付が複数行ある場合に同一値の行をまとめて扱います。移動平均計算には ROWS BETWEEN が適切で、「同じ日付の行が複数あっても正確に N 行で計算できる」保証があります。ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING(全行)になります。移動平均では必ず ORDER BY を指定してフレームを確定させてください。ORDER BY なしで AVG OVER () を書くと全行の平均が各行に繰り返される(CUM AVG ではなく全体 AVG)ので注意です。ROWS BETWEEN 6 PRECEDING AND CURRENT ROW で7日移動平均に変更するだけです。複数の期間の移動平均を並べる場合は OVER 句を複数書いても問題ありません(SQL優化により1回のスキャンに最適化されます)。AVG(revenue) OVER (ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) と ORDER BY を省略すると、行の物理順序が不定になり移動平均の結果が実行のたびに変わる可能性があります。OVER 句には必ず ORDER BY を指定してください。ROUND(AVG(revenue), 0) OVER (...) と書くと構文エラーになります。正しくは ROUND(AVG(revenue) OVER (...), 0)— OVER 句はウィンドウ関数(AVG など)に対して適用するもので、ROUND は外側でラップします。PARTITION BY product_category を OVER 句に追加することで、カテゴリ別の移動平均を1クエリで算出できます。さらに LAG(ma3, 7) OVER (ORDER BY sale_date) と組み合わせると「7日前の移動平均との差分」で成長率の動的な計算も実現できます。