BigQuery SQL UDF の挙動を色々確認する
BigQuery ではユーザ定義関数(UDF) を作ることができる。 これを使って、よく使われる式や関数呼び出しの組み合わせを名前付けして再利用できる。
UDF は SQL と JavaScript の二種類の言語による記述が可能で、後者は色々なトリッキーな利用例も世に出ているので知っている人も多いかも知れない。 例えば WebAssembly のコードを実行するとかである。
本記事では 地味な方 素直な SQL での記述による UDF について色々挙動を確認してみる。
戻り値の型を推論してくれる
UDF は例えばシンプルなものだと以下のように記述できる。
CREATE TEMP FUNCTION zero() RETURNS INT64 AS (0); SELECT zero()
ここで戻り値の型は省略することができる(勝手に解決してくれる)
CREATE TEMP FUNCTION zero() AS (0); SELECT zero()
STRUCT 型の戻り値も推論してくれる。が、もちろん各フィールド名の情報はないのでダミーの値が補完されてしまう。
CREATE TEMP FUNCTION user() AS ((1, "taro")); SELECT user()
[ { "f0_": { "_field_1": "1", "_field_2": "taro" } } ]
RETURNS
で型を明示するとそこに記述されたフィールド名を拾ってくれる。
CREATE TEMP FUNCTION user() RETURNS STRUCT<id INT64, name STRING> AS ((1, "taro")); SELECT user()
[ { "f0_": { "id": "1", "name": "taro" } } ]
UDF から UDF を呼ぶ際はすでに定義済みである必要がある
UDF から別の UDF を呼び出すこともできる。
CREATE TEMP FUNCTION callee() AS (42); CREATE TEMP FUNCTION caller() AS (callee()); SELECT caller()
この定義順序を逆にするとクエリの実行が通らなくなる。 Function not found: callee; failed to parse CREATE [TEMP] FUNCTION statement at [5:16]
というエラーが出てしまう。
CREATE TEMP FUNCTION caller() AS (callee()); -- <- Function not found :( CREATE TEMP FUNCTION callee() AS (42); SELECT caller()
UDF の再帰呼び出しをすることもできない。この例では Function not found: fib; failed to parse CREATE [TEMP] FUNCTION statement at [10:7]
と怒られてしまう。
CREATE TEMP FUNCTION fib(n INT64) AS ( IF ( n > 2, fib(n - 1) + fib(n - 2), 1 ) ); SELECT fib(2)
だったら一度ダミーの UDF を定義してみよう!ということで一時 UDF じゃなく永続 UDF を、一度ダミーのものを定義したあと REPLACE してみる。 このクエリはそれ自体は valid と言われ実行可能である。
-- dummy CREATE OR REPLACE FUNCTION udfs.fib(n INT64) AS (1); CREATE OR REPLACE FUNCTION udfs.fib(n INT64) AS ( IF ( n > 2, udfs.fib(n - 1) + udfs.fib(n - 2), 1 ) ); SELECT udfs.fib(2)
が、これは実行してみると Query error: Too many nested views/persistent user-defined functions or possible circular reference of views/persistent user-defined functions referenced in query. Only 16 levels of nested views/persistent user-defined functions are allowed. at [11:1]
と怒られてしまう。 UDF では 16 チェーンまでしか UDF 呼び出しをできない制限がありそれに引っかかっているように見えるエラーメッセージである。この例では udfs.fib(2)
を呼び出してもそもそも再帰呼び出しされない気がするが・・・。
頑張ればループ処理を書ける
UDF では前述の通り再帰呼び出しできず、ループするような制御構文もサポートされていない。
が、 ARRAY 型の値と ARRAY 型関連関数を使うとループ、リストの各要素に対する繰り返し処理的なことができる。
ミソとなるのがみんな大好き UNNEST()
でテーブルを手にいれることができることと、 ARRAY()
の引数はサブクエリを取れる辺りにある!
CREATE TEMP FUNCTION array_twice(n INT64) AS (ARRAY( SELECT AS STRUCT ( SELECT ARRAY_AGG(vv) FROM ( SELECT v * 2 -- <- like a map function FROM UNNEST(GENERATE_ARRAY(1, n)) AS v) AS vv))); SELECT array_twice(100)
テーブルを参照することもできる
上記より SELECT 文が打てることを確認できたので FROM 句に実テーブルを渡してみる。 このクエリも合法で実行することができる!
CREATE TEMP FUNCTION do() AS (ARRAY( SELECT AS STRUCT ( SELECT ARRAY_AGG(vv) FROM ( SELECT LENGTH(repo_name) -- <- like a map function FROM `bigquery-public-data.github_repos.licenses` -- <- refer to a table LIMIT 10) AS vv))); SELECT do()
ドキュメントにテーブル参照に関する記述があるので、この挙動は一応想定されたもののはず。(もしかしたら他のまっとうな参照方法があるのかも)
一意の UDF とテーブル参照を合わせたクエリあたりの最大数 - 1,000。完全な展開後に、UDF ごとに一意のテーブルと UDF を合わせて 1,000 個まで参照できます。