SQL 格式化器

SQL 格式化器究竟在决定什么

与 JSON 不同,SQL 没有规范化的序列化形式,所以格式化器实际上是一捆风格决策。第一是子句布局:每个主要子句(SELECT、FROM、WHERE、GROUP BY、ORDER BY)独占一行,把查询的逻辑管线变成可以纵向扫读的大纲。第二是子句内部怎么办 — 每个选择列是否独占一行、JOIN 条件放在 JOIN 同一行还是缩进其下、子查询嵌套多深。 接着是没有标准答案的争论。尾随逗号(col,)是多数语言的写法;把逗号放到下一行开头的前置逗号(, col)则是 SQL 分析师的老传统,因为注释掉任何一列 — 包括最后一列 — 都不会破坏语句,新增一列也只产生一行 diff。关键字对齐同样分裂:"river" 风格把关键字右对齐,让 SELECT、FROM、WHERE 在同一列结束,在查询中形成一条纵向空白河道(由 Simon Holywell 的 sqlstyle.guide 推广);而多数自动格式化器偏爱简单的左对齐,因为编辑后无需重新对齐一切。 实际价值不在美学,而在规范化。当每条查询都经过同一个格式化器,文本 diff 就收敛为语义 diff,代码评审不再纠缠风格,两个查询变体也能机械比较。执行这一点的团队会用 SQLFluff 这类工具,像 Prettier 之于 JS、Black 之于 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 关键字在任何地方都不区分大小写 — 如今每个编辑器都用颜色高亮关键字,一些现代风格指南已经转向全小写。两种选择都说得通;在同一个代码库里混用则不行。 大小写真正危险的地方在标识符,因为数据库会悄悄改写它们。SQL 标准规定不加引号的标识符折叠为大写,Oracle 和 Db2 正是如此:my_table 被存成 MY_TABLE。PostgreSQL 故意违反标准,折叠为小写:MY_TABLE 变成 my_table。MySQL 加了第三种行为 — 表名映射到文件,大小写敏感性取决于文件系统和 lower_case_table_names 设置,是"macOS 上能跑、Linux 上报错"的经典来源。 给标识符加引号("UserAccounts")会抑制折叠、永久保留精确大小写。在 PostgreSQL 里创建一个带引号的混合大小写表,此后这张表一生中的每条查询都必须带引号,因为不带引号的引用会折叠成 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 各行其是

不知道方言,格式化器连正确分词都做不到。仅标识符引用就有三种: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,|| 在 MySQL 里是逻辑或。连布尔值都不一样 — 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

格式化规则的价值体现在让人头疼的查询上:五个 JOIN、三层子查询、一串 CASE。提升可读性最大的一招是把嵌套子查询改写成公共表表达式(CTE)。嵌套查询只能由内向外读 — 你得先找到最里层的 SELECT 再倒推;而 WITH 链条以命名步骤自上而下阅读:raw_events,然后 sessionized,然后 daily_totals。按"每个 CTE 一个块、块间空行"来格式化,CTE 名字本身就成了文档,而且只需改动最后的 SELECT,就能单独运行和检查每个中间步骤。 JOIN 的格式化在于让错误显形。每个 JOIN 独占一行,ON 条件短则同行、长则缩进在正下方,绝不能让 ON 子句漂离它的 JOIN 远到看不出配对。这种布局能让两个经典 JOIN 缺陷跳出来:缺失 ON 条件导致的意外笛卡尔积;以及把 LEFT JOIN 右表的过滤条件放进 WHERE 而不是 ON — 未匹配行产生的 NULL 过不了 WHERE 检验,left join 就悄悄变成了 inner join。 CASE 表达式应让每对 WHEN ... THEN 独占一行,ELSE 和 END 与 CASE 对齐;写成一行的 CASE 不仅藏住了分支逻辑,还藏住一个容易忘记的事实 — 缺少 ELSE 时结果是 NULL。当格式化后的查询超过 100 行或四五个 CTE 时,把它当作设计信号:视图、物化的中间表或应用层组合,可能比更深的缩进更善待下一位读者。

压缩后的 SQL、ORM 日志与调试往返

格式化 SQL 最常见的原因是:这条 SQL 是机器写的。ORM 和查询构建器 — Hibernate、Prisma、SQLAlchemy、ActiveRecord — 把执行的查询记成一行日志,带着 $1 或 ? 这样的占位符,有时长达数千字符。调试循环是机械化的:从日志复制那一行,在这里格式化让子句结构显现,把日志中的参数值填回占位符,再到数据库客户端里用 EXPLAIN ANALYZE 跑一遍。格式化是把不可读的产物变成可以推理的对象的那一步;只有在格式化后的查询里,你才能一眼看出 ORM 生成了多余的嵌套 SELECT,或者把同一张表 JOIN 了两次。 反向操作 — 压缩(minify)— 之所以存在,是因为查询会被嵌进各种地方:必须转义换行的 JSON 配置文件、环境变量、单行 shell heredoc、每个图表存一条查询的仪表盘。压缩也是一种粗糙的归一化 — 折叠空白后,两个查询变体就能用文本 diff 比较。 关于折叠空白的一句实话:SQL 字符串字面量里可能有语义上重要的连续空格,而对全文应用空白正则的朴素压缩器会把它们一并折叠,悄悄把 WHERE name = 'a b' 变成另一个谓词。压缩任何含字符串字面量的查询之后,请复查字面量。最后记住工具的边界:格式化器不证明任何正确性。它会愉快地美化一条引用了已删除列、或按错误键过滤的查询 — 校验是数据库、SQLFluff 之类的 linter 或测试套件的职责。
-- 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
最后更新:

关于此工具

SQL 格式化工具用统一缩进、关键字大写、每子句一行让长 SELECT 真正可读。还提供 minify 模式去掉换行,方便嵌入到应用代码或环境变量中。任何看过别人把 50 行查询写成 1 行的开发者都会喜欢。

使用方法

  1. Paste the SQL query into the input panel.
  2. Click Format to render an indented, keyword-uppercased version on the right.
  3. Use Minify to collapse the formatted query into a single line for embedding.
  4. Copy the result into your IDE, ORM raw query, BI tool, or migration file.
  5. Iterate until the query reads naturally — formatting helps catch missing JOIN conditions and stray commas.

常见用例

  • Cleaning up a complex JOIN before pasting it into a code review.
  • Formatting an ORM-generated query you copied from a log for debugging.
  • Preparing a migration script for a Pull Request that follows team style.
  • Minifying a query for embedding inside an application config string.
  • Comparing two slightly different queries with a diff tool after formatting both.
  • Sharing a readable query in documentation or a Slack thread.

常见问题

Q. Does the formatter validate my SQL?

A. No — it formats syntax structure but does not verify that columns or tables exist or that the query runs. Use a database client or linter for validation.

Q. Will formatting work for non-standard SQL dialects?

A. Mostly. Common keywords across PostgreSQL, MySQL, SQLite, and MSSQL are recognised. Vendor-specific syntax (PIVOT, MERGE) may format with awkward indentation.

Q. Why does my formatter break a CTE in the wrong place?

A. CTEs (WITH clauses) and subqueries are tricky. If the result looks off, hand-tweak after formatting — it is still faster than starting from scratch.

Q. Should I commit formatted SQL to my repo?

A. Yes — consistent SQL formatting in migrations and stored procedures pays off in code review, just like JS/Python formatting.