BigQueryのSQLをDatabricksへ:方言の違い14箇所をsqlglotで変換

BigQuery の dbt モデルを Databricks 向けに手で書き直した14箇所が、無料の変換ツール sqlglot で自動変換できるかを検証しました。版を固定した再現手順、手作業の結果との比較、自動では直らなかった箇所まで実測で紹介します。

12分で読めます Data
BigQuery の SQL が書かれたカードから sqlglot の丸い箱を通り、Databricks の書き方に変わったカードへ矢印が伸びる変換の概念図
この記事は AI が下書きを補助し、運営者が内容を確認して公開しています。

BigQuery で動かしている dbt(SQL でデータの加工処理を組み立てるツール)のモデルを、Databricks でも動かせるか試したい方向けの記事です。筆者が実際に移したときに手で書き直した14箇所について、無料の変換ツール sqlglot が同じ結果を出せるかを検証しました。読み終わると、移行の手作業のうちどこが機械に任せられて、どこが残るのかを見積もれるようになります。

この記事で分かること

  • 1つのプロジェクトで、SQL の方言(データベースごとに違う SQL の書き方)の違いが出た箇所は14箇所だったこと
  • その14箇所が sqlglot 30.30.0 で自動変換でき、出力が手作業の結果と一致したこと
  • 一方で、dbt のファイルをそのまま sqlglot に渡すと失敗すること、配線の変更は自動化できないこと

この記事の数字は、筆者の1プロジェクト(政府統計 e-Stat を取り込む dbt モデル14ファイル)を対象にした結果です。他のプロジェクトでそのまま同じ割合になるとは限りません。

何を試したか

筆者は政府統計の API から取り込んだデータを、BigQuery 上の dbt で bronze / silver / marts の3層に加工していました。これを Databricks でも動かすため、SQL を Databricks 向けに手で書き直しました。

このとき書き直した箇所を、sqlglot に自動でやらせたらどうなるかを確かめます。sqlglot は Python 製のライブラリで、SQL を構文木(SQL の構造を木の形で表した中間表現)に読み込み、別の方言の書き方で出力し直します。

BigQuery 版の dbt モデル 14 ファイル
         |
         | (1) 方言依存の書き方を数える
         v
     14 箇所 / 13 行 / 8 ファイル
         |
         | (2) sqlglot で bigquery -> databricks に変換
         v
   sqlglot の出力
         |
         +--- (3) 手で書き直した版と構文木で比較
         |
         +--- (4) Databricks で実行して確認

BigQuery 版の dbt モデルから、Jinja の退避、sqlglot での変換、Jinja の復元、Databricks での実行確認までの 5 段階を上から下へ並べ、手作業が残る 2 段階を破線で示した流れ図。1 段目に 14 ファイル中 8 ファイルで方言の違いがあったことを書いている

方言の違いは14箇所、行で数えると13箇所

まず「14箇所」の数え方をはっきりさせます。BigQuery 版の14ファイルを対象に、Databricks に無い書き方を正規表現で検索しました。

