はじめに
複雑なデータ分析や集計処理を行う際、ネストしたサブクエリが深くなり、SQLの可読性が著しく低下した経験はありませんか。また、「各グループ内での順位付け」「前後の行との比較」「累計値の算出」といった処理を、アプリケーション側で実装せざるを得なかったことはないでしょうか。
PostgreSQLのCTE(Common Table Expression、共通テーブル式)とウィンドウ関数を使いこなすことで、これらの課題をSQL単体でエレガントに解決できます。本記事では、CTEによるクエリの構造化、再帰CTEによる階層データの処理、そしてROW_NUMBER、RANK、LAG、LEAD、SUM OVERといったウィンドウ関数を活用した分析クエリの書き方を、実践的なサンプルコードとともに解説します。
この記事を読むことで、以下のことができるようになります。
- CTEを使って複雑なクエリを可読性の高い形で構造化できる
- 再帰CTEで階層構造やツリー構造のデータを効率的に処理できる
- ROW_NUMBER、RANK、DENSE_RANKで順位付けや重複排除ができる
- LAG、LEADで前後の行と比較した差分計算ができる
- SUM OVER、AVG OVERで累計や移動平均を算出できる
前提条件
- PostgreSQL 14以降がインストールされていること
- psqlまたはGUIツール(pgAdmin、DBeaver等)でデータベースに接続できること
- SELECT文の基本的な構文(WHERE句、GROUP BY、ORDER BY等)を理解していること
基本的なSELECT文やJOINについては、以下の関連記事を参照してください。
サンプルデータの準備
本記事で使用するサンプルテーブルとデータを準備します。売上分析や組織階層の処理を想定したデータ構成です。
|
|
CTE(共通テーブル式)の基本
CTEとは何か
CTE(Common Table Expression、共通テーブル式)は、WITH句を使用してクエリ内で一時的な名前付き結果セットを定義する機能です。サブクエリを名前付きのブロックとして分離できるため、複雑なクエリの可読性と保守性が大幅に向上します。
CTEの基本構文は以下の通りです。
|
|
サブクエリとの比較
同じ結果を得るクエリをサブクエリとCTEで比較してみましょう。「部門ごとの平均給与を超える社員を抽出する」という要件を考えます。
サブクエリを使った場合は以下のようになります。
|
|
同じ処理をCTEで書き直すと、以下のように構造化できます。
|
|
実行結果は以下の通りです。
|
|
CTEを使うことで、「部門ごとの平均給与を計算する」という処理がdept_avgという名前で明確に分離され、メインクエリの意図が理解しやすくなります。
複数のCTEを連結する
CTEは複数定義でき、後続のCTEから前のCTEを参照できます。これにより、段階的にデータを加工する処理を明確に表現できます。
以下は「月別の売上を集計し、さらにその中から売上が10万円を超える月だけを抽出し、上位3件を取得する」という処理です。
|
|
実行結果は以下の通りです。
|
|
再帰CTE - 階層データの処理
再帰CTEの基本構造
再帰CTE(Recursive CTE)は、自分自身を参照するCTEです。階層構造やツリー構造のデータを処理する際に威力を発揮します。WITH RECURSIVEキーワードを使用して定義します。
再帰CTEの基本構造は以下の通りです。
|
|
再帰CTEは以下の順序で実行されます。
- 非再帰項(アンカーメンバー)を実行し、初期結果を生成
- 再帰項を実行し、前のステップの結果を使って新しい行を生成
- 再帰項が行を返さなくなるまで2を繰り返す
- すべての結果を結合して返す
組織階層の取得
社員テーブルにはmanager_idで上司と部下の関係が定義されています。再帰CTEを使って、特定の社員から始まる組織ツリーを取得してみましょう。
|
|
実行結果は以下の通りです。
|
|
特定のマネージャー配下の全社員を取得
特定のマネージャー(例:鈴木花子)の配下にいる全社員を取得する例です。
|
|
実行結果は以下の通りです。
|
|
再帰CTEの注意点
再帰CTEを使用する際は、以下の点に注意が必要です。
- 終了条件の確保: 再帰項が最終的に空の結果を返すように設計しないと、無限ループになります
- UNION vs UNION ALL:
UNIONは重複を排除するためサイクル検出に役立ちますが、UNION ALLより遅くなります - メモリ消費: 大きな階層を処理する場合、中間結果がメモリを消費します
サイクルが存在する可能性があるデータでは、PostgreSQL 14以降で導入されたCYCLE句を使用できます。
|
|
ウィンドウ関数の基本
ウィンドウ関数とは
ウィンドウ関数は、現在の行に関連する一連の行(ウィンドウ)に対して計算を行う関数です。通常の集計関数(SUM、AVGなど)と異なり、行をグループ化して1行にまとめることなく、各行に対して結果を返します。
ウィンドウ関数の基本構文は以下の通りです。
|
|
以下は、各社員の給与と部門平均給与を同時に表示する例です。
|
|
実行結果は以下の通りです。
|
|
ウィンドウ関数と集計関数の違い
通常の集計関数とウィンドウ関数の違いを明確にするため、同じデータに対する処理を比較します。
|
|
|
|
|
|
ウィンドウ関数では元の行数が維持され、各行に集計結果が付加されます。
順位付け関数 - ROW_NUMBER / RANK / DENSE_RANK
ROW_NUMBER - 連番を振る
ROW_NUMBER()は、パーティション内で各行に一意の連番を振ります。同じ値があっても異なる番号が割り当てられます。
|
|
実行結果は以下の通りです。
|
|
RANK と DENSE_RANK - 同順位の扱い
RANK()とDENSE_RANK()は同じ値に対して同じ順位を付けますが、次の順位の扱いが異なります。
|
|
実行結果は以下の通りです。
|
|
同じ値がある場合の違いを明確にするため、給与データを追加して確認してみます。
|
|
実践例: 部門ごとの上位2名を抽出
ROW_NUMBER()を使って、各部門の給与上位2名を抽出する例です。
|
|
実行結果は以下の通りです。
|
|
実践例: 重複データの排除
ROW_NUMBER()を使って、特定の条件で重複するデータから最新の1件だけを抽出できます。
|
|
実行結果は以下の通りです。
|
|
行間比較関数 - LAG / LEAD
LAG - 前の行の値を参照
LAG()関数は、現在の行から指定した行数だけ前の行の値を取得します。前月比や前日比の計算に便利です。
|
|
以下は、月別売上と前月との差分を計算する例です。
|
|
実行結果は以下の通りです。
|
|
LEAD - 次の行の値を参照
LEAD()関数は、現在の行から指定した行数だけ後の行の値を取得します。
|
|
実行結果は以下の通りです。
|
|
実践例: 売上の増減トレンド分析
LAGを使って、売上の増減を視覚的に表示する例です。
|
|
実行結果は以下の通りです。
|
|
累計・移動計算 - SUM OVER / AVG OVER
累計(Running Total)の計算
SUM() OVER()を使用すると、累計値を簡単に計算できます。ORDER BYを指定すると、その順序で累計が計算されます。
|
|
実行結果は以下の通りです。
|
|
パーティション別の累計
PARTITION BYを組み合わせることで、グループごとの累計を計算できます。
|
|
実行結果は以下の通りです。
|
|
移動平均(Moving Average)の計算
ウィンドウフレームを指定することで、直近N件の移動平均を計算できます。
|
|
実行結果は以下の通りです。
|
|
ウィンドウフレームの指定
ウィンドウフレームは、ウィンドウ関数が計算に使用する行の範囲を定義します。
|
|
CTEとウィンドウ関数の組み合わせ
複雑な分析クエリの構築
CTEとウィンドウ関数を組み合わせることで、複雑な分析クエリを段階的に構築できます。
以下は「月別・カテゴリ別の売上を集計し、前月比と構成比を計算する」例です。
|
|
実行結果は以下の通りです。
|
|
売上ランキングと達成率の分析
|
|
実行結果は以下の通りです。
|
|
実践的な分析クエリ例
顧客のリピート分析
売上データから、顧客ごとの購入パターンを分析します。
|
|
実行結果は以下の通りです。
|
|
パーセンタイル分析
PERCENT_RANK()とNTILE()を使用した分布分析です。
|
|
実行結果は以下の通りです。
|
|
WINDOW句による再利用
同じウィンドウ定義を複数回使用する場合、WINDOW句で名前を付けて再利用できます。
|
|
実行結果は以下の通りです。
|
|
パフォーマンスと注意点
CTEのマテリアライゼーション
PostgreSQL 12以降では、CTEのマテリアライゼーション(一時テーブルへの保存)を制御できます。
|
|
CTEを1回しか参照しない場合はNOT MATERIALIZED(またはデフォルト動作)が効率的です。複数回参照する場合や副作用のある関数を含む場合はMATERIALIZEDが有効です。
ウィンドウ関数のインデックス活用
ウィンドウ関数のORDER BY句に合わせたインデックスを作成することで、ソートコストを削減できます。
|
|
実行計画の確認
複雑なクエリではEXPLAIN ANALYZEで実行計画を確認することが重要です。
|
|
詳細な実行計画の読み方については、PostgreSQL EXPLAIN ANALYZE入門 - 実行計画を読み解くを参照してください。
まとめ
本記事では、PostgreSQLのCTEとウィンドウ関数を使った高度なクエリの書き方を解説しました。
CTEを活用することで得られるメリットは以下の通りです。
- 複雑なクエリを論理的なブロックに分割し、可読性を向上できる
- 再帰CTEにより、階層構造やツリー構造のデータを効率的に処理できる
- 同じCTEを複数箇所で参照でき、クエリの重複を排除できる
ウィンドウ関数を活用することで得られるメリットは以下の通りです。
ROW_NUMBER、RANK、DENSE_RANKでグループ内の順位付けや重複排除ができるLAG、LEADで前後の行と比較し、差分や増減を計算できるSUM OVER、AVG OVERで累計や移動平均を効率的に算出できる
これらの機能を組み合わせることで、従来はアプリケーション側で実装していた複雑なデータ分析処理を、SQL単体でエレガントに記述できるようになります。