BigQuery ハンズオン教科書|2026-08 版
第 11 章

スクリプト・UDF・ELT パイプライン

SQL を部品化して自動化する。スケジュールクエリから Dataform まで

この章で学ぶこと

前提: スクリプトと SQL UDF のハンズオンはサンドボックスで完走可。スケジュールクエリと Dataform のハンズオンは課金有効プロジェクトが必須(概算 1 円未満)。

スクリプトで手続き型ロジックを書く

これまでの章で書いてきた SQL は「1 つの SELECT / DDL / DML 文」が単位でした。GoogleSQL の手続き型言語(procedural language)を使うと、複数の文をセミコロンで連結したマルチステートメントクエリとして、1 つのジョブの中で順番に実行できます。変数を宣言して状態を持ち回したり、条件分岐やループでロジックを組んだりできるため、「テーブルが存在すれば洗い替え、なければ新規作成する」「一定件数ずつ処理してリトライする」といった、単純な SELECT では書けない自動化が可能になります。

DECLARE で変数を宣言し、SET で値を更新します。変数の型は明示するか、DEFAULT 句の式から推論されます。宣言はブロックの先頭にまとめて書く必要があります。

DECLARE target_word STRING DEFAULT 'methinks';
DECLARE corpus_count INT64;
DECLARE word_count INT64;

-- 処理: 数 MB 未満(shakespeare は小さい公開データセット)
SET (corpus_count, word_count) = (
  SELECT AS STRUCT COUNT(DISTINCT corpus), SUM(word_count)
  FROM `bigquery-public-data.samples.shakespeare`
  WHERE LOWER(word) = target_word
);

IF word_count > 100 THEN
  SELECT FORMAT('%s は %d 回登場(頻出語)', target_word, word_count) AS result;
ELSE
  SELECT FORMAT('%s は %d 回登場(希少語)', target_word, word_count) AS result;
END IF;

繰り返し処理には LOOP / WHILE / REPEAT / FOR...IN があります。FOR record IN (SELECT ...) DO ... END FOR はクエリ結果を 1 行ずつ変数に受けて処理する形で、他言語のカーソルに近い書き方です。ループを抜けるには BREAK(別名 LEAVE)、次の反復に進むには CONTINUE(別名 ITERATE)を使います。ブロックや条件分岐のネストは最大 50 段までという制約があります。

エラー処理は BEGIN...EXCEPTION WHEN ERROR THEN...END で行います。BEGIN 節でエラーが起きると、残りの文はスキップされ EXCEPTION 節に処理が移ります。EXCEPTION 節の中では @@error.message(人間可読なエラーメッセージ)・@@error.stack_trace(呼び出し元をたどる配列)・@@error.statement_text(エラーを起こした文のテキスト)という 3 つのシステム変数でエラーの詳細を取得できます。これらは EXCEPTION 節の外では参照できません。

BEGIN
  DECLARE attempt INT64 DEFAULT 0;
  DECLARE max_attempts INT64 DEFAULT 3;

  LOOP
    SET attempt = attempt + 1;
    IF attempt > max_attempts THEN
      LEAVE;
    END IF;
    -- ここに実際の処理(例: 外部呼び出しのリトライ対象)を書く
  END LOOP;

  SELECT FORMAT('%d 回試行しました', attempt) AS result;
EXCEPTION WHEN ERROR THEN
  SELECT
    @@error.message AS error_message,
    @@error.statement_text AS failed_statement;
END;
DECLARE / SET IF / LOOP 正常終了 後続処理へ EXCEPTION @@error.message @@error.stack_trace RAISE で再送出も可 順次実行 正常終了 エラー発生
図 11-1 BEGIN...END の中で例外が起きると、EXCEPTION 節に処理が移り、@@error.message などのシステム変数でエラー内容を取得できる。

動的に組み立てた SQL 文字列を実行したいときは EXECUTE IMMEDIATE を使います。? や @identifier プレースホルダで USING 句から値を渡し、INTO 句で結果を変数へ受け取れます。ただし EXECUTE IMMEDIATE の中に IF や BEGIN などの制御構文をネストすることはできません(動的実行できるのは単一の SELECT / DDL / DML / DCL 文のみ)。

