😄

BigQuery超入門⑤

2025/02/23に公開

はじめに

こちらは BigQuery 超入門 ④ の続きです。

学ぶこと

  • 「ウィンドウ関数とは」について
  • 代表的なウィンドウ関数一覧とそれぞれの処理について

ウィンドウ関数とは

任意のフィールドでグループを作り、グループの元データを保持しながら集計する関数。
グループ化する範囲のことを「パーティション」という。
設定したパーティション内で集計する関数というイメージを持つと分かりやすい。
また「分析関数」とも呼ばれておりデータの分析には欠かせない関数である。

特徴としてはパーティションに含まれる複数のレコードが持つ値を単純に一つの値として集計せず、各レコードの値を保持しながら計算ができる。
これまで GROUP BY を使って集計していた関数を 1 レコードにまとめることなく適用できる。

通常の集計関数との違いを確認してみる。
例えばお店ごとに月の売り上げ金額をもつテーブルがある。

ID store amount month
1 A 店 100 1
2 A 店 200 2
3 B 店 150 1
4 B 店 250 2
5 C 店 300 1

GROUP BY を使った集計関数だとグループ化した時に一つのレコードにまとめられてしまうので合計の売り上げ金額しか分からないので月毎の売り上げとの比較ができない。

SELECT
  store, SUM(amount) AS total_sales
FROM
    project_id.dataset.fsales
GROUP BY
    store

結果テーブル

store total_sales
A 店 300
B 店 400
C 店 300

次にウィンドウ関数を確認してみる。
パーティションを store にすることで **store でグループ化して store 毎の売り上げも保持したまま合計の売上金額を集計できる。

SELECT
    id,
    store,
    amount,
    SUM(amount) OVER (PARTITION BY store) AS total_sales_per_store
FROM
    project_id.dataset.fsales
ID store amount total_sales_per_store
1 A 店 100 300
2 A 店 200 300
3 B 店 150 400
4 B 店 250 400
5 C 店 300 300

違いを比較すると次のようなことがわかる。

比較項目 通常の集計関数 (GROUP BY) ウィンドウ関数 (OVER(PARTITION BY))
データの粒度 集約されて 1 行になる 各行ごとに計算される
元のデータの保持 失われる そのまま保持される
具体的な用途 店舗ごとの売上集計 各取引のデータを残しつつ、店舗ごとの合計も出す

分析に応じて使い分ける

番号付け関数

ランキング順による商品の分析やゴールデン導線分析などで使える。

関数名 処理内容
ROW_NUMBER() 各行に一意の連番を振る(同じ値があっても別の番号)
RANK() 指定範囲内で順位を振る(同じ値は同じ順位、次の順位は飛ぶ)
DENSE_RANK() RANK() に似ているが、順位を詰めて振る(次の順位は飛ばない)
NTILE(n) 指定した n 個のグループに均等に分割し、各行にグループ番号を付与

ナビゲーション関数

CRM 分析や Web 解析などで使える分析関数。

関数名 処理内容
FIRST_VALUE(column) 指定範囲内の最初の値を取得
LAST_VALUE(column) 指定範囲内の最後の値を取得
NTH_VALUE(column, n) 指定範囲の n 番目 の値を取得
LAG(column, n) 指定カラムの n 行前 の値を取得(デフォルト n=1
LEAD(column, n) 指定カラムの n 行後 の値を取得(デフォルト n=1
PERCENTILE_CONT() 五数要約の指標を取得する

集計分析関数

元のレコードを保持したまま累計や平均などの値を取得できる。
ウィンドウ関数として使用する際は「集計関数」ではなく「集計分析関数」としての利用用途になる。

関数名 処理内容
SUM() 指定範囲の合計を計算する
AVG() 指定範囲の平均値を計算する
COUNT() 指定範囲の行数をカウントする
MAX() 指定範囲の最大値を取得する
MIN() 指定範囲の最小値を取得する

ウィンドウ関数の基本構文

PARTITION BY はパーティション(グループ化)として定義したいフィールド名を指定する。(省略した場合はテーブル全体がパーティションの範囲になる
WINDOW フレーム定義したパーティション内でもさらに「どのレコードを使うか」を「フレーム」として指定する。(省略可能な場合もある)
また OVER 句内の PARTITION BY,ORDER BY,WINDOWフレーム で指定した内容を「ウィンドウ」と呼ぶ。
ウィンドウはウィンドウ関数によって指定が必要なもの不要なものに分かれる。

ウィンドウ関数名 OVER (
PARTITION BY フィールド名
ORDER BY フィールド名
WINDOWフレーム
)

WINDOW フレームについて理解する

WINDOW フレームについて、次のような 3 ヶ月の売り上げ平均(A~F)を算出する場合を例に理解してみる。

ID store amount sale_date moving_avg
1 A 店 100 2023-01-01 A
2 A 店 200 2023-02-01 B
3 A 店 150 2023-03-01 C
4 B 店 350 2023-01-01 D
5 B 店 400 2023-02-01 E
6 B 店 450 2023-03-01 F

この時 A~F は次のように求めることができる。

  • A => 100
  • B => (100+200)/2 = 150
  • C => (100+200+150)/3 = 150
  • D => 350
  • E => (350+400)/2 = 375
  • F => (350+400+450)/3 = 400

この時 AVG 関数はパーティション内のレコードの値を全て使わずに一部の値のみを使うことになる。
このようにパーティション内の全てのレコードの値を使わない場合にどのレコードを利用するのかを指定する役割を持つのが WINDOW フレームである。ここで指定するレコードは「フレーム」と呼ばれる。

ROWS を使ったフレームの指定

このフレームの上限・下限を指定する構文の一つが ROWS BETWEEN 句になる。

ROWS BETWEEN
パーティション内で利用するレコード(フレーム)の上限
AND
パーティション内で利用するレコード(フレーム)の下限

また上限と下限を指定するに次のようなキーワードを指定する必要がある。

  • UNBOUNDED PRECENDING(パーティションの上限)
  • X PRECENDING(現在の行から X 行だけ上)
  • CURRENT ROW(現在の行)
  • X FOLLOWING(現在の行から X 行だけ下)
  • UNBOUNDED FOLLOWING(パーティションの下限)

例えばパーティション内で使用するレコードが 直前の一行 ~ 現在の行 であれば次のような指定になる。

ROWS BETWEEN 1 PRECENDING AND CURRENT ROW

最初に見た 3 ヶ月の売り上げ平均(A~F)を算出する場合だと一番レコードを使用するのは 3 ヶ月目のレコードで、平均を求めるためには前 2 行のレコードと現在の行のレコードの値を使う。そのため次のような指定になる。

// パーティション内で利用するフレームの上限が前2行
// パーティション内で利用するフレームの下限が現在の行
ROWS BETWEEN 1 PRECENDING AND CURRENT ROW

最終的には次のような SQL で求めることができる。

WITH sales AS (
    SELECT 1 AS id, 'A店' AS store, 100 AS amount, DATE '2023-01-01' AS sale_date UNION ALL
    SELECT 2, 'A店', 200, DATE '2023-02-01' UNION ALL
    SELECT 3, 'A店', 150, DATE '2023-03-01' UNION ALL
    SELECT 4, 'B店', 350, DATE '2023-01-01' UNION ALL
    SELECT 5, 'B店', 400, DATE '2023-02-01' UNION ALL
    SELECT 6, 'B店', 450, DATE '2023-03-01'
)

SELECT
    id,
    store,
    amount,
    sale_date,
    AVG(amount) OVER (
        PARTITION BY store
        ORDER BY sale_date
        ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
    ) AS moving_avg
FROM
    project_id.dataset.sales
ORDER BY
    store, sale_date
ID store amount sale_date moving_avg
1 A 店 100 2023-01-01 100
2 A 店 200 2023-02-01 150
3 A 店 150 2023-03-01 150
4 B 店 350 2023-01-01 350
5 B 店 400 2023-02-01 375
6 B 店 450 2023-03-01 400

ちなみに WINDOW フレームを省略した場合のデフォルトは次のようになる。
(フレームの上限から現在の行までが範囲)

ROWS BETWEEN UNBOUNDED PRECENDING AND CURRENT ROW

RANGE を使ったフレームの指定

ROWS 以外にも RANGE を使ってフレームの指定ができる。
ROWS がフレーム内の「行」を基準に動作するのに対して RANGE はフレーム内の「値」を基準に動作する。

ROWS では次のような場合「一行前 ~ 現在の行」という範囲を表すが、

ROWS BETWEEN 1 PRECENDING AND CURRENT ROW

RANGE の場合は次のような場合「現在の値から -2 した値 ~ 現在の値」という値の範囲の指定になる。
例えば現在の行の値が「5」だとすると「3」「4」「5」の値をもつレコードが対象になる。
OVER 句の ORDER BY は数値である必要がある(ROWS は単純な行の数なのでこの制約はない)。

RANGE BETWEEN 2 PRECENDING AND CURRENT ROW

実際に違いを確認してみる。

WITH sales AS (
  SELECT 1 AS id, 100 AS amount,
  UNION ALL SELECT 2 AS id, 200 AS amount,
  UNION ALL SELECT 3 AS id, 200 AS amount,
  UNION ALL SELECT 4 AS id, 300 AS amount,
  UNION ALL SELECT 5 AS id, 400 AS amount,
)

SELECT
    id,
    amount,
    -- rows_sum は「一つ前の行」~「現在の行」が範囲なのでそれを足し算しているだけ
    SUM(amount) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS rows_sum,
    -- range_sum は「現在の値から100引いた値」~「現在の行の値」が範囲なので該当する値を全て足し算する
    SUM(amount) OVER (ORDER BY amount RANGE BETWEEN 100 PRECEDING AND CURRENT ROW) AS range_sum
FROM
    project_id.dataset.sales

rows_sum は単純に行の範囲なので理解しやすいと思う。
range_sum は例えば現在の値が「200」の場合は「100」~「200」の範囲に一致する全てのレコードの値が足し算されるようになる。

結果テーブルは次のようになる。

ID amount rows_sum range_sum
1 100 100 100
2 200 300 500
3 200 400 500
4 300 500 700
5 400 700 700

ウィンドウ関数の結果に対する絞り込み

ウィンドウ関数の結果を絞り込む時に WHERE 句を使うことはできない。
解決方法の一つはサブクエリ。

SELECT
  *
FROM (
  SELECT
      id,
      store,
      amount,
      sale_date,
      AVG(amount) OVER (
          PARTITION BY store
          ORDER BY sale_date
          ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
      ) AS moving_avg
  FROM
    project_id.dataset.sales
  ORDER BY
    store, sale_date
)
WHERE moving_avg > 150

もう一つは QUALIFY 句を使った絞り込みになる。
QUALIFY はウィンドウ関数の後に実行されるためサブクエリを使わずに絞り込みができる。

SELECT
    id,
    store,
    amount,
    sale_date,
    AVG(amount) OVER (
        PARTITION BY store
        ORDER BY sale_date
        ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
    ) AS moving_avg
FROM
    project_id.dataset.fsales
QUALIFY
    moving_avg > 150
ORDER BY
    store, sale_date

ここからは基本的な構文を確認してきたのでそれぞれのウィンドウ関数がどのように時に使えるのかを確認していく。

番号付け関数

ランキングを取得する

RANK 関数を使うとパーティション内でランキング順に表示できる。

WITH sales AS (
    SELECT 1 AS product_id, 'A店' AS store, 100 AS amount UNION ALL
    SELECT 2, 'A店', 200 UNION ALL
    SELECT 3, 'A店', 150 UNION ALL
    SELECT 4, 'B店', 350 UNION ALL
    SELECT 5, 'B店', 400 UNION ALL
    SELECT 6, 'B店', 450
)

SELECT
    id,
    store,
    amount,
    RANK() OVER (
        PARTITION BY store
        ORDER BY amount DESC
    ) AS rank
FROM
    project_id.dataset.sales
ORDER BY
    store, rank

お店毎に商品の売り上げがランキング順で表示されるようになる。

product_id store amount rank
2 A 店 200 1
3 A 店 150 2
1 A 店 100 3
6 B 店 450 1
5 B 店 400 2
4 B 店 350 3

行番号を取得する

ROW_NUMBER 関数を使うと、パーティション内で順番に番号を降ってくれる。
RANK 関数とは違いランキングを付けるのではなくシンプルに順番通りに番号を振る
そのため同じ値があってもスキップせずに番号を連番で振ってくれる。

次のようなテーブルがあっても 1,2,3 と振ってくれることを確認してみる。

employee_id month amount
1 2024-01-01 100
1 2024-02-01 100
1 2024-03-01 300
2 2024-01-01 250
2 2024-02-01 250
2 2024-03-01 350
SELECT
    employee_id,
    month,
    amount,
    ROW_NUMBER() OVER (
        PARTITION BY employee_id
        ORDER BY month
    ) AS row_num
FROM
    project_id.dataset.sales
ORDER BY
    employee_id, month;

結果テーブル

employee_id month amount row_num
1 2024-01-01 100 1
1 2024-02-01 100 2
1 2024-03-01 300 3
2 2024-01-01 250 1
2 2024-02-01 250 2
2 2024-03-01 350 3

指定した n 個のグループに均等に分割し、各行にグループ番号を付与する

CRM 分析の一つに「デジル分析」というのがある。
デジル分析とは顧客を販売金額の大きい順に並べた上で同じ顧客数となるように 10 等分(語源はラテン語で「10 等分」という意味らしい)し、グループごとの販売金額合計や全体の占める割合を分析するもの。この分析することで優良顧客層を見つけ出したりでき、各グループの貢献度を知ることができる。
このデジル分析する時に最適なウィンドウ関数が NTILE 関数になる。

「2024 年毎のユーザーの合計購入金額」を表したテーブルで挙動を確認してみる。

user_id sum_rev
1 19300
2 18000
3 17700
4 17200
5 15160
6 14400
7 14400
8 13000
9 12600
10 12400

まずは 10 人の user_id が存在する想定で NTILE 関数を適用した場合。

SELECT
    user_id,
    sum_rev,
    NTILE(10) OVER (ORDER BY sum_rev DESC) AS rev_group
FROM
    master
ORDER BY
    sum_rev DESC

10 人単位でグループ化するためシンプルに 1~10 のグループ番号がそれぞれ振り分けられる。

user_id sum_rev rev_group
1 19300 1
2 18000 2
3 17700 3
4 17200 4
5 15160 5
6 14400 6
7 14400 7
8 13000 8
9 12600 9
10 12400 10

次に 20 人の場合で確認してみる。

user_id sum_rev
1 19300
2 18000
3 17700
4 17200
5 15160
6 14400
7 14400
8 13000
9 12600
10 12400
11 12200
12 12000
13 11400
14 10840
15 10600
16 10600
17 10320
18 10080
19 9800
20 9800

-- select * from master
SELECT
    user_id,
    sum_rev,
    NTILE(10) OVER (ORDER BY sum_rev DESC) AS rev_group
FROM
    master
ORDER BY
    sum_rev DESC

1~10 のグループ番号が振られるが「購入金額順」で番号が振り分けられている。
例えば user_id = 2 は先ほどグループ番号 2 だったが、今回はグループ番号 1 に割り振られている。

結果テーブル

user_id sum_rev rev_group
1 19300 1
2 18000 1
3 17700 2
4 17200 2
5 15160 3
6 14400 3
7 14400 4
8 13000 4
9 12600 5
10 12400 5
11 12200 6
12 12000 6
13 11400 7
14 10840 7
15 10600 8
16 10600 8
17 10320 9
18 10080 9
19 9800 10
20 9800 10

ナビゲーション関数

最初の値を取得する

FIRST_VALUE 関数を使うとパーティション内で並び替えた際に最も上のレコードの値を取得できる。
例えば「複数回購入している顧客の初回購入日」のような情報を取得できる。

WITH master AS (
    SELECT
        user_id,
        sum_rev,
        -- 顧客毎の初回購入日を取得
        FIRST_VALUE(purchase_date) OVER (
            PARTITION BY user_id
            ORDER BY purchase_date
        ) AS first_purchase_date,
    FROM
        project_id.dataset_id.customers
)

SELECT
    user_id,
    MAX(first_purchase_date) AS first_purchase_date
FROM
    master
GROUP BY
    user_id

次のように取得できる。

user_id first_purchase_date
1 2023-09-24
2 2022-09-05
3 2021-04-27
4 2021-03-03
5 2023-09-08

NULL の取り扱い

幾つかのウィンドウ関数では NULL を無視できる IGNORE NULLS オプションを適用できる。
FIRST_VALUE 関数でも適用でき、NULL が存在する場合は NULL を拾わないように存在する値から取得できる。
例えば customer テーブルで「都道府県別に初めてユーザーが登録した日付」を取得する想定で確認してみる(確認しやすいように佐賀県で)。

SELECT
    prefecture,
    FIRST_VALUE(register_date) OVER (
        PARTITION BY prefecture
        ORDER BY register_date
    ) AS first_register_date
FROM
    project_id.dataset.customers
WHERE
    prefecture = '佐賀'

結果テーブルを見ると佐賀県の customer は 5 人登録されているが null になっている。

prefecture register_date
佐賀 null
佐賀 null
佐賀 null
佐賀 null
佐賀 null

ただ実際に次のように日付が登録されている customer も存在している。
FIRST_VALUE 関数が NULL を拾ってしまったことが原因。

prefecture register_date
佐賀 2023-01-01
佐賀 null
佐賀 2023-09-07
佐賀 2022-11-04
佐賀 2021-05-14

そのため IGNORE NULLS オプションを使うことで存在する値の中から一番最初の日付を拾うようになる。

SELECT
    prefecture,
    FIRST_VALUE(register_date IGNORE NULLS) OVER (
        PARTITION BY prefecture
        ORDER BY register_date
    ) AS first_register_date
FROM
    project_id.dataset_id.customers
WHERE
    prefecture = '佐賀'

結果テーブルでは次のように NULL が取り除かれた状態の日付を取得できた。

prefecture register_date
佐賀 null
佐賀 2021-05-14
佐賀 2021-05-14
佐賀 2021-05-14
佐賀 2021-05-14

最後の値を取得する

LAST_VALUE 関数を使うとパーティション内に並び替えたうち最後のレコードの値を取得できる。
例えば顧客の最終購入日を取得したりできるようになる。

LAST_VALUE 関数で注意したい点は WINDOW フレームの指定方法について。
指定自体必須ではなくオプションだがデフォルトだと「パーティションの上端から現在の行」までが範囲になるためパーティション全体が範囲にならない
そのため意図しない顧客の最終購入日を取得してしまう可能性がある。次のように「パーティションの上端から下端まで」を指定しておくことが必須になる。

ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
WITH master AS (
    SELECT
        user_id,
        LAST_VALUE(date_time) OVER (
            PARTITION BY user_id
            ORDER BY date_time
            ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
        ) AS last_purchase_date_time
    FROM
        project_id.dataset_id.sales
)

SELECT
    user_id,
    MAX(last_purchase_date_time) AS last_purchase_date_time
FROM
    master
GROUP BY
    user_id

次のように顧客毎に最終購入日を取得できた。

user_id last_purchase_date_time
1 2021-07-24
2 2022-12-10
3 2023-06-23
4 2022-06-05
5 2022-02-04

任意の値を取得する

NTH_VALUE 関数を使うとパーティション内で並び替えた際に任意の順番の値を取得できる。
例えば初回と 2 回目の購入日を取得したその差分を知りたい時などに使える。

WITH master AS (
    SELECT
    user_id,
    -- 1回目の購入日を取得
    NTH_VALUE(order_date, 1) OVER(
      PARTITION BY user_id
      ORDER BY order_date
    ) AS first_purchase_date,
    -- 2回目の購入日を取得
    NTH_VALUE(order_date, 2) OVER(
      PARTITION BY user_id
      ORDER BY order_date
    ) AS secound_purchase_date,
  FROM
    project_id.dataset_id.sales
)

SELECT
    user_id,
    DATETIME_DIFF(
        MAX(secound_purchase_date),
        MAX(first_purchase_date),
        DAY
    ) AS diff
FROM
    master
GROUP BY
    user_id

次のように 1 回目と 2 回目の日付の差分を求めることができた。

user_id diff
1 43

また FIRST_VALUE や LAST_VALUE, NTH_VALUE にも言えるのはパーティション内のレコード全てに適用したい値が取得されるため(GROUP BY ではないので)
最終的に集計したい場合は GROUP BY を使う必要がある。

現在のレコードの「直後」のレコードの値を取得する

LEAD 関数を使うと現在のレコードの「直後」のレコードの値を取得できる。

例えば次のようなテーブルが存在する場合に「次の月の売り上げ」を取得して比較ができたりする。

employee_id month amount
1 2024-01-01 100
1 2024-02-01 200
1 2024-03-01 300
1 2024-04-01 400
1 2024-05-01 500
1 2024-06-01 600
SELECT
    employee_id,
    month,
    amount,
    LEAD(amount) OVER (
        PARTITION BY employee_id
        ORDER BY month
    ) AS next_month_amount
FROM
    project_id.dataset.fsales
ORDER BY
    employee_id, month;

結果テーブルは次のようになる。次の行が存在しない場合は null が入るのは挙動として正しい。

employee_id month amount next_month_amount
1 2024-01-01 100 300
1 2024-02-01 200 400
1 2024-03-01 300 500
1 2024-04-01 400 600
1 2024-05-01 500 null
1 2024-06-01 600 null

現在のレコードの「直前」のレコードの値を取得する

LAG 関数を使うことで実現できる。
こちらは前月比などで分析をしたいときに使えそう。
先ほどとは逆のパターンで試してみる。

SELECT
    employee_id,
    month,
    amount,
    LAG(amount) OVER (
        PARTITION BY employee_id
        ORDER BY month
    ) AS next_month_amount
FROM
    project_id.dataset.fsales
ORDER BY
    employee_id, month;

結果テーブルは次のようになる。前の行が存在しない場合は null が入るのは挙動として正しい。

employee_id month amount next_month_amount
1 2024-01-01 100 null
1 2024-02-01 200 100
1 2024-03-01 300 200
1 2024-04-01 400 300
1 2024-05-01 500 400
1 2024-06-01 600 500

LEAD と LAG 関数のどちらとも第二引数を指定でき、指定した場合は「任意の行数後 or 行数前の値」という指定をできる。

集計分析関数

平均の取得

集計分析関数として AVG 関数を使うと移動平均を取得できる。
移動平均とは「時系列データにおいて、ある一定区間ごとの平均値を区間をずらしながら求める手法」とのこと。
通常の平均とは違い対象のレコードを「すらしながら」平均を求めるイメージ。

例として「3 項移動平均」という「1 レコード前」「現在のレコード」「1 レコード後」の 3 レコードの平均を求める手法で移動平均のイメージを掴んでみる。

employee_id month amount
1 2024-01-01 100
1 2024-02-01 200
1 2024-03-01 300
1 2024-04-01 150
1 2024-05-01 250
1 2024-06-01 350
SELECT
    employee_id,
    month,
    amount,
    FLOOR(AVG(amount) OVER (
        PARTITION BY employee_id
        ORDER BY month
        ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
    )) AS moving_avg_sales
FROM
    project_id.dataset.sales
ORDER BY
    employee_id, month;

結果テーブルは次のようになる。
注意点としては「どの行に対しても同じルール」で計算されるということ(足りないデータは考慮されない)。
そのため最初の行と最後の行は 1 レコードデータがない状態で計算されてしまう(2 レコードの平均になってしまっている)。

employee_id month amount moving_avg_sales
1 2024-01-01 100 150.0
1 2024-02-01 200 200.0
1 2024-03-01 300 216.0
1 2024-04-01 150 233.0
1 2024-05-01 250 250.0
1 2024-06-01 350 300.0

これを回避したい場合は「計算対象のレコードが十分にある場合だけ表示する」というようなロジックを追加することになる。

SELECT
    employee_id,
    month,
    amount,
    CASE
      -- 3項移動平均を求めるために必要なレコード数を満たす場合のみ
      WHEN COUNT(amount) OVER (
        PARTITION BY employee_id
        ORDER BY month
        ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
      ) = 3
      THEN FLOOR(AVG(amount) OVER (
          PARTITION BY employee_id
          ORDER BY month
          ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
      ))
      END AS moving_avg_sales
FROM
    project_id.dataset.sales
ORDER BY
    employee_id, month;

結果は次のように計算対象のレコードが十分に満たない最初の行と最後の行は null になったことを確認できた。

employee_id month amount moving_avg_sales
1 2024-01-01 100 null
1 2024-02-01 200 200.0
1 2024-03-01 300 216.0
1 2024-04-01 150 233.0
1 2024-05-01 250 250.0
1 2024-06-01 350 null

最後に

ここまで参考書籍を読みつつ手を動かしながら進めてきましたが今回で最後になります。
取り組んでみた感想として次のような実感を得ることができました。

  • どんな関数が存在しているのかを知れたので引き出しが増えた
  • クエリを書くときに意味を理解しながら書けるようになった
  • 他の人が書いたクエリの意味を理解して読めるようになった

参考記事・書籍

BigQuery ではじめる SQL データ分析 GA4 & Search Console & Google フォーム対応

GitHubで編集を提案

Discussion