grep -n -i -E "as int64|as float64|safe_cast|safe_divide|regexp_contains" \
  models/*/*.sql tests/*.sql

結果は次のとおりです。

探した言葉出現数備考
as int64(型名)43ファイルに分かれて出てきます
as float64(型名)1下の safe_cast と同じ1行に出てきます
safe_cast(関数名)1上の as float64 と同じ1行に出てきます
safe_divide(関数名)75ファイルに分かれて出てきます
regexp_contains(関数名)1
合計14

一方、grep が報告した行数は13行でした。差が出る理由は、as float64 と safe_cast が1行に同時に出ているためです。

-- 1 行に float64 と safe_cast の 2 箇所が入っている
else safe_cast(replace(value, ',', '') as float64)

方言の違い 14 箇所の内訳を横棒で示した図。safe_divide が 7 箇所、as int64 が 4 箇所、safe_cast と as float64 が同じ 1 行で 2 箇所、regexp_contains が 1 箇所

つまり出現数で14箇所、行で数えると13行、ファイルで数えると8ファイルです。以降は出現数の14箇所を基準に話を進めます。

sqlglot を版を固定して動かす

結果を再現できるように、版を固定して実行しました。筆者は uv(Python の環境とパッケージを扱うツール)の一時環境を使い、sqlglot==30.30.0 を指定しました。

uv run --python 3.12 --with "sqlglot==30.30.0" python -I transpile.py

変換そのものは1行です。read に元の方言、write に移したい方言を指定します。

import sqlglot

out = sqlglot.transpile(sql, read="bigquery", write="databricks", pretty=True)[0]

ここで1つ注意点があります。dbt のファイルは {{ source('bronze', 'bronze_estat_meta_info') }} のような Jinja(dbt が使うテンプレートの書き方)を含んでおり、sqlglot はこれを SQL として読めません。そのまま渡すと次のエラーで止まります。

ParseError: Expected table name but got <Token token_type: TokenType.L_BRACE, text: {, line: 8, col: 10>

そのため、変換の前に Jinja の部分を仮の識別子(jinja_ph_0 など)へ置き換え、変換の後に元へ戻す処理を自分で書く必要がありました。

import re

JINJA = re.compile(r"\{\{.*?\}\}|\{%.*?%\}")

holes = []
def repl(m):
    holes.append(m.group(0))
    return "jinja_ph_%d" % (len(holes) - 1)

masked = JINJA.sub(repl, src)                     # Jinja を仮の名前に置き換える
out = sqlglot.transpile(masked, read="bigquery", write="databricks", pretty=True)[0]
for i, h in enumerate(holes):
    out = out.replace("jinja_ph_%d" % i, h)       # 変換後に元へ戻す

この置き換えは手作業です。「ファイルを渡せば終わり」ではない点は、見積もりのときに差し引いて考える必要があります。

結果:14箇所すべてが自動で変換された

変換後のファイルから、BigQuery 固有の書き方が残っているか、Databricks 向けの書き方に変わっているかを数え直しました。14箇所すべてが変換され、BigQuery 固有の書き方は1つも残りませんでした。

行の数ではなく出現数で数えているため、2行目は1行で2箇所分です(合計14箇所)。

元の書き方(BigQuery)sqlglot の出力(Databricks)箇所数
as int64AS BIGINT4
safe_cast と as float64TRY_CAST と AS DOUBLE2
safe_divideTRY_DIVIDE7
regexp_containsREGEXP_LIKE1

2行目を2箇所と数えているのは、safe_cast と as float64 が同じ1行にあるためです。表に載せた書き方を、実際の式の形で並べると次のようになります。

-- 元の書き方(BigQuery)            -- sqlglot の出力(Databricks)
cast(left(time_code, 4) as int64)    CAST(LEFT(time_code, 4) AS BIGINT)
safe_cast(replace(value, ',', '')    TRY_CAST(REPLACE(value, ',', '')
  as float64)                          AS DOUBLE)
safe_divide(a, b)                    TRY_DIVIDE(a, b)
regexp_contains(area_code,           REGEXP_LIKE(area_code,
  r'^[0-9]{2}000$')                    '^[0-9]{2}000$')

変換前の BigQuery の書き方 4 つを上段、変換後の Databricks の書き方 4 つを下段に並べ、間の sqlglot の矢印でつないだ対比図

変換の中身は、手で直したときの判断と同じでした。たとえば safe_divide は、0 で割っても止まらずに null を返す BigQuery の関数です。Databricks には同じ名前の関数が無く(try_divide の公式ドキュメント)、/ で割ると 0 除算のエラー(DIVIDE_BY_ZERO)になります。sqlglot はこれを単純な / ではなく TRY_DIVIDE に置き換えたので、0 除算でエラーにならない振る舞いが保たれました。

正規表現の扱いも確かめました。BigQuery の r'...'(バックスラッシュをそのまま扱う文字列)は、出力では r が外れます。ただしバックスラッシュを含む正規表現では、意味が変わらないように自動でエスケープされました。

-- 入力(BigQuery)
select regexp_contains(x, r'^\d{2}000$') from t
-- 出力(Databricks)
SELECT REGEXP_LIKE(x, '^\\d{2}000$') FROM t

手で書き直した版と一致するかを確かめた

「変換できた」だけでは、手作業の結果と同じかは分かりません。そこで sqlglot の出力と、手で書き直して実際に動かした Databricks 版を、1ファイルずつ比べました。

文字列のまま比べると、改行やキーワードの大文字・小文字の違いで差が出ます。そこで両方を sqlglot で構文木に読み込み、同じ条件(Databricks 方言・コメント無視)で出力し直してから比較しました。Jinja の部分は、BigQuery 版と Databricks 版で参照先の名前が違うため、両方とも同じ仮の識別子に置き換えています。

a = sqlglot.parse_one(mask(bigquery_src), read="bigquery").sql(dialect="databricks", comments=False)
b = sqlglot.parse_one(mask(databricks_src), read="databricks").sql(dialect="databricks", comments=False)
assert a == b

結果は14ファイルすべてで一致しました(same=14 diff=0)。手で書き直した版は dbt のテストを含めて全件成功していたので、sqlglot の出力も同じ内容だったことになります。

Databricks で実際に実行して確かめた

比較だけでは「文字列として同じ」ことしか言えないため、sqlglot の出力そのものを Databricks の SQL ウェアハウスで実行しました。表を作り直すと既存のデータを壊すため、読み取りだけで済む dbt show --inline(SQL をその場で実行して先頭の数行を表示するコマンド)を使いました。

dbt show --inline "$(cat converted_model.sql)" --limit 1

14箇所を含む8ファイルすべてで成功し、失敗は0件でした。つまりこの実験では「文字列として直った」だけでなく、変換後の SQL が Databricks で実行できるところまで確認できています。

自動では直らなかったもの

一方で、移行作業のうち sqlglot が触れない部分もありました。筆者が手で直した変更には、方言とは別に次の2種類がありました。

変更内容sqlglot で自動化できるか
データの置き場の配線参照先の名前を書き換えるできない。Jinja と YAML の設定なので SQL の方言ではない
dbt のキー名tests: を data_tests: に変えるできない。dbt 側の書き方の変更

データの置き場の配線で書き換えたのは、次の2か所です。

  • source('bronze', ...) を source('estat_raw', ...) に変える
  • database: "{{ env_var('GCP_PROJECT') }}" を database: workspace に変える

BigQuery の「プロジェクト.データセット」は、Databricks では「カタログ.スキーマ」にあたります。この対応づけはプロジェクトごとの判断なので、ツールが決められる話ではありません。

注意したい点: 方言と関係ない部分も変わる

sqlglot は構文木を通して出力し直すため、方言と関係ない部分も形が変わります。差分レビューのときに驚かないよう、確認できたものを挙げます。

  • ファイル全体が整形される: 改行位置、インデント、キーワードの大文字化がすべて変わります。元の整形は保てません。
  • コメントの書き方と位置が変わる: -- が /* */ になり、位置が動きます。筆者が数えた fact_estat_value.sql では、コメントの数は減りませんでした(3件 → 3件)。ほかのファイルでは数えていないため、どのファイルでも減らないとは言えません。
  • except distinct が EXCEPT になる: Databricks では except distinct もそのまま動くので、この書き換えは必須ではありません。
  • item_code is not null が NOT item_code IS NULL になる: 意味は同じですが、読みやすさは落ちます。