実務の視点: スクリプトは「その場限りの自動化」に向いています。1 回のジョブで完結する DDL の条件分岐やリトライ付きバッチはスクリプトで十分です。一方、同じロジックを複数のクエリやスケジュールから呼び出したくなったら、次節以降のストアドプロシージャに昇格させると、呼び出し側のコードが 1 行の CALL で済み、変更点も 1 か所に集約できます。

UDF とリモート関数でロジックを部品化する

ユーザー定義関数(UDF)は、繰り返し使う計算やロジックを名前付きの関数として部品化する仕組みです。CREATE FUNCTION で作った永続 UDF はデータセットに保存され、プロジェクトをまたいで複数のクエリ・複数の利用者から呼び出せます。同じデータセットに同名の永続 UDF は 1 つしか置けません。一方 CREATE TEMP FUNCTION で作る一時 UDF はクエリの実行中だけ有効で、クエリが終わると同時に消えます。使い捨ての変換ロジックや実験段階の式は一時 UDF、チームで共有する定義は永続 UDF、と使い分けます。

CREATE OR REPLACE FUNCTION handson_ch11.price_with_tax(price NUMERIC, tax_rate NUMERIC)
RETURNS NUMERIC AS (
  ROUND(price * (1 + tax_rate), 0)
);

SELECT
  product_name,
  price,
  handson_ch11.price_with_tax(price, 0.10) AS price_with_tax
FROM UNNEST([
  STRUCT('ノート PC' AS product_name, 98000 AS price),
  STRUCT('マウス' AS product_name, 2400 AS price)
]);

SQL で表現できるロジックは SQL UDF として書くのが基本です。SQL では書きにくい文字列処理や正規表現を伴う変換には JavaScript UDF が使えますが、公式ドキュメントは「JavaScript UDF は標準的な SQL クエリより多くのスロットを消費し、ジョブのパフォーマンスを下げる傾向がある」としています。また JavaScript の数値表現の制約により、JavaScript UDF の引数に INT64 を直接渡すことはできません(FLOAT64 か STRING にキャストします)。外部の JavaScript ライブラリは OPTIONS (library=['gs://bucket/lib.js']) で Cloud Storage 上のファイルを読み込ませます。

CREATE OR REPLACE FUNCTION handson_ch11.slugify(input STRING)
RETURNS STRING
LANGUAGE js AS r"""
  return input.toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/(^-|-$)/g, '');
""";

SELECT handson_ch11.slugify('BigQuery スクリプト & UDF!') AS slug;

永続 UDF を安全に共有する仕組みが認可ルーチン(authorized routines)です。IAM で UDF 自体への実行権限を付与しつつ、UDF が参照する基盤テーブルへの直接アクセスは与えないという構成にできるため、「集計済みの結果だけを見せたい」という要件に使えます。

UDF が「BigQuery の中で完結するロジック」だとすれば、リモート関数は「BigQuery の外にあるロジックをクエリから呼び出す」仕組みです。SQL の CREATE FUNCTION ... REMOTE WITH CONNECTION で、Cloud Run functions(または Cloud Run)の HTTP エンドポイントを 1 つの関数として登録します。クエリ実行時、BigQuery は複数行の引数をバッチにまとめて JSON の POST リクエストを送り、エンドポイントは同じ順序で結果を返します。

CREATE FUNCTION handson_ch11.classify_sentiment(review_text STRING)
RETURNS STRING
REMOTE WITH CONNECTION `PROJECT_ID.us.remote-conn`
OPTIONS (
  endpoint = 'https://REGION-PROJECT_ID.cloudfunctions.net/classifySentiment'
);

接続には CLOUD_RESOURCE タイプの接続(connection)が必須です。接続作成時に自動生成されるサービスアカウントへ、呼び出し先の Cloud Run functions / Cloud Run を起動できる IAM ロール(invoker)を付与しておく必要があります。エンドポイント側が受け取るリクエストと返すレスポンスの形は固定の契約です。

// リクエスト(BigQuery → エンドポイント)
{
  "requestId": "124ab1c",
  "caller": "//bigquery.googleapis.com/projects/PROJECT_ID/jobs/JOB_ID",
  "calls": [["とても良い商品でした"], ["普通です"]]
}

// レスポンス(エンドポイント → BigQuery)
{"replies": ["positive", "neutral"]}

