こんにちは。SCSKの山口です。
今回は、BigQueryでウィンドウ関数(分析関数)を使ってデータを集約してみたいと思います。
テーブル全体ではなく、特定の実行範囲(パーティション)での処理が実行できる便利な機能です。ぜひご覧ください。
ウィンドウ関数とは(イメージ)
ウィンドウ関数とは、一言で言うと「行のグループ単位で計算を行い、行ごとに一つの結果を返してくれる」ものです。
似た言葉で、「集約関数」がありますが、ウィンドウ関数の有無で違う挙動を示します。
おそらくこの説明では???な方が多いと思うので、集約関数を使用する際、ウィンドウ関数がある場合とない場合に分けて図で説明します。
以下、テスト結果のクラスごとの平均点数を算出する例で説明します。
集約関数のみ
集約関数(avg)のみを使用した場合は上図のようになります。
テーブル全体に対して、「クラス」カラムでGroup byし、「クラス」ごとに平均点を算出して結果を返します。
上図を見てわかる通り、「グループ(クラス)ごとの出力」となり、「氏名」の情報は欠落します。
ウィンドウ関数+集約関数
ウィンドウ関数と集約関数を組み合わせた場合は上図のようになります。
集約関数のみの場合と異なり、行ごとの出力となっています。
また、元テーブルの情報を欠落させることなく出力することも可能です。
実践:ウィンドウ関数を使った集計
ここから実際にウィンドウ関数を使用してデータを分析してみます。
利用データ
今回は、下記情報を持つテーブルデータを使用してウィンドウ関数を使ってみます。
- ユーザID
- ユーザ名
- アクション
- タイムスタンプ
やりたいこと
今回は上記テーブルから、各ユーザが実行した最終アクションを、元テーブル情報(カラム)を維持したまま抽出します。
集約関数を使用する場合と、使用しない場合の2パターンで抽出してみます。
せっかくなので「Geminiを使用してSQLを生成」してみます。
下記の鉛筆のようなマークをクリックし、実現したい内容を書くとSQLを生成してくれます。
①集約関数のみ
Geminiが生成したSQLは下記です。
-- 分析関数を使わずに、user_action_allテーブルから、各userIDの最新のデータを全カラムSELECT SELECT user_action_all.* FROM `evocative-entry-277709.yamaguchi_test.user_action_all` AS user_action_all INNER JOIN ( SELECT userID, MAX(timestamp) AS max_timestamp FROM `evocative-entry-277709.yamaguchi_test.user_action_all` GROUP BY 1 ) AS latest_user_action ON user_action_all.userID = latest_user_action.userID AND user_action_all.timestamp = latest_user_action.max_timestamp;
下記2データをINNER JOINすることで実現しています。
- テーブル全量
- userIDでGroup byし、MAX関数でtimestampを集約したデータ
上記SQLでも、抽出したいデータを得ることはできました。(ちょっとメンドクサイSQLですが。)
②ウィンドウ関数+集約関数
Geminiが生成したSQLは下記です。
-- 分析関数を使って、user_action_allテーブルから、各userIDの最新のデータを全カラムSELECT SELECT * FROM ( SELECT *, ROW_NUMBER() OVER(PARTITION BY userID ORDER BY timestamp DESC) AS rn FROM `evocative-entry-277709.yamaguchi_test.user_action_all` ) AS t WHERE t.rn = 1;
先ほどよりスッキリしたSQLですが、実はさらにスッキリさせることができます(詳細は後述)。
SELECT *, ROW_NUMBER() OVER(PARTITION BY userID ORDER BY timestamp DESC) AS rn FROM `evocative-entry-277709.yamaguchi_test.user_action_all` QUALIFY rn=1
実行するとこちらも抽出したいデータを得ることができました。
よく見ると、SQLに見慣れない一文があります。
ROW_NUMBER() OVER(PARTITION BY userID ORDER BY timestamp DESC) AS rn
この部分です。ここがウィンドウ関数の正体です。
ここからは章を分けて、ウィンドウ関数について詳細を見ていきます。
ウィンドウ関数とは(詳細)
先ほどはウィンドウ関数について、図を使ってイメージを把握していただきましたが、ここからもう少し詳しく見ていきます。
ウィンドウ関数の「ウィンドウ」は、結果セットの中の特定の一部分のことを指します。
下図のような、「窓越しに風景の一部を見る」絵を想像していただけるとわかりやすいかと思います。
またまたイメージの話になってしまいましたが、本題に移ります。
ウィンドウ関数を使用するには、「OVER句」が必要になります。このOVER句内の3要素によって、ウィンドウを細かくコントロールすることができます。
- パーティション:データを分割
- オーダー:パーティション内の結果に順序付け
- フレーム:現在の行に対して相対的に移動するスライディングウィンドウフレームを指定可能(※今回は省略)
今回、実践で使用したデータに当てはめてみます。
ROW_NUMBER() OVER(PARTITION BY userID ORDER BY timestamp DESC) AS rn
- PARTITION BY userID:userIDごとにデータを分割
- ORDER BY timestamp DESC:userIDごとのデータをtimestampで降順にソート
ここまでで、下記のデータが取得されます。
上記結果を見ると、一番右の「rn」列でタイムスタンプをもとにランク付けがされていることがわかります。
今回のケースでは、ウィンドウ関数で取得したそれぞれのユーザのデータから、ランク(rn)が「1」のものを取得することで各ユーザの最新のデータを取得することができます。
ウィンドウ関数の結果を絞り込みは、QUALIFYを使用して実現することができます。
QUALIFY rn=1
先ほど実践にて「実はさらにスッキリさせることができます(詳細は後述)」と、せっかくGeminiが生成したSQLに少しケチをつけてしまいましたが、ウィンドウ関数の結果を絞り込む際はQUALIFYを使用することをオススメします。
QUALIFYを使用することで、今回のケースのように「サブクエリが削減できる」等のメリットがあります。
WHEREで絞り込み | QUALIFYで絞り込み |
SELECT
*
FROM
(
SELECT
*,
ROW_NUMBER() OVER(PARTITION BY userID ORDER BY timestamp DESC) AS rn
FROM
`evocative-entry-277709.yamaguchi_test.user_action_all`
) AS t
WHERE t.rn = 1;
|
SELECT
*,
ROW_NUMBER() OVER(PARTITION BY userID ORDER BY timestamp DESC) AS rn
FROM
`evocative-entry-277709.yamaguchi_test.user_action_all`
QUALIFY
rn=1
|
まとめ
今回は、ウィンドウ関数を使ってデータを分析してみました。
ウィンドウ関数を使用することで少ない文量のSQLで、元テーブルのデータを欠落させることなくデータが取得できました。
調べていく中で「スライディングウィンドウ」が気になったので、機会があれば詳しく調べてみたいと思います。
最後に、
やっぱりGeminiって便利ですね、、、