いずれも意味は変わりませんが、差分が小さく収まることは期待できません。レビューでは、整形による差分と方言による差分を分けて見る必要があります。筆者は、同じファイルを write="bigquery" でも出力して比べ、両者の差分だけを方言由来として確認しました。

この結果をそのまま当てはめない方がよい点

この検証には範囲の限りがあります。

  • 対象は1プロジェクト・14ファイルです。使っていた BigQuery 固有の機能は5種類に収まっており、配列や構造体を多用する SQL や、_TABLE_SUFFIX によるワイルドカード参照は含まれていません。ウィンドウ関数(行の並び順を使って計算する関数)も、使ったのは row_number と lag だけで、ほかのウィンドウ関数の方言の差は試していません。
  • 検証したのは read="bigquery", write="databricks" の組み合わせだけです。
  • 固定した版は sqlglot 30.30.0 です。sqlglot は更新が速いため、別の版では結果が変わる可能性があります。
  • 実行して確かめたのは、方言の書き直しがある8ファイルです。行数や集計値の一致は、手で書き直した版で確認した結果です。

また、この検証に使った Databricks の環境は Free Edition(個人・非営利向けの無償版)です。規約がベンチマーク結果の公開を禁じているため、実行時間・費用・性能の数値はこの記事に載せていません。載せているのは「正しく変換できたか」という事実だけです。

使ったデータは政府統計の総合窓口(e-Stat)の統計データです(出典: 政府統計の総合窓口(e-Stat))。

関連する記事

まとめ

  • 方言の違いは出現数で14箇所・13行・8ファイルでした。数え方を決めないと「14」と「13」が混ざるので、記事や報告では単位を書くべきです。
  • sqlglot 30.30.0 は14箇所すべてを自動変換し、出力は手で書き直した版と構文木レベルで一致し、Databricks で実行できました。
  • ただし dbt のファイルはそのまま渡せず(Jinja で ParseError になる)、データの置き場の配線や dbt のキー名は手作業が残ります。方言の書き換えは任せられても、移行全体が自動化できるわけではありません。

分からない用語や、もっと詳しく知りたい箇所があれば遠慮なくお知らせください。