リモート関数の引数・戻り値は BOOL / BYTES / NUMERIC / STRING / DATE / DATETIME / TIME / TIMESTAMP / JSON のいずれか 1 つに限られ、ARRAY・STRUCT・INTERVAL・GEOGRAPHY はサポートされません。複合的な結果を返したい場合は JSON 型にシリアライズします。結果はキャッシュされず(非決定論的とみなされるため)、マテリアライズドビューの定義にも使えません。max_batching_rows オプションでバッチの最大行数を絞り、エンドポイント側のタイムアウトを避けられます。料金は通常の BigQuery 課金に加えて、呼び出し先の Cloud Run / Cloud Run functions の実行料金が発生します。

BigQuery クエリ実行 Cloud Run functions HTTP エンドポイント CLOUD_RESOURCE 接続 (認証を仲介) POST calls(バッチ) 200 { replies } 認証情報 呼び出し許可
図 11-2 BigQuery は CLOUD_RESOURCE 接続を介して認証し、Cloud Run functions へバッチで POST する。応答は JSON の replies 配列としてクエリ結果に統合される。
実務の視点: 選定の優先順位は「SQL UDF → JavaScript UDF → リモート関数」です。SQL で表現できるならスロット効率と保守性で SQL UDF が勝ります。正規表現や複雑な文字列整形が必要なら JavaScript UDF、既存の Python / Node.js の資産や外部 API・機械学習モデルをそのまま呼びたいならリモート関数、という順で検討すると過剰な複雑化を避けられます。

ストアドプロシージャ・テーブル関数と ELT の設計思想

ストアドプロシージャは CREATE PROCEDURE で定義し、BEGIN...END の中にスクリプトと同じ制御構文(IF・LOOP・EXCEPTION)を書けます。引数には修飾子なしの入力専用パラメータのほか、値を返す OUT、入出力を兼ねる INOUT が使えます。呼び出しは CALL 文です。

CREATE OR REPLACE PROCEDURE handson_ch11.refresh_mart(OUT rows_affected INT64)
BEGIN
  CREATE OR REPLACE TABLE handson_ch11.mart_word_by_corpus AS
  SELECT corpus, COUNT(DISTINCT word) AS distinct_words, SUM(word_count) AS total_words
  FROM handson_ch11.stg_word_counts
  GROUP BY corpus;

  SET rows_affected = (SELECT COUNT(*) FROM handson_ch11.mart_word_by_corpus);
END;

DECLARE affected INT64;
CALL handson_ch11.refresh_mart(affected);
SELECT affected AS mart_rows;

ストアドプロシージャと似て非なるものがテーブル関数(TVF)です。CREATE TABLE FUNCTION で定義し、ビューのようにテーブルを返しますが、ビューと違ってパラメータを取れます。本体は SELECT 文 1 本に限られ、DDL や DML、制御構文は書けません。副作用(テーブル作成・更新)が必要な処理はプロシージャ、パラメータ付きの読み取り専用ビューが欲しいときは TVF、と役割が分かれます。

CREATE OR REPLACE TABLE FUNCTION handson_ch11.words_by_corpus(target_corpus STRING)
AS (
  SELECT word, word_count
  FROM `bigquery-public-data.samples.shakespeare`
  WHERE corpus = target_corpus
  ORDER BY word_count DESC
  LIMIT 20
);

SELECT * FROM handson_ch11.words_by_corpus('hamlet');

ここまでの部品(スクリプト・UDF・プロシージャ)は、ELT パイプラインという 1 つの設計に組み上がっていきます。本書で扱う最小の設計原型は、raw → staging → mart という 3 層のデータセット構造です。raw は取り込んだ生データをそのまま保持し、後から再処理できる状態を残します。staging では型の整備や重複排除、名寄せといったクレンジングを行います。mart は集計済みでレポーティングや BI ツールから直接参照される層です。層を分ける理由は、障害が起きたときにどの層まで戻ればよいかを切り分けやすくすること、そして staging より上流を作り直す必要が出ても mart のスキーマを変えずに済ませられることにあります。第 9 章で扱った DML(特に MERGE による upsert)は staging → mart の更新でよく使われ、この最小構成の完成形は第 18 章の総合演習で組み上げます。

公開データセット / GCS raw 生データ保持 staging 型整備・重複排除 mart 集計・提供 ロード 整形 集計
図 11-3 raw は生データをそのまま保持し、staging で型整備・重複排除、mart で集計して BI ツールへ提供する。層を分けることで再処理と障害切り分けが容易になる。
実務の視点: staging → mart の変換をどう自動化するかで、スケジュールクエリと Dataform のどちらを選ぶかが決まります。テーブルが数枚で依存関係も単純なら、次節のスケジュールクエリで十分です。テーブルが増えて依存関係が複雑になったら、依存解決とデータテストを備えた Dataform に寄せると、実行順序を手で管理しなくて済みます。

