NTILE() — 購入額を4分位に分割してユーザーをセグメント化する
NTILE(N) は全行を 最大 N 個のサイズ差が1行以内のグループ(バケット)に分割し、各行に 1〜N の範囲でグループ番号を割り当てます。
ROW_NUMBER / RANK / DENSE_RANK と同じ「番号付け系」関数の仲間です。
NTILE(4) OVER(ORDER BY total_purchase DESC) -- 購入額を高い順に並べて4等分 -- 1=最高額グループ(上位25%) -- 4=最低額グループ(下位25%)
以下の customers テーブルを使用し、購入総額(total_purchase)の高い順に全ユーザーを 4段階のティア (tier) に分類してください。
tier 1 が最高額、tier 4 が最低額グループとなるよう降順で分割すること。
| user_id | name | total_purchase |
|---|---|---|
| U1 | 田中 | 48000 |
| U2 | 鈴木 | 12000 |
| U3 | 佐藤 | 95000 |
| U4 | 高橋 | 33000 |
| U5 | 伊藤 | 71000 |
| U6 | 渡辺 | 8000 |
| user_id | name | total_purchase | tier |
|---|---|---|---|
| U3 | 佐藤 | 95000 | 1 |
| U5 | 伊藤 | 71000 | 1 |
| U1 | 田中 | 48000 | 2 |
| U4 | 高橋 | 33000 | 2 |
| U2 | 鈴木 | 12000 | 3 |
| U6 | 渡辺 | 8000 | 4 |
PERCENT_RANK() — スコアが全体の何%に位置するかを計算する
PERCENT_RANK() は各行の「全体の中での相対的な位置」を 0.0〜1.0 の小数で返します。
複数行なら最小値の行は 0.0(0パーセンタイル)、最大値の行は 1.0(100パーセンタイル)になります。1行だけの場合は 0.0 です。
PERCENT_RANK() OVER(ORDER BY score) -- 計算式: (rank - 1) / (total_rows - 1) -- 例: 5行中3位 → (3-1)/(5-1) = 0.5 = 50パーセンタイル
CUME_DIST()(累積分布)は「自分以下の行が全体の何割か」を返し、0 より大きく 1 以下の値になります。PERCENT_RANK は順位を (rank - 1) / (total_rows - 1) で正規化して 0 始まりになります。両者は同点がなくても値が異なり、同点(タイ)が生じると順位の扱いによる差がさらに重要になります。以下の exam_scores テーブルを使用し、各社員のテストスコアが全体の中で何%に位置するか(パーセンタイル)を求めてください。
出力は小数点以下1桁(%換算)で表示し、スコアの昇順で出力すること。
| employee_id | name | score |
|---|---|---|
| E1 | 田中 | 70 |
| E2 | 鈴木 | 90 |
| E3 | 佐藤 | 60 |
| E4 | 高橋 | 85 |
| E5 | 伊藤 | 75 |
| name | score | percentile |
|---|---|---|
| 佐藤 | 60 | 0.0 |
| 田中 | 70 | 25.0 |
| 伊藤 | 75 | 50.0 |
| 高橋 | 85 | 75.0 |
| 鈴木 | 90 | 100.0 |
WITH句(CTE)— ウィンドウ関数をモジュール化して累計・構成比を求める
ウィンドウ関数を含む長いクエリをサブクエリで書くと、ネストが深くなり読みづらくなります。WITH 句(CTE: Common Table Expression)を使うと、「まずウィンドウ関数で中間テーブルを作り、次にそれを使って計算する」という2ステップを上から下へ自然に読めるモダンな書き方になります。
WITH cte_name AS ( -- ① ウィンドウ関数で中間テーブルを作成 SELECT ..., SUM(col) OVER(...) AS running_total FROM tbl ) -- ② 中間テーブルを参照して追加計算 SELECT ..., running_total / total * 100 AS pct FROM cte_name;
SUM(revenue) OVER() のように OVER 内に何も書かないと、全行を対象にした集計が計算されます(= テーブル全体の合計)。これを利用して「全体合計」と「累計」を1つのCTE内で同時に計算できます。以下の monthly_revenue テーブルを使用し、WITH 句を活用して月ごとに以下の3つの値を出力してください。
1. running_total — 1月からの累計売上
2. monthly_pct — その月の売上が全体に占める割合(%、小数点1桁)
| month | revenue |
|---|---|
| 01月 | 200 |
| 02月 | 250 |
| 03月 | 180 |
| 04月 | 320 |
| 05月 | 290 |
| month | revenue | running_total | monthly_pct |
|---|---|---|---|
| 01月 | 200 | 200 | 16.1 |
| 02月 | 250 | 450 | 20.2 |
| 03月 | 180 | 630 | 14.5 |
| 04月 | 320 | 950 | 25.8 |
| 05月 | 290 | 1240 | 23.4 |
LAG() + LEAD() 同時活用 — 前後の値を比較して「ピーク日」を自動検出する
LAG(前の行)と LEAD(次の行)を 同時に 使うことで、「前後の値との3点比較」が可能になります。これはデータのピーク(極大値)・谷(極小値)検出、急変検知など、時系列分析で頻出のパターンです。
LAG(amount) OVER(ORDER BY date) AS prev_amount -- 1行前の値 LEAD(amount) OVER(ORDER BY date) AS next_amount -- 1行後の値 -- CASE WHEN で3点比較 → ピーク判定 CASE WHEN amount > prev_amount AND amount > next_amount THEN 'PEAK' END
NULL との比較(> NULL)は UNKNOWN となり、CASE WHEN では真として扱われないため、先頭・末尾行は自動的にピーク判定から除外されます。これは意図通りの動作です。以下の daily_sales テーブルを使用し、各日付の「前日の売上 prev_amount」「翌日の売上 next_amount」を横に並べ、さらに前後両方の日より売上が高い日に 'PEAK' を、それ以外は空文字 '' を is_peak カラムとして出力してください。
| date | amount |
|---|---|
| 04-01 | 80 |
| 04-02 | 160 |
| 04-03 | 110 |
| 04-04 | 190 |
| 04-05 | 130 |
| 04-06 | 175 |
| date | amount | prev_amount | next_amount | is_peak |
|---|---|---|---|---|
| 04-01 | 80 | NULL | 160 | |
| 04-02 | 160 | 80 | 110 | PEAK |
| 04-03 | 110 | 160 | 190 | |
| 04-04 | 190 | 110 | 130 | PEAK |
| 04-05 | 130 | 190 | 175 | |
| 04-06 | 175 | 130 | NULL |
PARTITION BY × フレーム指定 — 店舗ごとに独立した移動平均を計算する
PARTITION BY(グループ分割)と ROWS BETWEEN(フレーム指定)を組み合わせることで、「各グループの中だけで動くウィンドウ」を実現できます。これはウィンドウ関数の中でも最も実務的なパターンのひとつです。
AVG(amount) OVER( PARTITION BY store_id -- 店舗単位でウィンドウを区切る ORDER BY date -- 各パーティション内で日付順に並べる ROWS BETWEEN 1 PRECEDING AND CURRENT ROW -- 直前1行 + 現在行 = 2日間平均 )
以下の store_daily_sales テーブルを使用し、店舗(store_id)ごとに独立した「直近2日間の移動平均(moving_avg_2d)」を計算してください。
つまり、店舗が切り替わった時点でウィンドウが必ずリセットされる必要があります。
| store_id | date | amount |
|---|---|---|
| S1 | 04-01 | 100 |
| S1 | 04-02 | 140 |
| S1 | 04-03 | 120 |
| S1 | 04-04 | 160 |
| S2 | 04-01 | 200 |
| S2 | 04-02 | 180 |
| S2 | 04-03 | 220 |
| S2 | 04-04 | 190 |
| store_id | date | amount | moving_avg_2d |
|---|---|---|---|
| S1 | 04-01 | 100 | 100.0 |
| S1 | 04-02 | 140 | 120.0 |
| S1 | 04-03 | 120 | 130.0 |
| S1 | 04-04 | 160 | 140.0 |
| S2 | 04-01 | 200 | 200.0 |
| S2 | 04-02 | 180 | 190.0 |
| S2 | 04-03 | 220 | 200.0 |
| S2 | 04-04 | 190 | 205.0 |