SQLGlotは、SQLを構文木(AST)に分解し、別のデータベースの書き方に組み立て直すPythonライブラリです。依存パッケージは無く、pip install sqlglot だけで使えます。本記事では2026年10月20日公開の30.24.0をPython 3.13の仮想環境に入れ、parse_one・transpile・テーブル名の抽出・リネージ・SQLの実行を実際に動かした出力をもとに、使い方と落とし穴を整理します。
まとめ:SQLGlotの要点
| 項目 | 2026年10月時点の事実 |
|---|---|
| 現行版 | 30.24.0(PyPI、2026-10-20公開) |
| 対応Python | 3.9以上(高速版 sqlglot[c] は3.10以上) |
| ライセンス | MIT |
| リポジトリ | github.com/tobymao/sqlglot |
| 組み込み方言 | 32(公式18・コミュニティ14)+プラグイン |
| 高速化の手段 | sqlglot[c](mypyc版)。sqlglot[rs]は非推奨 |
| 版の意味 | MINORが上がると後方互換なし |
SQLGlotの価値は「SQLを文字列ではなく木として扱える」ことに尽きます。方言変換、テーブル名の抽出、列の由来(リネージ)の追跡、クエリの書き換えは、どれも同じ木の読み書きです。一方でSQLGlotは構文チェッカーではありません。実行エンジンが弾くSQLも通すため、入力の検証やセキュリティ上の関門として使う設計にはしないでください。
SQLGlotの主要機能:解析・SQL生成・最適化・実行
READMEの自己定義は SQLGlot is a no-dependency SQL parser, transpiler, optimizer, and engine. です(tobymao/sqlglot)。作者はToby Maoで、2021年3月18日に0.1.0がPyPIへ公開されて以来、30.24.0まで787回リリースされています。開発を担うTobiko Data(SQLMeshとSQLGlotの開発元)は、2025年9月3日にFivetranによる買収が発表されました(Fivetranのプレスリリース)。主な機能とAPIを次の4項目に整理します。
| 層 | 主なAPI | 用途 |
|---|---|---|
| パーサ | parse_one / parse | SQLを式オブジェクトの木にする |
| ジェネレータ | transpile / .sql() | 木を任意の方言のSQLへ戻す |
| オプティマイザ | optimize / qualify / lineage | 列の修飾・正規化・由来の追跡 |
| 実行エンジン | executor.execute | Pythonの辞書に対してSQLを実行 |
ライブラリとしての採用は多く、READMEの「Used By」にはSQLMesh、Apache Superset、Dagster、Ibis、dlt、Splink、Querybookなどが並びます。Supersetは pyproject.toml で sqlglot>=30.19.0, <31 を要求しており、BIツールがSQLを解釈する層に組み込まれています(構成はApache Supersetとは?構成・Docker導入手順とMetabase比較で採用判断を解説【2026年版】)。Pythonのデータフレーム風APIをSQLに変換するIbis(Python)とは?統一APIで20以上のバックエンドを扱う使い方も、内部でSQLGlotを使って各バックエンドの方言を生成しています。
インストール手順:pip・sqlglot[c]・バージョン固定
pip install sqlglot と高速版 sqlglot[c] の違い
導入は次のどちらかです。
# 純粋なPython版(依存なし)
pip install sqlglot
# mypycでコンパイルした高速版(Python 3.10以上)
pip install "sqlglot[c]"
[c] を付けると sqlglotc という別パッケージが入り、parser や tokenizer がコンパイル済みの .so に置き換わります。TPC-H Q1 相当のクエリ1本を200回処理する計測を5セット行い、1クエリ当たりの平均時間の中央値を比べました(Intel Core i9-9880H・macOS 26.6.2・Python 3.13.14・sqlglot/sqlglotc 30.24.0)。純粋Python版と [c] 版は別の仮想環境に入れ、同じスクリプトで計っています。
import statistics, time
from sqlglot import parse_one, transpile
def bench(f, n=200, sets=5):
per_call = []
for _ in range(sets):
t0 = time.perf_counter()
for _ in range(n):
f()
per_call.append((time.perf_counter() - t0) / n * 1000)
return statistics.median(per_call) # ミリ秒
Q1 = """select l_returnflag, l_linestatus, sum(l_quantity) as sum_qty,
sum(l_extendedprice) as sum_base_price,
sum(l_extendedprice * (1 - l_discount)) as sum_disc_price,
sum(l_extendedprice * (1 - l_discount) * (1 + l_tax)) as sum_charge,
avg(l_quantity) as avg_qty, avg(l_extendedprice) as avg_price,
avg(l_discount) as avg_disc, count(*) as count_order
from lineitem where l_shipdate <= date '1998-12-01' - interval '90' day
group by l_returnflag, l_linestatus order by l_returnflag, l_linestatus"""
print(bench(lambda: parse_one(Q1)), bench(lambda: transpile(Q1, write="bigquery")))
| 処理 | 純粋Python版 | sqlglot[c] | 比 |
|---|---|---|---|
| parse_one | 3.420 ms | 1.088 ms | 約3.1倍 |
| transpile(BigQuery向け) | 5.423 ms | 2.266 ms | 約2.4倍 |
READMEは「roughly 3-5x faster」、[c] を開発したFivetranの解説記事(2026年5月1日)は解析が約5倍・SQL生成が約2.5倍としています(How we accelerated transpilation by compiling SQLGlot with mypyc)。手元の計測でも、生成を含む transpile では差が縮みました。数千本規模のSQLを一括変換するバッチなら [c] を入れる価値があります。Webアプリで都度解析する用途では、SQLの長さや同時リクエスト数で負荷が変わるため、まず純粋Python版で解析時間を測り、応答時間やCPU使用量が要件を超えたら [c] に切り替える順序が無難です。
[c] には2つの落とし穴があります。1つ目はPython 3.9です。macOS標準の /usr/bin/python3(3.9.6)で pip install "sqlglot[c]" を実行すると、エラーも警告も出ずに純粋Python版だけが入ります。sqlglotc の依存条件が python_version >= "3.10" のためです。2つ目は独自方言です。[c] 環境で Dialect を継承し、内側で Generator を継承したクラスを定義すると、定義までは通りますが、その方言でSQLを生成した時点で次の例外が出ます(Tokenizer の KEYWORDS を足すだけの方言は動きました)。
TypeError: interpreted classes cannot inherit from compiled
READMEも、mypycでコンパイルしたクラスは実行時のサブクラス化を無効にしているため独自方言は純粋Python版が必要になりうる、と注意しています。独自方言を書くなら純粋Python版の環境を分けてください。
sqlglot[rs](Rustトークナイザ)が非推奨になった経緯
以前は pip install "sqlglot[rs]" でRust製トークナイザ(sqlglotrs)を入れるのが高速化の定番でした。この方式は2026年2月23日のコミット「feat!!: remove rust and build c (#7120)」で本体から外れ、同日の29.0.0からはmypyc版に置き換わっています。PyPIの sqlglotrs の説明文は Deprecated: use sqlglotc instead です。30.24.0の tokens.py も、sqlglotrs だけが入っている環境で次の警告を出します。
sqlglot[rs] is deprecated and no longer compatible with sqlglot. Please use sqlglotc instead for faster parsing: pip install sqlglot[c]
2025年以前の日本語記事や社内手順書に [rs] が残っていたら、[c] に書き換えてください。現行の [rs] extra は互換のために sqlglotc も引き込む定義になっていますが、Rust部分は使われません。
バージョン固定:MINOR更新時の非互換変更
SQLGlotの版番号は一般的なセマンティックバージョニングと意味が違います。READMEの定義は次のとおりです。
- PATCH:後方互換のある変更
- MINOR:後方互換のない変更
- MAJOR:大きな後方互換のない変更
リリースは速く、30.13.0(2026-07-20)から30.24.0(2026-10-20)までの3か月でMINORが11回上がりました。CHANGELOGの「BREAKING CHANGES」に載った項目は、30.21.0〜30.24.0の4回だけで計36件(6・23・3・4件)あります。sqlglot>=30 や ^30.0 のような指定では、再インストールのたびに出力SQLが変わりえます。依存側の指定も分かれています。SQLMeshは sqlglot~=30.8.0(30.8系のPATCHだけ許可)、Supersetは sqlglot>=30.19.0, <31(MINORの更新も許可)です。変換結果を本番に流すなら sqlglot==30.24.* のようにMINORまで固定し、更新時は変換結果のスナップショットテストで差分を確認するのが安全です。
Node.js向けsqlglot-tsの提供元と本家との差異
npmの sqlglot-ts は、Ilia Ablamonov氏(Flamefork)による第三者のTypeScript移植です。tobymao/sqlglot のリポジトリやREADMEには記載が無く、npmレジストリの最新版は0.1.5(公開日時2026-03-13)です。Node.jsから本家と同じ変換結果を得たいなら、Pythonのプロセスを呼び出すほうが確実です。
parse_oneでSQLをASTに変換する手順
parse_oneの戻り値とrepr
parse_one はSQL文字列を式オブジェクトの木にします。repr() で構造を確認できます。
from sqlglot import parse_one
e = parse_one("SELECT a, SUM(b) AS s FROM x WHERE c > 1 GROUP BY a")
print(repr(e))
Select(
expressions=[
Column(
this=Identifier(this=a, quoted=False)),
Alias(
this=Sum(
this=Column(
this=Identifier(this=b, quoted=False))),
alias=Identifier(this=s, quoted=False))],
from_=From(
this=Table(
this=Identifier(this=x, quoted=False))),
where=Where(
this=GT(
this=Column(
this=Identifier(this=c, quoted=False)),
expression=Literal(this=1, is_string=False))),
group=Group(
expressions=[
Column(
this=Identifier(this=a, quoted=False))]))
ノードの型は sqlglot.exp(sqlglot.expressions)のクラスです。Select、Column、Table、Alias、Sum といった型で木を検索・置換するのが基本操作になります。
読み込み方言の省略によるバッククォート・TOPの解析エラー
READMEのFAQで最初に挙がる質問が「正しいはずのSQLがパースできない」で、答えは読み込み方言の指定漏れです。dialect を省くと「SQLGlot方言」として読まれます。READMEは全方言の上位集合を目指すと説明していますが、実際にはMySQLやBigQueryのバッククォートもT-SQLの TOP も通りません。
from sqlglot import transpile
transpile("SELECT `a` FROM t", write="postgres")
# ParseError: Invalid expression / Unexpected token. Line 1, Col: 10.
transpile("SELECT TOP 10 name FROM users", write="postgres")
# ParseError: Invalid expression / Unexpected token. Line 1, Col: 13.
出どころの分かっているSQLは、必ず parse_one(sql, read="bigquery") のように方言を渡してください。エラーの位置はメッセージの文字列を解析しなくても、ParseError の errors 属性から辞書で取り出せます。ログや画面に「何行目の何文字目か」を出したいときに使えます。
from sqlglot.errors import ParseError
try:
parse_one("SELECT foo FROM (SELECT baz FROM t")
except ParseError as e:
err = e.errors[0]
print(err["description"], err["line"], err["col"], err["highlight"])
# Expecting ) 1 34 t
parse_oneの複数文入力とBlock型の戻り値
名前から「1文だけ受け付ける関数」と思われがちですが、30.24.0の parse_one はセミコロン区切りの複数文をエラーにしません。
r = parse_one("SELECT 1 AS a; DROP TABLE users")
print(type(r).__name__) # Block
print(r.sql()) # SELECT 1 AS a; DROP TABLE users
「parse_one が通ったから単一のSELECTだ」と判断するコードは、後ろに付いた DROP TABLE を素通しします。文ごとに扱いたいなら sqlglot.parse() でリストを受け取り、ルートがSELECT型かは isinstance(r, exp.Select) で確認できます。ただし、SELECT INTOやデータ更新を含むCTEもこの判定を通るため、読み取り専用の保証には使えません。
transpileでSQL方言を変換する方法
主要な方言ペアの変換結果
transpile は read(読む方言)と write(書く方言)を受け取り、SQL文字列のリストを返します。関数名だけでなく、意味を保つための書き換えまで行う点がSQLGlotの強みです。30.24.0での実行結果を並べます。
import sqlglot
sqlglot.transpile(
"SELECT DATE_ADD(dt, 7), date_format(dt, 'yyyy-MM-dd') FROM t",
read="spark", write="duckdb")[0]
# SELECT CAST(dt AS DATE) + INTERVAL 7 DAY, STRFTIME(CAST(dt AS TIMESTAMP), '%Y-%m-%d') FROM t
sqlglot.transpile(
"SELECT DATE_TRUNC(order_date, MONTH), SAFE_DIVIDE(a, b) FROM `proj.ds.orders`",
read="bigquery", write="snowflake")[0]
# SELECT DATE_TRUNC('MONTH', order_date), IFF(b <> 0, a / b, NULL) FROM "proj"."ds"."orders"
sqlglot.transpile("SELECT TOP 10 name FROM dbo.users ORDER BY id",
read="tsql", write="postgres")[0]
# SELECT name FROM dbo.users ORDER BY id NULLS FIRST LIMIT 10
sqlglot.transpile("SELECT IFNULL(a, 0) FROM t LIMIT 5, 10",
read="mysql", write="postgres")[0]
# SELECT COALESCE(a, 0) FROM t LIMIT 10 OFFSET 5
T-SQLからPostgreSQLへの例で NULLS FIRST が足されているのは、昇順ソートでNULLを先頭に置くT-SQLの挙動を、NULLを末尾に置くPostgreSQLで再現するためです。BigQueryの SAFE_DIVIDE も、ゼロ除算でNULLを返す意味を保って IFF に展開されています。人手の移行で見落としやすいのはこの種の差です。BigQueryとSnowflakeの間で移行を検討している場合は、課金や同時実行の違いをBigQueryとSnowflakeの比較:課金モデルの構造差と同時実行で決める選定基準で先に押さえておくと、SQL以外の論点も揃います。
未対応構文の既定の警告動作と例外による停止
変換先に対応する構文が無い場合、既定では警告をログに出して「できる範囲で」変換します。PrestoのAPPROX_DISTINCTにある精度引数は、Hiveには渡せません。
sqlglot.transpile("SELECT APPROX_DISTINCT(a, 0.1) FROM foo", read="presto", write="hive")
# WARNING:sqlglot:Argument 'accuracy' is not supported for expression 'ApproxDistinct' when targeting Hive.
# ['SELECT APPROX_COUNT_DISTINCT(a) FROM foo']
警告を見落とすと、精度指定が失われたSQLをそのまま実行してしまいます。集計結果が変わる可能性があるため、警告の確認が必要です。移行バッチでは unsupported_level=sqlglot.ErrorLevel.RAISE を渡し、UnsupportedError で止めて人が確認する運用にしてください。
identify=True と大文字小文字の解決
identify=True は識別子をすべて引用符で囲みます。Snowflakeは引用符なしの識別子を大文字で解決し、引用符付きは大文字小文字を区別します(Snowflake Identifier requirements)。そのため SELECT MyCol FROM T に identify=True だけを掛けると "MyCol" になり、実在する MYCOL 列を指さなくなります。先に正規化を通すと意味が保たれます。
from sqlglot.optimizer.normalize_identifiers import normalize_identifiers
e = parse_one("SELECT MyCol FROM T", read="snowflake")
normalize_identifiers(e, dialect="snowflake").sql("snowflake", identify=True)
# SELECT "MYCOL" FROM "T"
対応方言の一覧と3つのサポート区分
本記事の「32方言」はREADMEの掲載区分を数えたもので、公式(Official)18、コミュニティ(Community)14です。30.24.0の実装上の登録数はSpark2を別に数えると33です。公式方言はコアチームが優先的に保守します。コミュニティ方言は主にコミュニティの貢献で保守され、問題解決の優先度が公式方言より低くなる場合があります。
| 区分 | 方言 |
|---|---|
| 公式 | Athena, BigQuery, ClickHouse, Databricks, DuckDB, Hive |
| 公式 | MySQL, Oracle, Postgres, Presto, Redshift, Snowflake |
| 公式 | Spark, SQLite, StarRocks, Tableau, Trino, TSQL |
| コミュニティ | DAX, Doris, Dremio, Drill, Druid, Dune, Exasol |
| コミュニティ | Fabric, Materialize, PRQL, RisingWave, SingleStore, Solr, Teradata |
| プラグイン | YDB, MaxCompute(別パッケージ) |
プラグイン方言は28.6.0(2026-01-13)から使える仕組みで、sqlglot.dialects のエントリポイントに登録した外部パッケージを read/write の名前で呼べます。SQLGlotのチームは保守しないため、不具合は各パッケージのリポジトリへ報告します。
テーブル名・カラム名の抽出方法
SQLからテーブル名を取り出す用途では、find_all(exp.Table) が紹介されることが多いものの、この方法はCTE名も拾います。また、下のCTE名を集合から引く方法は、同名の実テーブルまで除外するため、この例に限った簡易処理です。
from sqlglot import parse_one, exp
q = """WITH recent AS (SELECT * FROM orders WHERE dt > '2026-01-01')
SELECT r.id, c.name FROM recent r JOIN customers c ON r.cid = c.id
WHERE r.id IN (SELECT order_id FROM refunds)"""
t = parse_one(q)
sorted({x.name for x in t.find_all(exp.Table)})
# ['customers', 'orders', 'recent', 'refunds'] ← recent はCTE
ctes = {c.alias_or_name for c in t.find_all(exp.CTE)}
sorted({x.name for x in t.find_all(exp.Table)} - ctes)
# ['customers', 'orders', 'refunds']
sorted({c.sql() for c in t.find_all(exp.Column)})
# ['c.id', 'c.name', 'dt', 'order_id', 'r.cid', 'r.id']
アクセス権の棚卸しやCRUD表づくりで recent を実テーブルとして数えると、存在しない表が一覧に混ざります。CTEと同名の実テーブルがあっても正しく数えるには、sqlglot.optimizer.scope.build_scope で各スコープの参照元を調べ、exp.Table だけを集めます。名前は exp.table_name() で取り出してください。source.sql() にすると別名まで付いて customers AS c が返ります。なお build_scope は INSERT INTO t SELECT * FROM u のような文でも、含まれるSELECT部分のスコープは返しますが、DELETE FROM users や UPDATE users SET a = 1 を渡すと None になり、次の関数は traverse で落ちます。更新系も含めたCRUD表では、文の型で処理を分けてください。
from sqlglot.optimizer.scope import build_scope
def real_tables(tree):
found = set()
for scope in build_scope(tree).traverse():
for _alias, (_node, source) in scope.selected_sources.items():
if isinstance(source, exp.Table):
found.add(exp.table_name(source))
return sorted(found)
real_tables(t)
# ['customers', 'orders', 'refunds']
real_tables(parse_one("WITH recent AS (SELECT * FROM raw.recent) SELECT * FROM recent"))
# ['raw.recent'] ← CTE名を引く方法では空集合になる例
列は r.id のように別名付きで返ります。qualifyは列の参照先を修飾しますが、別名を実テーブル名へ置き換える機能ではありません。実テーブルの列まで追うには、スコープの参照元を調べるか、後述のlineageを使います。
pretty整形とコメントの扱い
pretty=True で改行とインデントを付けて出力します。
sqlglot.transpile(
"select a,b /* keep */ from t where x=1 and y in (select z from u) -- tail",
write="postgres", pretty=True)[0]
SELECT
a,
b /* keep */
FROM t
WHERE
x = 1 AND y IN (
SELECT
z
FROM u
) /* tail */
行末の -- tail はブロックコメントに変わっています。SQLGlotは木からSQLを作り直すため、元の改行位置・大文字小文字・コメントの形式は保たれません(READMEのFAQも、コメントの保持は best-effort としています)。レビュー差分を最小にしたい整形や、社内のコーディング規約に沿った体裁の強制が目的なら、SQLFluffのようなリンターのほうが向いています。
SQLの組み立てと書き換え(build・transform・diff)
式の組み立てとtransformによるテーブル名の差し替え
文字列の連結ではなく、式オブジェクトを組み立ててSQLを作れます。出力方言を変えれば構文も切り替わります。
from sqlglot import exp
q = exp.select("id", "name").from_("users").where("age >= 20").order_by("id").limit(10)
q.sql("tsql")
# SELECT TOP 10 id, name FROM users WHERE age >= 20 ORDER BY id
既存クエリのテーブル名を差し替えるには transform を使います。次の rename は表名だけで判定する簡易版で、別名付きで参照しているクエリを前提にしています。このとき別名を引き継がないと、参照が壊れたSQLができます。
def rename(node):
if isinstance(node, exp.Table) and node.name == "users":
new = exp.to_table("prod.users_v2")
if node.alias:
new.set("alias", node.args["alias"].copy()) # 別名を引き継ぐ
return new
return node
parse_one("SELECT u.id FROM users u JOIN logs l ON u.id = l.uid").transform(rename).sql()
# SELECT u.id FROM prod.users_v2 AS u JOIN logs AS l ON u.id = l.uid
別名を引き継ぐ2行を省くと出力は FROM prod.users_v2 JOIN ... になり、u.id の参照先が消えます。SQLGlotはこの状態でもエラーを出しません。表名だけで判定しているため、users という名前のCTEや archive.users も置き換わり、SELECT users.id FROM users は SELECT users.id FROM prod.users_v2 になって列の修飾が宙に浮きます。汎用化するなら、node.db とCTE名を条件に足し、表名で修飾された列(exp.Column の table)も合わせて書き換えてください。
diffによる2つのSQLの差分検出
sqlglot.diff は2つの木を比べ、変わったノードを Insert・Remove・Update・Move・Keep の列で返します。改行やSQLキーワードの大文字小文字だけの違いは通常差分になりませんが、識別子や文字列リテラルの大文字小文字の違いは差分になりえます。書き換え処理の前後で、意図しないノードまで変わっていないかを確かめる用途に向きます。
from sqlglot import diff
edits = diff(parse_one("SELECT a, b FROM t WHERE x = 1"),
parse_one("SELECT a, c FROM t WHERE x = 2"))
for e in edits:
name = type(e).__name__
if name in ("Insert", "Remove"):
print(name, e.expression.sql())
elif name in ("Update", "Move"): # この2つは source と target を持つ
print(name, e.source.sql(), "->", e.target.sql())
# Remove 1
# Remove b
# Insert c
# Insert 2
並び順は保証されません。このコード単体では上の順でしたが、同じ入力でもスクリプトの構成を変えると Remove b・Remove 1・Insert 2・Insert c の順で返る実行がありました。差分は件数や集合として比べ、並び順に依存した判定は書かないでください。
オプティマイザとカラムリネージ
optimize・qualifyによる列解決とスキーマ指定
optimize は列の所属テーブルを補い、型に合わせたキャストを入れ、1 = 1 のような冗長な条件を落とします。この例のようにJOIN先の所属列や型を解決するには、スキーマ(テーブルごとの列と型)を辞書で渡します。スキーマなしで処理できる式もあります。
from sqlglot.optimizer import optimize
schema = {"orders": {"id": "INT", "cid": "INT", "amount": "DECIMAL(10,2)", "dt": "DATE"},
"customers": {"id": "INT", "name": "VARCHAR"}}
sql = """SELECT name, SUM(amount) AS total FROM orders o JOIN customers c ON o.cid = c.id
WHERE dt >= '2026-01-01' AND 1 = 1 GROUP BY name"""
print(optimize(parse_one(sql), schema=schema).sql(pretty=True))
SELECT
"c"."name" AS "name",
SUM("o"."amount") AS "total"
FROM "orders" AS "o"
JOIN "customers" AS "c"
ON "c"."id" = "o"."cid"
WHERE
"o"."dt" >= CAST('2026-01-01' AS DATE)
GROUP BY
"c"."name"
スキーマを渡さずに qualify を呼ぶと、JOINで列の所属が決まらないため OptimizeError: Column 'name' could not be resolved. で止まります。information_schema などから列定義を取得して渡す処理を、最初から組み込んでおく必要があります。
lineageによる出力列の由来の追跡
sqlglot.lineage.lineage は、出力列がどのテーブルのどの列から来ているかを木としてたどります。
from sqlglot.lineage import lineage
node = lineage("total",
"SELECT name, SUM(amount) AS total FROM (SELECT o.amount, c.name FROM orders o "
"JOIN customers c ON o.cid = c.id) sub GROUP BY name", schema=schema)
for n in node.walk():
print(n.name)
# total
# sub.amount
# o.amount
サブクエリを1段挟んでも、total が orders.amount に由来することまで追えます。データマートの列ごとに上流の制約を引き継ぐ仕組みを、この機能とCIで組んだ事例も公開されています(ドコモ開発者ブログ)。
executorによるPythonデータへのSQL実行と用途の制限
SQLGlotにはデータベース無しでSQLを実行するエンジンが入っています。テーブルを「列名をキーにした辞書のリスト」で渡すと、解析→最適化→実行計画→実行の順に処理して結果を返します。
from sqlglot.executor import execute
tables = {
"orders": [{"id": 1, "cid": 10, "amount": 1200},
{"id": 2, "cid": 10, "amount": 800},
{"id": 3, "cid": 20, "amount": 500}],
"customers": [{"id": 10, "name": "A社"}, {"id": 20, "name": "B社"}],
}
r = execute("SELECT c.name, SUM(o.amount) AS total "
"FROM orders o JOIN customers c ON o.cid = c.id "
"GROUP BY c.name ORDER BY total DESC", tables=tables)
print(r.columns, r.rows)
# ('name', 'total') [('A社', 2000), ('B社', 500)]
公式の解説(Writing a Python SQL engine from scratch)は、目的をモックデータでのSQLパイプラインの単体テストとし、the implementation is slow and unsuitable for large amounts of data (> 1 million rows) と書いています。入力全体を100グループに集約する GROUP BY を計ると、10万行で3.9秒、100万行で37.3秒かかりました(純粋Python版。sqlglot[c] でも3.4秒・38.4秒で、ほぼ変わりません)。CIでSQLの論理を数十行のデータで確かめる用途に限り、実データの集計はDuckDBなどのエンジンに任せてください。
SQLGlotを使うべきでない場面:構文チェックとSQLの検証
READMEのFAQは SQLGlot is a transpiler, not a validator. と明言しています。30.24.0では、PostgreSQLでGROUP BY不足となるSQLや、関数が未定義なら実行に失敗するSQLもパースできます。
parse_one("SELECT a, SUM(b) FROM t", read="postgres").sql("postgres")
# SELECT a, SUM(b) FROM t ← GROUP BY なしでも通る
parse_one("SELECT no_such_fn(a) FROM t", read="postgres").sql("postgres")
# SELECT NO_SUCH_FN(a) FROM t ← 存在しない関数も通る
検出できるのは括弧の閉じ忘れや SELECT 1 + のような途切れた式といった構文レベルの誤りまでです。次の用途では、SQLGlotの成功を根拠にしないでください。
- ユーザー入力のSQLを受け付ける前の安全確認(複数文がBlockで通る問題も重なる)
- 方言変換後のSQLが実行できることの保証(変換先のエンジンで実行して確かめる)
- 元の改行・引用符・コメントを完全に保持した書き換え(書式規約に合わせる整形とは別の要件)
変換先エンジンのEXPLAINでは、構文や参照先など計画作成時の問題を確認できます。ただし、実行時エラーや変換前後の結果の一致は保証されないため、検証用データでの実行結果も比較してください。ローカルで試せる範囲なら、DuckDBの使い方:インストールからCLI・Python・ファイル読み込みまでの手順【1.5.6対応】で紹介しているように、DuckDBのインメモリ接続で実行してみるだけでも多くの誤りを拾えます。
sqlparse・SQLFluffとの違いと選び方
PythonのSQL関連ライブラリは目的が分かれています。READMEのベンチマーク(Python 3.14.3)では、TPC-Hのクエリの解析にSQLGlotが0.0027秒、sqlparseが0.0142秒、SQLFluffが0.2410秒かかっています。
| ライブラリ | 版 | 得意なこと | 方言変換 |
|---|---|---|---|
| SQLGlot | 30.24.0 | AST化・変換・リネージ | あり |
| sqlparse | 0.6.0 | トークン分割・簡易整形 | なし |
| SQLFluff | 4.4.0 | 規約チェック・自動修正 | なし |
| polyglot-sql | 0.13.4 | Rust実装の方言変換 | あり |
sqlparseはPyPIでの説明が A non-validating SQL parser. のとおり、文をトークン化し、識別子や式を階層的にグループ化するライブラリです。ただし、SQLGlotのようなTable・Column型のASTや、列の所属を解決する機能とは異なります。SQLFluffはdbtプロジェクトなどでSQLの書式規約を強制するリンターです。方言変換、テーブル名や列の由来の抽出、クエリの機械的な書き換えが目的ならSQLGlotを選び、書式の統一だけが目的ならSQLFluffを選ぶ、という分け方で迷いません。なお、Jupyterの代替ノートブックであるMarimoとは?使い方とUI要素・SQLセル・Jupyter移行を実測で解説のSQLセルも、依存として sqlglot[c] を使っています。
READMEのベンチマーク表には、Rustで書かれた sqloxide(sqlparser-rsのPythonバインディング)と polyglot-sql も並んでいます。TPC-Hの解析時間を純粋Python版のSQLGlotを1とした比で見ると、sqlglot[c] が0.27、sqloxide が0.24、polyglot-sql が0.26で、[c] を入れればRust実装とほぼ同じ速さです。polyglot-sql はPyPIでの初公開が2026年4月6日(0.3.0)の0.x系で、30以上の方言の変換をうたっています。このTPC-H解析ベンチマークでは速度差が小さいため、オプティマイザやリネージまで使うならSQLGlotを選ぶのが妥当です。速度を優先する場合は、実際に扱うSQLと解析・変換の処理全体で比較してください。SQLの変換を含むデータ基盤の移行を外部に任せる場合は、データ分析基盤構築・MLOps構築支援で設計から相談できます。
よくある質問
SQLGlotとは何ですか?
Python製のSQLパーサ兼トランスパイラです。SQLを構文木に変換し、BigQuery・Snowflake・DuckDB・PostgreSQLなど32の組み込み方言の間で書き換えられます。依存パッケージは無く、ライセンスはMITです。
SQLGlotはどうやってインストールしますか?
pip install sqlglot で入ります。高速版は pip install "sqlglot[c]" ですが、Python 3.10以上が必要です。Python 3.9では [c] を指定しても純粋Python版だけが入ります。Rust版の sqlglot[rs] は非推奨です。
SQLGlotとsqlparseの違いは何ですか?
sqlparseはSQLをトークンに分けて整形する軽量ライブラリで、方言変換はできません。SQLGlotはSQLの意味を構文木として保持するため、方言変換・テーブル名の抽出・列のリネージ追跡までできます。
SQLGlotは何種類のSQL方言に対応していますか?
30.24.0の組み込み方言は32で、コアチームが保守する公式18とコミュニティ保守の14に分かれます。このほか、YDBやMaxComputeのように別パッケージで提供されるプラグイン方言も使えます。
SQLGlotの公式ドキュメントやGitHubはどこですか?
ソースは GitHub の tobymao/sqlglot、APIドキュメントは sqlglot.com、パッケージは PyPI の sqlglot です。READMEにはインストール、FAQ、方言一覧、ベンチマークがまとまっており、最初に読むならREADMEが最短です。