データベーススペシャリスト試験の午後問題、特にSQLの記述で「どう書けばいいんだ…」と手が止まってしまった経験はありませんか?近年の試験では、単なるデータ抽出だけでなく、より高度で複雑なデータ分析力が問われる傾向が強まっています。その合格の鍵を握るのが、最頻出テーマである「Window関数」です。
Window関数は、一言でいえば「集計しつつも、元の行を失わずに結果を各行へ返せる便利な機能」です。従来のGROUP BY
句では難しかった「部門ごとの売上ランキングの算出」や「前月の売上との比較」といった処理を、驚くほどシンプルに記述できます。
しかし、その強力さゆえに OVER()
や PARTITION BY
といった独特の構文に戸惑い、苦手意識を持つ受験者も少なくありません。
この記事では、そんなWindow関数をゼロから理解できるよう、RANK
による順位付けからLAG
を使った行間比較、そして最大の難関であるフレーム句(ROWS BETWEEN
)まで、豊富な図解と実践的なSQLの具体例を交えて徹底的に解説します。
本記事を読み終える頃には、Window関数はあなたの強力な武器となり、午後試験のSQL問題に対する視界が大きく開けているはずです。
目次
Window関数の基本構文│GROUP BYとの決定的な違いとは?
Window関数を理解する最初のステップは、その独特な構文 OVER()
句と、長年の相棒である GROUP BY
句との決定的な違いを掴むことです。
【思考プロセス】なぜ GROUP BY ではダメなのか?
例えば、ここに次のような社員の給与データがあったとします。
テーブル:employees
id | name | department | salary |
---|---|---|---|
1 | 佐藤 | 営業部 | 500 |
2 | 鈴木 | 営業部 | 600 |
3 | 高橋 | 開発部 | 650 |
4 | 田中 | 開発部 | 700 |
5 | 渡辺 | 開発部 | 750 |
このテーブルから「各社員の給与と、その社員が所属する部署の平均給与を並べて表示したい」と考えたとしましょう。
GROUP BY
を使って部署ごとの平均給与を計算してみます。
SELECT
department,
AVG(salary) AS avg_salary
FROM
employees
GROUP BY
department;
結果
department | avg_salary |
---|---|
営業部 | 550 |
開発部 | 700 |
これでは部署ごとの平均給は分かりますが、「佐藤さん」や「高橋さん」といった個々の社員の情報が消えてしまいます。GROUP BY
は行を集約してしまうため、元の行と集計結果を共存させることができないのです。
Window関数なら「元の行を保ったまま」集計できる
この課題を解決するのがWindow関数です。Window関数は、GROUP BY
のように元の行を潰すことなく、集計結果を各行に追加するイメージです。
【図解:GROUP BY
と Window関数のイメージ違い】
(※ここはMermaid.jsなどのライブラリで描画される想定です)
graph TD
subgraph 元データ
A[社員A
営業部
500]
B[社員B
営業部
600]
C[社員C
開発部
650]
end
subgraph GROUP BY の場合
D{GROUP BY
department} --> E[営業部: 550
開発部: 650]
subgraph Note
F[元の行は消える]
end
end
subgraph Window関数の場合
G{Window関数
AVG(...) OVER (...)} --> H[社員A, 営業部, 500, 550]
G --> I[社員B, 営業部, 600, 550]
G --> J[社員C, 開発部, 650, 650]
subgraph Note
K[元の行に集計値が付与される]
end
end
A & B & C --> D
A & B & C --> G
基本構文と PARTITION BY
Window関数は OVER()
句とセットで使います。OVER()
の中で集計範囲を指定します。
関数名() OVER (PARTITION BY 集計の区切りとなる列名)
この PARTITION BY
が非常に重要です。これは、集計する際の「仕切り」や「グループ」を指定するもので、GROUP BY
に似ていますが、行を集約しない点が異なります。身近な例で言えば、「クラス分け」のようなものです。学校全体の生徒名簿を見ながら、各生徒の隣に「自分のクラスの平均点」を書き込むような処理を実現します。
先ほどの課題をWindow関数で解決するSQLを見てみましょう。
SELECT
id,
name,
department,
salary,
AVG(salary) OVER (PARTITION BY department) AS dept_avg_salary
FROM
employees;
結果
id | name | department | salary | dept_avg_salary |
---|---|---|---|---|
1 | 佐藤 | 営業部 | 500 | 550 |
2 | 鈴木 | 営業部 | 600 | 550 |
3 | 高橋 | 開発部 | 650 | 700 |
4 | 田中 | 開発部 | 700 | 700 |
5 | 渡辺 | 開発部 | 750 | 700 |
このように、元の行構造はそのままに、PARTITION BY
で区切られた「営業部」「開発部」それぞれの平均給与が、各行に正しく付与されているのが分かります。これがWindow関数の基本であり、最大の強みです。
順位付け関数の使い分け│RANK, DENSE_RANK, ROW_NUMBER
Window関数が最も活躍する場面の一つが、この「順位付け」です。データベーススペシャリストの午後試験でも、「売上上位の顧客を抽出する」「成績順に並べる」といった要件で頻繁に登場します。
GROUP BY
では難しかった「グループ内でのランキング作成」を簡単に行えますが、一つ注意点があります。それは、もし同点だった場合、順位をどう扱うかです。
SQLには、この「同順位の扱い方」が異なる3つの主要な順位付け関数が用意されています。
【仕事での具体例】営業成績ランキングを作成する
ここに、営業部員の売上成績データがあるとします。同点のメンバーがいる点に注目してください。
テーブル:sales_records
id | name | sales |
---|---|---|
1 | 遠藤 | 950 |
2 | 加藤 | 800 |
3 | 木村 | 800 |
4 | 斎藤 | 720 |
5 | 中村 | 950 |
このデータに対し、3つの関数を同時に使って売上(sales
)の高い順にランキングを付けてみましょう。順位付けの際は OVER()
句の中で ORDER BY
を使うのが基本です。
SELECT
name,
sales,
RANK() OVER (ORDER BY sales DESC) AS rank_result,
DENSE_RANK() OVER (ORDER BY sales DESC) AS dense_rank_result,
ROW_NUMBER() OVER (ORDER BY sales DESC) AS row_number_result
FROM
sales_records;
結果
name | sales | rank_result | dense_rank_result | row_number_result |
---|---|---|---|---|
遠藤 | 950 | 1 | 1 | 1 |
中村 | 950 | 1 | 1 | 2 |
加藤 | 800 | 3 | 2 | 3 |
木村 | 800 | 3 | 2 | 4 |
斎藤 | 720 | 5 | 3 | 5 |
結果を見比べると、同点(950点と800点)のときの順位の付け方が全く違うことが分かります。
3つの関数の特徴と使い分け
それぞれの関数の挙動は、以下の表のようにまとめられます。
関数 | 同順位の扱い | 次の順位の扱い | 身近な例え |
---|---|---|---|
RANK() |
同じ順位を割り当てる | 順位を飛ばす(1位が2人なら次は3位) | 一般的な競争の順位(金メダルが2人なら銀はなし) |
DENSE_RANK() |
同じ順位を割り当てる | 順位を飛ばさない(1位が2人でも次は2位) | 欠番のないランキング |
ROW_NUMBER() |
同順位を考慮しない | 常に1ずつ増加する連番 | 受付番号、整理券 |
【午後試験での判断基準】
どの関数を使うべきかは、問題文の要件次第です。
- 「上位3名を抽出せよ(同点者は全員含める)」のような場合は、
RANK()
かDENSE_RANK()
を使います。(WHERE rank_result <= 3
のように後続処理で絞り込む) - 「順位の欠番を許容しない」といった指定があれば
DENSE_RANK()
を選びます。 - 単純に「先頭からN件」や重複を許さないユニークな連番が必要な場合は
ROW_NUMBER()
が最適です。
要件を正確に読み取り、最適な関数を選択する能力が問われます。
行間のデータ操作を実現するLAG / LEAD関数
RANK
が「縦方向」の比較だとしたら、LAG
(ラグ)とLEAD
(リード)は「前後方向」のデータを参照するための強力な関数です。これらを使いこなせると、時系列データの分析が一気に楽になります。
【身近な例え】SNSのタイムライン
LAG
とLEAD
のイメージは、SNSのタイムラインやブログの記事が分かりやすいでしょう。
LAG()
: 今見ている投稿の「前の投稿」のデータを取得する。LEAD()
: 今見ている投稿の「次の投稿」のデータを取得する。
このように、現在の行を基準に、指定した順序での「前」や「後」の行にあるデータに直接アクセスできるのが特徴です。
【仕事での具体例】前月の売上と比較する
月ごとの売上実績データがあるとします。ここから「各月の売上と、その前月の売上を並べて表示し、売上の増減を分析したい」というケースは頻繁に発生します。
テーブル:monthly_sales
month | sales_amount |
---|---|
2025-04 | 1200 |
2025-05 | 1500 |
2025-06 | 1400 |
2025-07 | 1800 |
2025-08 | 1750 |
この課題をLAG
関数で解決してみましょう。LAG
は「L...Late(遅い、過去)」と覚えると分かりやすいです。「何順で前後の行を見るか」を指定するため、OVER()
句の中のORDER BY
が必須です。
SELECT
month,
sales_amount,
LAG(sales_amount, 1, 0) OVER (ORDER BY month) AS previous_month_sales
FROM
monthly_sales;
LAG(sales_amount, 1, 0)
の部分は、以下を意味します。
sales_amount
: 取得したいデータを持つ列(前月の売上高)1
: 何行前のデータを取得するか(1行前 = 前月)0
: 参照する行が存在しない場合(例:最初の行)のデフォルト値
結果
month | sales_amount | previous_month_sales |
---|---|---|
2025-04 | 1200 | 0 |
2025-05 | 1500 | 1200 |
2025-06 | 1400 | 1500 |
2025-07 | 1800 | 1400 |
2025-08 | 1750 | 1800 |
見事に、各月のsales_amount
の隣に、前月の売上高previous_month_sales
が表示されました。最初の行である4月は前月が存在しないため、指定したデフォルト値の 0
が入っています。
これにより、sales_amount - previous_month_sales
を計算すれば、簡単に前月比の差額を求めることができます。LEAD
関数はこの逆で、指定した行数だけ「未来」のデータを取得します。
LAG
/LEAD
は、午後試験で移動平均や差分計算が問われた際に、思考をシンプルにし、SQLを簡潔に記述するための強力な武器となります。
高度な集計を実現するフレーム句│ROWS BETWEENを使いこなす
PARTITION BY
が「どのグループで集計するか」という全体の窓(Window)を定義するのに対し、フレーム句は、その窓の中でさらに「どの範囲の行を計算に含めるか」という、より動的な小窓(フレーム)を定義します。
これをマスターすれば、「移動平均」や「累積合計」といった高度な分析がSQLだけで完結できるようになります。
【図解】フレーム句のイメージ
フレーム句は、「カレント行(現在処理中の行)を基準に、上下何行目までを計算範囲とするか」を指定する機能です。まるで、長いデータの帯の上をスライドしていく小窓のようなイメージです。
この「小窓」の範囲を定義するのが ROWS BETWEEN ... AND ...
という構文です。
【仕事での具体例】「直近3日間の移動平均売上」を計算する
日々の売上データから、日々の変動をならして傾向を掴むために「移動平均」を算出する、という場面を考えます。
テーブル:daily_sales
sales_date | sales_amount |
---|---|
2025-09-01 | 50 |
2025-09-02 | 70 |
2025-09-03 | 60 |
2025-09-04 | 90 |
2025-09-05 | 85 |
このデータを使って、「当日を含む直近3日間の平均売上」を計算してみましょう。SQLでは ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
という構文を使います。これは「2行前から、カレント行(当日)まで」を計算範囲に指定するという意味です。
SELECT
sales_date,
sales_amount,
AVG(sales_amount) OVER (
ORDER BY sales_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS moving_avg_3days
FROM
daily_sales;
計算範囲の動き
このSQLがどのように動くか、カレント行が 2025-09-04
の場合を見てみましょう。
- カレント行: 2025-09-04 (売上: 90)
- 1行前: 2025-09-03 (売上: 60)
- 2行前: 2025-09-02 (売上: 70)
- 計算範囲 (フレーム): 上記の3行
- 計算内容:
(70 + 60 + 90) / 3 = 73.3
結果
sales_date | sales_amount | moving_avg_3days |
---|---|---|
2025-09-01 | 50 | 50.0 |
2025-09-02 | 70 | 60.0 |
2025-09-03 | 60 | 60.0 |
2025-09-04 | 90 | 73.3 |
2025-09-05 | 85 | 78.3 |
最初の2行は、指定した3日分のデータが存在しないため、計算できる範囲だけで平均が算出されています。このように、行が一つ進むごとに対象フレームがスライドしていくのが特徴です。
フレーム句の主な指定方法
指定方法 | 意味 | 主な用途 |
---|---|---|
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW |
2行前からカレント行まで | 移動平均 |
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW |
先頭行からカレント行まで | 累積合計 |
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING |
1行前から1行後まで | 前後を含めた平滑化 |
フレーム句は一見すると複雑ですが、「カレント行を基準とした計算範囲の指定」というコアな概念さえ掴めば、午後試験の複雑な集計問題にも柔軟に対応できる究極の武器となります。
まとめ:Window関数を武器に午後試験を突破しよう
今回は、データベーススペシャリスト試験の午後問題で最頻出テーマである「Window関数」について、基本から応用までを具体的なSQL例と共に解説しました。
最初は複雑に見えるかもしれませんが、一つ一つの関数の役割と動きを理解すれば、これほど頼りになる武器はありません。最後に、本記事で学んだ重要ポイントを振り返りましょう。
PARTITION BY
が基本のキGROUP BY
とは異なり、元の行を集約せずに「グループ分け」だけを行うWindow関数の心臓部です。- 順位付けは3兄弟を使い分ける
RANK()
,DENSE_RANK()
,ROW_NUMBER()
は、同順位の扱い方が異なります。問題文の要件を正確に読み取り、最適な関数を選びましょう。 - 時系列分析なら
LAG
/LEAD
現在の行を基準に「前後の行」のデータを直接参照できます。前月比の計算などで絶大な効果を発揮します。 - 移動平均・累積はフレーム句で
ROWS BETWEEN ...
を使いこなせば、移動平均や累積合計といった、より高度で実践的なデータ分析が可能になります。
Window関数の本質は、「元の行の構造を保ったまま、1つのクエリで柔軟な集計と分析を完結させる力」にあります。この強力な機能を使いこなせるかどうかは、午後試験の得点を大きく左右します。
知識として理解するだけでなく、ぜひお手元の環境で様々なSQLを試し、その動きを体感してみてください。手を動かして試行錯誤することが、合格への一番の近道です。
次回は、本記事と並んで重要なテーマである『第2回:JOIN・サブクエリ・集合演算マスター編』をお届けします。複数のテーブルを自在に組み合わせるテクニックを深掘りしますので、ぜひご期待ください。