🪂

SQLGlotで軽やかに越える、SQL方言の壁。

に公開

どうも、stable株式会社でデータエンジニアをしているmyshmehです。

先日、以下の記事で、sqlparseを使って自前SQLトランスパイラを実装する事例を拝読しました。
https://www.m3tech.blog/entry/2025/08/19/110000

トランスパイルは、形式言語の具体・抽象を行き来する非常に難しい問題だと思います。複雑なbashスクリプトとの連携を成立させるなど、技術的背景により自前実装となったかと想像しますが、やり切られたことに脱帽です。

一方で、この激アツな取り組みを実行することは、普段の業務の傍らで全エンジニアがなせることではないかもしれません。もし、特定のドメイン・技術スタックによらない一般的なSQLトランスパイルで足りるならば、SQLGlotというOSSプロジェクトが存在します。

そこで、本記事では、SQLGlotを使って、任意のSQL方言を別のSQL方言に翻訳する方法をご紹介します。

課題 - 意外と高いSQL方言の壁

データ分析基盤の移行や利用するDBの多様化が進む中で、多くのエンジニアが直面するのがSQL方言の壁ではないでしょうか?

例えば、次のようなよくあるシナリオを考えてみましょう。

  • 状況: これまでアプリケーションのバックエンドDB(MySQL)に対し、分析クエリを直接実行していた。
  • 課題: クエリの実行時間やサーバー負荷が増大し、分析パフォーマンスが悪化してきた。
  • 対策: そこで、分析専用のデータウェアハウスとして新たにSnowflakeを導入することを決定した。

このとき、「MySQLで使っていた既存の分析クエリをSnowflakeにコピーすれば済む」とはいきません。なぜなら、データベースごとにSQLの構文や関数、挙動が微妙に異なるからです。

たとえば、MySQLで「30日後の日付」を求めるクエリは以下のようになります。

MySQL
SELECT DATE_ADD('2025-01-01', INTERVAL 30 DAY);

しかし、このクエリはSnowflakeではそのまま実行できず、エラーになります。Snowflakeでは、日付の計算にDATE_ADDは存在せず、代わりにDATEADD関数を呼び出す必要があるからです。

Snowflake
SELECT DATEADD(DAY, 30, '2025-01-01');

このように、単純な日付計算一つをとっても書き換えが必要です。これが複雑な分析クエリともなれば、修正箇所はさらに増えていくでしょう。

結果として、私たちは既存のクエリ資産を移行するために、次のような地道な作業を強いられることになります。

  1. 手作業やCoding Agentを駆使して、膨大なクエリを一つひとつ新しい方言に書き換える。
  2. 書き換えたクエリが、元のクエリと同じ結果を返すかを検証する。

このプロセスは非常に手間がかかり、ヒューマンエラーも起こりやすいため、エンジニアにとって大きな負担となります。
確かにClaude CodeなどのCoding Agentを駆使すれば一定程度品質の高い変換ができるかもしれません。しかし、執筆時点での私の経験では、クエリが長くなるにつれて、一部のカラムが欠損したりロジックを勝手に変えてしまうことがままありました。

解決策 - SQLGlot

そんな我々を助けてくれるのが、SQLGlotです。
https://github.com/tobymao/sqlglot

概要

SQLGlotは、PythonベースのSQLトランスパイラです。Oracle, BigQuery, Snowflakeを含む30ものSQL方言をサポートしています。コンパイラと同様のステップでSQLの構造を分析していることから、文法だけでなく意味的にも正しいSQLを行えます。


よって、単純に手元のSQLを他の方言に変換したい場合には良い選択肢だと思われます!

コラム: 以外な所で使われるSQLGlot

実は、SQLGlotはApache Superset, Dagsterのような人気プロジェクトで利用されていて、データツール業界で重要な役割を果たしています!

使い方

以下のtranspileメソッドを通じて、簡単に任意のSQL方言間の変換をしてくれます。

def transpile(
    sql: str,  # トランスパイル対象のSQL文字列
    read: DialectType = None,  # 元のSQL方言
    write: DialectType = None,  # 変換先のSQL方言
    identity: bool = True,  # Trueでread/write指定なければ、同じ方言に変換
    error_level: t.Optional[ErrorLevel] = None,  # パースエラー時の挙動
    **opts,  # 追加オプション
) -> t.List[str]:

では、早速デモとして、色々なSQL変換をしてみましょう!

デモ

本デモでは、以下のトランスパイルを行い、SQLGlotの実力を確かめます。

  1. 文法の翻訳(Oracle SQL -> BigQuery SQL)
  2. 意味の翻訳(BigQuery SQL -> Snowflake SQL)

インストール

何よりもまず、sqlglotをインストールします。

uv add sqlglot[rs]
コラム: [rs]ってなに?

sqlglot実装のうち、tokenizer部分をrustにしたものを指しています。速いです。rustバンザイ!

これであとはコードを書いていくだけです。いざっっ!

1. 文法の翻訳(Oracle SQL -> BigQuery SQL)

最初に、以下のOracle SQLを同等のBigQuery SQLに翻訳できるか試してみましょう!

Oracle SQL
select nvl(col1, 'default') from table;

早速、上記SQL文に関してtranspile関数を呼び出してみます。

main.py
import sqlglot