ハンズオン

ハンズオン 11-1|スクリプトで DECLARE / IF / LOOP と BEGIN...EXCEPTION を書く

手続き型ロジックを実際に動かします。所要時間の目安は 10 分。サンドボックスで可・無料枠内(スキャン数 MB 未満)。

  1. BigQuery Studio のクエリエディタで、次のスクリプトを実行します。
DECLARE attempt INT64 DEFAULT 0;
DECLARE max_attempts INT64 DEFAULT 3;
DECLARE target_word STRING DEFAULT 'sword';
DECLARE word_count INT64;

BEGIN
  LOOP
    SET attempt = attempt + 1;

    SET word_count = (
      SELECT SUM(word_count)
      FROM `bigquery-public-data.samples.shakespeare`
      WHERE LOWER(word) = target_word
    );

    IF word_count IS NOT NULL OR attempt >= max_attempts THEN
      LEAVE;
    END IF;
  END LOOP;

  SELECT
    target_word,
    word_count,
    attempt AS attempts_used;
EXCEPTION WHEN ERROR THEN
  SELECT @@error.message AS error_message;
END;
  • 結果に word_count と、何回のループで確定したかを示す attempts_used が返ります。
  • target_word を存在しない単語(例: 'zzzznotaword')に変えて再実行し、word_count が NULL のまま max_attempts 回でループを抜けることを確認します。
  • 後片付け不要(テーブルを作成していません)。

    ハンズオン 11-2|SQL UDF を永続化して複数クエリから呼び出す

    永続 UDF を作り、データセットに保存したロジックを別のクエリから呼び出します。所要時間の目安は 10 分。サンドボックスで可・無料枠内(UDF の作成は DDL、呼び出しクエリのスキャンは数 KB)。

    1. 演習用データセットを作成します(初出のため。以降のハンズオンでも同じデータセットを使います)。
    CREATE SCHEMA IF NOT EXISTS handson_ch11
    OPTIONS (location = 'US');
  • 永続 SQL UDF を作成します。
  • CREATE OR REPLACE FUNCTION handson_ch11.price_with_tax(price NUMERIC, tax_rate NUMERIC)
    RETURNS NUMERIC AS (
      ROUND(price * (1 + tax_rate), 0)
    );
  • 別のクエリとして呼び出します。
  • SELECT
      product_name,
      price,
      handson_ch11.price_with_tax(price, 0.10) AS price_with_tax
    FROM UNNEST([
      STRUCT('ノート PC' AS product_name, 98000 AS price),
      STRUCT('マウス' AS product_name, 2400 AS price)
    ]);
  • 期待される出力: price_with_tax 列に 10% 加算後の丸めた金額(107800、2640)が表示されます。エクスプローラで handson_ch11 データセットを開き、price_with_tax がルーティンとして保存されていることを確認します。
  • 後片付け: UDF はハンズオン 11-3・11-4 でも使わないため、ここで消しても構いません(データセット自体は次のハンズオンで使うため残します)。

    DROP FUNCTION IF EXISTS handson_ch11.price_with_tax;
    ハンズオン 11-3|raw → staging → mart の最小 ELT をスケジュールクエリで自動化する

    3 層のテーブルを作り、staging → mart の再集計を毎日自動実行する設定を組みます。所要時間の目安は 15 分。課金プロジェクト必須(スケジュールクエリは BigQuery Data Transfer Service の機能であり、サンドボックスでは利用できません。概算 1 円未満。クエリ実行自体は無料枠のスキャン量で収まります)。

    1. raw 層: 公開データセットから対象を絞ってコピーします。
    -- 処理: 6 MB 未満(shakespeare は小さい公開データセット、corpus を 3 作品に絞る)
    CREATE OR REPLACE TABLE handson_ch11.raw_shakespeare AS
    SELECT word, word_count, corpus, corpus_date
    FROM `bigquery-public-data.samples.shakespeare`
    WHERE corpus IN ('hamlet', 'macbeth', 'othello');
  • staging 層: 型と表記を整えます。
  • CREATE OR REPLACE TABLE handson_ch11.stg_word_counts AS
    SELECT
      LOWER(word) AS word,
      corpus,
      word_count,
      corpus_date
    FROM handson_ch11.raw_shakespeare
    WHERE word_count > 0;
  • mart 層: コーパスごとに集計します。
  • CREATE OR REPLACE TABLE handson_ch11.mart_word_by_corpus AS
    SELECT
      corpus,
      COUNT(DISTINCT word) AS distinct_words,
      SUM(word_count) AS total_words
    FROM handson_ch11.stg_word_counts
    GROUP BY corpus;
  • mart への再集計クエリを、Cloud Shell から bq でスケジュールクエリとして登録します(コンソールの場合は File > Schedule query からも同じ設定ができます)。
  • bq query \
      --use_legacy_sql=false \
      --display_name="ch11_daily_mart_refresh" \
      --schedule="every 24 hours" \
      --destination_table=PROJECT_ID:handson_ch11.mart_word_by_corpus \
      --replace=true \
    'SELECT corpus, COUNT(DISTINCT word) AS distinct_words, SUM(word_count) AS total_words
     FROM `PROJECT_ID.handson_ch11.stg_word_counts`
     GROUP BY corpus'
  • 実行結果はスケジュール実行の 24 時間後まで待たずに、BigQuery Studio の「Scheduled queries」画面から「Run now」で即時実行して確認できます。サンドボックスで試す場合は、手順 4 のスケジュール登録の代わりに、手順 3 の CREATE OR REPLACE TABLE をそのままクエリエディタで手動実行する運用に置き換えます。
  • 後片付け:

    -- スケジュールクエリの削除は BigQuery Studio の Scheduled queries 画面から対象を選び Delete
    -- テーブル・データセットは章末でまとめて削除(次のハンズオンでも使うため、ここでは削除しない)
    ハンズオン 11-4|Dataform で依存関係を宣言し最小ワークフローを実行する

    ハンズオン 11-3 と同じ 3 層構造を、SQLX ファイルの ref() だけで依存関係を宣言する形に書き直します。所要時間の目安は 20 分。開発ワークスペースの作成自体は無料。ワークフロー実行は BigQuery のクエリジョブとして課金されます(概算 1 円未満)。課金プロジェクト必須(本書執筆時点でサンドボックスでの Dataform 利用可否はドキュメントに明記がないため、課金有効プロジェクトで実施してください)。

    1. BigQuery Studio の左メニューから Dataform を開き、リポジトリ(例: ch11-quickstart-repository)を作成し、開発ワークスペース(例: ch11-quickstart-workspace)を初期化します。「Initialize workspace」を実行すると workflow_settings.yaml を含む雛形が生成されます。
    2. workflow_settings.yaml の宛先を確認・編集します。
    defaultProject: PROJECT_ID
    defaultDataset: handson_ch11
    dataformCoreVersion: CORE_VERSION
  • definitions/raw_shakespeare.sqlx を作成します。
  • config { type: "table" }
    
    SELECT word, word_count, corpus, corpus_date
    FROM `bigquery-public-data.samples.shakespeare`
    WHERE corpus IN ('hamlet', 'macbeth', 'othello')
  • definitions/stg_word_counts.sqlx を作成します。ref() で raw への依存を宣言します。
  • config { type: "table" }
    
    SELECT
      LOWER(word) AS word,
      corpus,
      word_count
    FROM ${ref("raw_shakespeare")}
    WHERE word_count > 0
  • definitions/mart_word_by_corpus.sqlx を作成します。assertions でコーパスの重複がないことを検査します。
  • config {
      type: "table",
      assertions: { uniqueKey: ["corpus"] }
    }
    
    SELECT
      corpus,
      COUNT(DISTINCT word) AS distinct_words,
      SUM(word_count) AS total_words
    FROM ${ref("stg_word_counts")}
    GROUP BY corpus
  • ワークスペース内で「Start execution」→「All actions」を選ぶと、Dataform が ref() の依存関係から raw → staging → mart の実行順序を自動で解決し、続けてアサーションを検査します。
  • 期待される出力: 実行ログに 3 つのアクション(テーブル作成 2 件相当 + アサーション)が依存順に並び、すべて成功と表示されます。

    後片付け: Dataform の画面からリポジトリを削除し、あわせてデータセットも削除します。

    bq rm -r -f -d PROJECT_ID:handson_ch11
    raw_shakespeare type: table stg_word_counts ref(raw_shakespeare) mart_word_by_corpus ref(stg_word_counts) assertion uniqueKey: corpus ref() ref() 検査
    図 11-4 SQLX の ref() で参照先を書くだけで、Dataform が raw → staging → mart の実行順序と assertion(データテスト)の依存関係を自動的に解決する。

    つまずきポイント

    JavaScript UDF に INT64 を渡してエラーになる: JavaScript の数値表現の制約により、INT64 を引数として直接渡すことはできません。引数を FLOAT64 か STRING にするか、呼び出し側で CAST してから渡します。なお、この制限は入力側だけで、戻り値は INT64 のまま返せます。
    サンドボックスでスケジュールクエリが設定できない: スケジュールクエリは BigQuery Data Transfer Service のバックエンドを使う機能で、サンドボックスの制限一覧に明記されている DTS 非対応の対象に含まれます。課金を有効化するか、手動実行に切り替えてください。
    EXECUTE IMMEDIATE の中に IF や BEGIN をネストして構文エラーになる: EXECUTE IMMEDIATE が動的に実行できるのは単一の SELECT / DDL / DML / DCL 文だけです。制御構文(IF・CASE・BEGIN・LOOP など)は EXECUTE IMMEDIATE の外側、静的なスクリプトの中に書きます。
    リモート関数の呼び出しで Permission denied になる: CLOUD_RESOURCE 接続を作成しただけでは呼び出せません。接続作成時に自動生成されたサービスアカウントへ、呼び出し先の Cloud Run functions / Cloud Run を起動できる IAM ロールを別途付与する必要があります。

    理解度チェック

    Q1. 永続 UDF(CREATE FUNCTION)と一時 UDF(CREATE TEMP FUNCTION)の違いは?

    永続 UDF はデータセットに保存され、複数のクエリ・複数の利用者から繰り返し呼び出せます。一時 UDF は実行中のクエリのスコープ内でのみ有効で、クエリが終わると同時に消えます。使い捨ての変換は一時 UDF、チームで共有するロジックは永続 UDF にします。

    Q2. BEGIN...EXCEPTION...END でエラーの詳細を取得するにはどうすればよいですか?

    EXCEPTION 節の中で @@error.message(メッセージ)・@@error.stack_trace(呼び出し元をたどる配列)・@@error.statement_text(エラーを起こした文のテキスト)というシステム変数を参照します。これらは EXCEPTION 節の外では参照できません。

    Q3. リモート関数の引数・戻り値としてサポートされていないデータ型はどれですか?

    ARRAY・STRUCT・INTERVAL・GEOGRAPHY はサポートされません。対応するのは BOOL・BYTES・NUMERIC・STRING・DATE・DATETIME・TIME・TIMESTAMP・JSON のいずれか 1 つです。複合的な結果を返したい場合は JSON 型にシリアライズします。

    Q4.(シナリオ)サンドボックスのプロジェクトのまま、raw → staging → mart の変換を毎日自動実行したい。最も適切な対応は?
    A. スケジュールクエリを設定する B. 課金を有効化してスケジュールクエリまたは Dataform を使う C. サンドボックスのまま LOOP 文で繰り返し実行するスクリプトを書く D. リモート関数で外部から定期実行させる

    B。スケジュールクエリは BigQuery Data Transfer Service の機能であり、サンドボックスでは利用できません。定期実行の仕組み自体に課金有効プロジェクトが必要です。A はコンソール上で設定できても実行されません。C は 1 回のジョブ内でループするだけで「日次の定期実行」にはならず、ジョブが終われば止まります。D はリモート関数自体に定期実行のトリガー機構はありません。

    Q5.(シナリオ)複雑な条件分岐とリトライを伴う日次バッチを実装し、処理件数を呼び出し元に返したい。最も適切な実装は?
    A. テーブル関数(TVF) B. SQL UDF C. ストアドプロシージャ(OUT パラメータ) D. リモート関数

    C。TVF は本体が SELECT 文のみで DDL・DML や制御構文を持てません。SQL UDF は単一の式しか返せず、複文や OUT パラメータを持てません。リモート関数は外部エンドポイント呼び出し用で、BigQuery 内の複雑な分岐処理には向きません。ストアドプロシージャは BEGIN...END 内で IF・LOOP・EXCEPTION を使え、OUT パラメータで処理件数を呼び出し元に返せます。

    まとめと次章へ

    参考リンク: