BigQueryで(擬似的に)集計関数や分析関数のUDFを作成する
ユーザー定義関数
BigQueryではユーザー定義関数(UDF)を作成することができます。
UDFは基本的には1つの行に対して1つの値を返すような関数しか作成できないので、複数行に対して1つの値を返すような集計関数や、ウィンドウフレーム内の他の行の情報を使いつつ1つの行に対して1つの値を返すようなウィンドウ関数(分析関数)を作ることはできません。
しかし、UDFの引数にarray_agg
関数の返り値を使うことで、擬似的に集計関数や分析関数のUDFを作成することができます。
集計関数を作る
BigQueryには平均値を計算するavg
関数はありますが、中央値を計算する関数が用意されていません。
以下の記事では、中央値を計算するUDFの紹介と実装が載っています。
これを参考にして以前作成したジニ係数を計算するUDFの実装を紹介します。
CREATE OR REPLACE FUNCTION `project.dataset.gini`(arr ANY TYPE) AS (
(
with
ranked as (
select
x
, row_number() over (order by x desc) as rank
from
unnest(arr) as x
)
select
1 - 2 * sum(x * (rank - 1/2)) / count(*) / sum(x)
from
ranked
)
);
ポイントは
- UDFの引数としてARRAY型のデータを受け取ること(型指定には
ANY TYPE
を使用する) -
(subquery)
という形式にすることで[スカラー サブクエリ](https://cloud.google.com/bigquery/docs/reference/standard-sql/subqueries?hl=ja#scalar_subquery_concepts)を使ったUDFにすること - サブクエリの中で
unnest(arr)
とすることでARRAY型のデータを展開して処理すること - 返り値が1行だけになるようにすること
です。通常の集計関数とは使い方も異なります。
select
-- 通常の集計関数
avg(x)
-- UDFの集計関数
, `dataset.gini`(array_agg(x))
from
unnest(generate_array(0,100)) as x
このように、一度array_agg()
で包んでからUDFに渡してあげることで機能します。
分析関数を作る
集計関数の作り方を応用して、分析関数のUDFを作成することもできます。
最近作成した、「同じ値が連続する間は同じIDで採番し、値が変わった時にIDをインクリメントする」という分析関数を紹介します。
CREATE OR REPLACE FUNCTION `project.dataset.context_id`(pref INT64, arr ANY TYPE) AS (
(
with
wndw as (
select
x
, p
, lag(x) over (order by p) as lagx
from
unnest(arr) as x
with offset as p -- position
)
, add_id as (
select
*
, countif(x != lagx) over (order by p) as id
from
wndw
)
select
id
from
add_id
where 1 = 1
and p = pref
)
);
集計関数の時と同様に、ARRAY型のデータを引数として受け取り、unnest(arr)
で展開して扱っています。
集計関数の時の違いは最後の部分です。途中までは分析関数だけを使っているので複数行あり、最後のクエリで1行に絞って返しています。(集計関数の場合は自然と1行になる。)ここで目的の行に絞るために、arr
の中の位置インデックスをpref
として引数に渡してもらうことを想定しています。
通常の分析関数との使い方の違いは、array_agg() over (...)
で包んでからUDFに渡すことです。OVER句をつけることでarray_agg
は分析集計関数になります。
select
-- 通常の分析関数
avg(x) over wndw
-- UDFの分析関数
-- row_numberは1-basedなので0-basedに直す
, `dataset.context_id`(
row_number() over wndw - 1,
array_agg(x) over wndw
)
from
unnest([1,2,2,3,3,3,4,5,2,3]) as x
with offset as p
window
wndw as (order by p)
最後に
もっと賢いやり方を知っている方がいらっしゃれば是非コメントで教えてください!
Discussion