SQL フォーマッタが実際に決めていること
JSON と違って SQL には正準のシリアライズが存在しないため、フォーマッタは実質的にスタイル決定の束です。第一は節の配置。主要な節(SELECT、FROM、WHERE、GROUP BY、ORDER BY)を 1 行に 1 つずつ置くと、クエリの論理パイプラインが一望できる縦のアウトラインになります。第二は節の内部 — 選択カラムごとに行を分けるか、JOIN 条件を JOIN と同じ行に置くか下にインデントするか、サブクエリをどこまで深くネストさせるか。
そして正解のない論争が続きます。行末カンマ(col,)はほとんどの言語のやり方ですが、次行の先頭に置く行頭カンマ(, col)は SQL アナリストの長い伝統です。最後のカラムを含めどのカラムをコメントアウトしても文が壊れず、カラム追加が 1 行の diff で済むからです。キーワードの整列も割れます。「川(river)」スタイルは SELECT、FROM、WHERE が同じ桁で終わるようにキーワードを右揃えし、クエリに沿って空白の水路を作ります(Simon Holywell の sqlstyle.guide が広めたスタイル)。一方、多くの自動フォーマッタは、編集後に全体を再整列しなくて済む単純な左揃えを好みます。
実用的な価値は美学ではなく正規化です。すべてのクエリが同じフォーマッタを通れば、テキストの diff は意味の diff に縮み、コードレビューからスタイル指摘が消え、クエリの 2 つの変種を機械的に比較できます。これを強制するチームは SQLFluff のようなリンターを使い、Prettier や Black が JS・Python にするのと同じように、CI で方言ごとに整形とチェックを行います。
-- trailing commas -- leading commas (analyst style)
SELECT SELECT
user_id, user_id
email, , email
created_at , created_at
FROM users; -- , last_login ← safe to toggle
FROM users;大文字キーワードと識別子フォールディングの罠
キーワードの大文字化は SQL 最古の慣習で、シンタックスハイライトのないモノクロ端末の時代に生まれました。大文字の SELECT と WHERE だけが、テキストの壁から構造を浮かび上がらせる唯一の手段だったのです。標準はこれを要求していません — SQL キーワードはどこでも大文字小文字を区別しません — し、あらゆるエディタがキーワードを色分けする今、モダンなスタイルガイドの中には全部小文字へ振れたものもあります。どちらでも構いませんが、1 つのコードベース内での混在は駄目です。
ケーシングが危険になるのは識別子です。データベースが黙って書き換えるからです。SQL 標準は引用符なしの識別子を大文字にフォールドすると定めており、Oracle と Db2 はその通りに動きます。my_table は MY_TABLE として保存されます。PostgreSQL は意図的に標準に背いて小文字にフォールドします。MY_TABLE は my_table になります。MySQL は第三の挙動を加えます — テーブル名はファイルに対応するため、大文字小文字の区別はファイルシステムと lower_case_table_names 設定に依存します。macOS では動くのに Linux で壊れるコードの古典的な原因です。
識別子を引用符で囲む("UserAccounts")とフォールドが抑止され、正確なケースが保存されます — 永久に。PostgreSQL で引用符付きの混合ケーステーブルを 1 つ作ると、そのテーブルが存在する限りすべてのクエリで引用符が必要になります。引用符なしの参照は useraccounts にフォールドされて外れるからです。フォーマッタが識別子の引用符内や文字列リテラルに決して触れてはいけない理由がこれです。キーワードの大文字化は安全ですが、識別子の大文字化はクエリが参照するオブジェクトそのものを変えかねません。このすべてから逃れる地味で移植性の高い方法は、どこでも引用符なしの snake_case 識別子を使うことです。
-- PostgreSQL folds unquoted identifiers DOWN
CREATE TABLE MyTable (id int); -- stored as: mytable
SELECT * FROM MYTABLE; -- works, folds to mytable
-- Oracle folds unquoted identifiers UP
CREATE TABLE MyTable (id int); -- stored as: MYTABLE
-- Quotes freeze the case forever (both systems)
CREATE TABLE "MyTable" (id int);
SELECT * FROM MyTable; -- ERROR: relation not found
SELECT * FROM "MyTable"; -- only this works方言の癖 — ANSI・T-SQL・MySQL・PostgreSQL は一致しない
方言を知らなければ、フォーマッタは SQL を正しくトークナイズすることさえできません。識別子の引用だけでも 3 種類あります。ANSI 標準はダブルクォート("order")、MySQL は既定でバッククォート(ダブルクォートは ANSI_QUOTES モードを有効にしたときだけ機能)、T-SQL はダブルクォートに加えて角括弧([order])を使います。角括弧付きの T-SQL 識別子を MySQL 流のフォーマッタに与えると、括弧を構文エラー扱いするか、もっと悪ければ勝手に並べ替えます。
ページネーションは最も目立つ分岐点です。MySQL・PostgreSQL・SQLite は LIMIT 10 OFFSET 20 を使い、SQL Server は歴史的に SELECT TOP 10 を使ってきて、現在は OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY(ORDER BY が必須)をサポートします。ANSI SQL:2008 形式の FETCH FIRST 10 ROWS ONLY は PostgreSQL、Db2、Oracle 12c 以降で動きます。文字列連結も三つ巴です。|| が標準(PostgreSQL、Oracle、SQLite)、T-SQL は +、MySQL では CONCAT() を呼ぶ必要があります。PIPES_AS_CONCAT を有効にしない限り || は論理 OR を意味するからです。ブール値さえ違います — PostgreSQL には本物の TRUE/FALSE リテラルがありますが、SQL Server は BIT の 1/0 で表現します。
コメントには最後の罠があります。-- と /* */ は普遍的ですが、# がコメントを開始するのは MySQL だけで、しかも MySQL の -- は直後に空白がないとコメントとして扱われません。クエリの整形や移植での実務的な教訓: 自分がどの方言で書いているかを把握し、対象がサポートするなら ANSI 形式(FETCH FIRST、||、標準の引用符)を選び、ベンダー固有の構文はすべて、承知の上で背負う移植性の負債として扱うことです。
-- "Rows 21-30, newest first" in three dialects
-- MySQL / PostgreSQL / SQLite
SELECT id, title FROM posts
ORDER BY created_at DESC
LIMIT 10 OFFSET 20;
-- SQL Server (T-SQL)
SELECT id, title FROM posts
ORDER BY created_at DESC
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
-- ANSI SQL:2008 (PostgreSQL, Db2, Oracle 12c+)
SELECT id, title FROM posts
ORDER BY created_at DESC
OFFSET 20 ROWS FETCH FIRST 10 ROWS ONLY;長いクエリの整形 — CTE・JOIN・CASE
整形ルールが真価を発揮するのは、つらいクエリです。5 つの JOIN、3 段のサブクエリ、CASE のはしご。可読性を最も大きく上げる一手は、ネストしたサブクエリを共通テーブル式(CTE)に変えることです。ネストしたクエリは内側から読むしかありません — 最奥の SELECT を見つけて逆向きにたどる必要があります。一方 WITH のチェーンは名前付きのステップとして上から下へ読めます。raw_events、次に sessionized、次に daily_totals。CTE ごとに 1 ブロック、ブロック間に空行という整形にすれば、CTE 名がそのままドキュメントになり、最後の SELECT を替えるだけで各中間ステップを単独で実行・確認できます。
JOIN の整形はミスを可視化する作業です。JOIN ごとに 1 行を与え、ON 条件は短ければ同じ行、長ければ直下にインデント。ON 句が自分の JOIN から遠く離れてペアが見えなくなる事態は絶対に避けます。このレイアウトなら、古典的な JOIN バグが 2 つとも浮かび上がります。ON 条件の欠落による偶発的な直積、そして LEFT JOIN の右テーブルへのフィルタを ON ではなく WHERE に置くミス — マッチしなかった行の NULL が WHERE の判定で落ち、left join が静かに inner join に変わります。
CASE 式は WHEN ... THEN のペアごとに 1 行、ELSE と END は CASE の下に揃えます。1 行の CASE は分岐ロジックだけでなく、忘れがちな事実 — ELSE がなければ NULL になる — も隠してしまいます。そして整形後のクエリが 100 行や CTE 4〜5 個を超えたら、それは設計のシグナルです。ビュー、マテリアライズドな中間テーブル、アプリケーション側での合成のほうが、さらなるインデントより次の読み手のためになるかもしれません。
ミニファイされた SQL・ORM ログ・デバッグの往復
SQL を整形する最も一般的な理由は、それを機械が書いたからです。ORM やクエリビルダー — Hibernate、Prisma、SQLAlchemy、ActiveRecord — は実行したクエリを、$1 や ? のような位置プレースホルダ入りの 1 行としてログに残します。ときに数千文字に及びます。デバッグのループは機械的です。ログからその行をコピーし、ここで整形して節構造を浮かび上がらせ、ログに残ったパラメータ値をプレースホルダに戻し、DB クライアントで EXPLAIN ANALYZE にかける。整形は、読めない生成物を推論できる対象へ変える工程です。ORM が不要なネストした SELECT を出していたり、同じテーブルを二度 JOIN していたりすることは、整形されたクエリでしかはっきり見えません。
逆方向の操作であるミニファイが存在するのは、クエリがどこかに埋め込まれるからです。リテラルの改行をエスケープしなければならない JSON 設定ファイル、環境変数、1 行のシェル heredoc、チャートごとにクエリを保存するダッシュボード。ミニファイは粗い正規化器でもあります — 空白を潰せば、クエリの 2 つの変種をテキスト diff で比較できます。
空白潰しについて正直な注意をひとつ。SQL の文字列リテラルには意味のある連続スペースが含まれえますが、グローバルな空白正規表現を当てる素朴なミニファイアはそれまで潰してしまい、WHERE name = 'a b' を静かに別の述語へ変えてしまいます。文字列リテラルを含むものをミニファイしたら、リテラルを再確認してください。最後に、道具の境界を忘れずに。フォーマッタは正しさについて何も証明しません。削除済みのカラムを参照するクエリも、間違ったキーでフィルタするクエリも、喜んで美しく整えます — 検証はデータベース、SQLFluff のようなリンター、あるいはテストスイートの仕事です。
-- What the ORM logged (one line, placeholders):
SELECT "u"."id", "u"."email" FROM "users" "u" LEFT JOIN "orders" "o" ON "o"."user_id" = "u"."id" WHERE "u"."created_at" >= $1 AND "o"."status" = $2
-- After formatting + substituting $1, $2:
SELECT "u"."id", "u"."email"
FROM "users" "u"
LEFT JOIN "orders" "o"
ON "o"."user_id" = "u"."id"
WHERE "u"."created_at" >= '2026-01-01'
AND "o"."status" = 'paid' -- ← WHERE on right table:
-- this LEFT JOIN acts as INNER