print(sqlglot.transpile("select nvl(col1, 'default') from table;"
    , read="oracle", write="bigquery"))

実行すると、以下のように結果が返りました。

$ uv run python main.py
["SELECT COALESCE(col1, 'default') FROM table"]

SQLGlotは、nvlcoalesceを差し替えました。Oracle SQLのnvl(expr1, expr2)は、expr1がnullであればexpr2を返し、nullでなければexpr1を返します。一方で、BigQuery SQLのcoalesce(expr[, ...])は、最初のnon-null exprを返します。

つまり、coalesceの指定引数が2つの今回の場合、coalescenvlと同等の働きをします。したがって、本変換前後で結果データは変わらないはずなので、翻訳は成功したと言えそうです!

2. 意味の翻訳(BigQuery SQL -> Snowflake SQL)

次に、以下のBigQuery SQLをSnowflake SQLに翻訳しようと思います。

BigQuery SQL
select * from person order by age asc;

ここで、一見「あれ、これってSnowflakeでも同じ記法では?」と思うかもしれません。しかし、上記SQLの記法は、Snowflakeのそれと意味が違います。order by句の挙動に関しては、下図のように、BigQueryではnullが先に来ますがSnowflakeでは後に来るのです。


出典: これからのSQL開発 — dbt FUSION engineの本質とその先の世界

したがって、文法が同じだからといって変換をしないと、異なる結果セットを返す場合があるのです。SQLGlotがこれを正しく解決できるのか試してみましょう。

コード:

main.py
import sqlglot

print(sqlglot.transpile("select * from person order by age asc"
    , read="bigquery", write="snowflake"))

実行結果:

$ uv run python main.py
['SELECT * FROM person ORDER BY age ASC NULLS FIRST']

SQLGlotは、Snowflake SQLにNULLS FIRSTを追加しました。NULLS FIRSTは、nullの順番をnon-null値より先にするものです。よって、変換後のクエリは、元のsort順を保持することができました!!最高ですね。

すごいぞSQLGlot!

今回のデモでは、文法・意味を考慮して翻訳できるSQLGlotの魅力をお伝えできたかと思います。

特に強調したいことは、2つ目で紹介した意味の差の特定が、人間だけでなく現状のLLMにも困難なタスクであるということです。丁寧な意味分析を実装してくださったSQLGlotには本当に頭が上がりませんね。

Claude Opus 4.1では意味を捉えられなかった

注意点 - されど銀の弾丸などない

ここまでSQLGlotの素晴らしさをご紹介してきました。「これでSQL方言はベンダーフリーだ!!」と喜びたいところですが、そうはならないのが令和の世です。

例えば、執筆時点で以下のpresto → hiveの厳密トランスパイルはできず、ベストエフォートな変換になってしまいます。

コード:

import sqlglot
sqlglot.transpile("SELECT APPROX_DISTINCT(a, 0.1) FROM foo"
    , read="presto", write="hive")

実行結果:

APPROX_COUNT_DISTINCT does not support accuracy
'SELECT APPROX_COUNT_DISTINCT(a) FROM foo'

つまり、SQLGlotのトランスパイルは網羅的でありません。これは、単純な実装漏れだけでなく、クエリ中に現れない追加情報(e.g. 参照テーブルのスキーマ情報)が必要なことによるものです。公式ドキュメントでは、SQLGlotはインクリメンタルにこうした問題を解決していくと明記されています。

したがって、SQLGlotを活用する際であっても、(特に重要なクエリに関しては)必ず変換クエリの実行結果セットが変換前のそれと同等であることをチェックすることが重要です。

ちなみ - dbt Fusionの場合

dbt Fusionは、SQLコンパイラを内包するため、SQLGlotと同様にSQLを構造的に理解することができます。さらに、SQLGlotが独立したクエリしか情報を持たないのに対して、Fusionはdbtモデル同士の依存関係やソーステーブルのスキーマ情報を有しています。

つまり、Fusionは、SQLコードの文脈を大量に持っているわけです。よって、SQLGlotが取り組みづらかったトランスパイル(e.g. 参照テーブルのスキーマ情報が必要なもの)も、Fusionなら、将来的には保有する文脈を活用して厳密にできてしまうかもしれません。SQLでwrite once, run anywhereの世界線も夢ではないかも。。

出典: これからのSQL開発 — dbt FUSION engineの本質とその先の世界

アツいぞFusion!!!

まとめ

いかがでしたでしょうか?本記事では、一般的なSQL方言間のトランスパイルであれば、SQLGlotが良い選択肢になり得ることをご紹介しました。

SQLGlotは構文だけでなく、order by句でのnullの扱いのような意味的な違いまで考慮してくれる強力なツールです。全てのケースを網羅する銀の弾丸ではありませんが、多くのユースケースで利用できると思いますので、お手元でぜひ試してみてください!

ここまでお読みいただきありがとうございました。


stableでは、dbt をはじめ、各種データウェアハウス・BI におけるデータ支援業務を実施しています。社内でデータ基盤に関する課題を抱えている方は、お気軽に下記からご相談ください。
https://stable.co.jp/contact

また、採用も行なっているので、データエンジニアリング領域のクライアントワークにご興味がある方もぜひお声がけください。Pittaを開放しているので、よろしければ一度カジュアルに話しましょう。
https://pitta.me/matches/loYNLznDRaqV

Discussion