把 SQL 跑出来是一回事,把 SQL 让别人能读是另一回事。格式化(formatter) 是后者,前者是数据库的事。大多数开发者的格式化习惯是「在编辑器里手按 Tab 调缩进」,这能解决 30% 的问题,剩下的 70%(方言差异、关键字大小写、字符串里的分号、注释被吞、CTE 没递归、子查询该不该独立行)是手按 Tab 解决不了的。
SQL 格式化器的核心价值不是「让 SELECT 看得好看」,而是让查询在 code review、生产事故复盘、慢查询日志分析时被人眼读得懂。所以它属于基础设施,不是美化器。本文 8 个真实踩坑,每个「症状 + 为什么 + 怎么修」三段式讲清楚,文末给 Piick SQL 格式化工具,5 种方言 + 3 种大小写 + 4 种缩进 + leading-comma + 语法高亮 + 点击错误位置跳转 + 5 项统计,1 分钟走完。
30 秒总览
-
SQL 格式化器 ≠ 美化器:前者是查询审计 / code review / 慢查询日志的可读性基础设施,后者只是「让代码看起来整齐」
-
方言决定一切:同一个
SELECT * FROM "my column"在 MySQL 是字符串、在 PostgreSQL 是带引号标识符、在 T-SQL 是字符串 + 错误,formatter 不带方言 = 必然改坏 -
8 个常见踩坑:方言错配、关键字大小写混用、字符串里的分号触发假 split、naive formatter 吞掉注释、标识符引号自动改写、WITH CTE 没递归、子查询全内联 derived table 不独立行、minify 后 debug 看不到结构
-
修复方向:永远带方言选项的 formatter,永远不自动改写引号,永远把 comment / CTE / 派生表当一等公民
-
用 Piick SQL 格式化工具在线格式化 / 压缩 / 美化,5 种方言 + 语法高亮 + 错误位置点击跳转,数据 100% 本地不上传,生产 DDL 也能放心粘贴
8 个常见踩坑
踩坑 1:方言错配(MySQL backtick 跑到 PG 炸)
症状:PG 数据库的 query 在 MySQL-style formatter 跑出来,所有 "my column" 变成 `my column`。粘回 psql 执行,报 ERROR: column "my column" does not exist(因为 PG 用双引号包标识符,反引号不是合法语法)。
为什么:每个方言的标识符引号字符不一样 — MySQL 用 ` 反引号、PostgreSQL / SQLite / Standard SQL 用 " 双引号、T-SQL 用 [ ] 方括号或双引号。naive formatter 看一眼字符串就当成字符串,根本没考虑方言上下文,所以把 PG 的标识符按 MySQL 规则改写成反引号。
修复:formatter 必须有方言选择器,且方言决定 2 件事 — (a) 关键字识别集合(120+ reserved words 每个方言不同),(b) 标识符引号字符。Piick SQL 格式化工具在 5 种方言间切换,粘贴 PG 的 SQL 选 PostgreSQL,formatter 才不会改你的引号。
真实场景:跨方言迁移 PG → MySQL 时,naive 反向操作,把 PG 的 "my column" 改成 MySQL 的 `my column`,但 PG 的 JSONB 列在 MySQL 是 JSON、PG 的 SERIAL 在 MySQL 是 AUTO_INCREMENT,这不是 formatter 能改的(必须人工重写 schema)。但引号这种「formatter 自己造成的语法错误」必须 0 容忍。
踩坑 2:关键字大小写混用(grep 找不到子句)
症状:code review 时 git blame 一段 SELECT,看到 Select * From users Where id = 1,你想 grep 整个 codebase 里所有 WHERE 子句,正则写 \bWHERE\b(case-sensitive),grep 出来 0 个匹配。然后你改成 -i,发现 codebase 里 70% 是 where(小写)、30% 是 WHERE(大写),原因是有 5 个 formatter 模板用 UPPER、3 个用 lower、其他不管。
为什么:formatter 不强制关键字大小写,开发者按自己编辑器配置(camelcase 大小写、snake_case)随便写,最后 codebase 里出现 4 种大小写混用 — Select / SELECT / select / sElEcT(是的,真有人这么写)。SQL 关键字不区分大小写,所以语义没问题,但 code review、grep、AST 分析工具全炸。
修复:formatter 加关键字大小写选项(UPPER / lower / Preserve),团队用 ESLint 风格配置文件锁死一种,CI 里跑 formatter 自动统一。Piick SQL 格式化工具在 3 种大小写模式切换,选 UPPER 后所有关键字变 SELECT FROM WHERE,选 Preserve 保留原始(适合已经形成风格的项目)。
真实场景:开源项目接 PR 时,新贡献者混用大小写,reviewer 提 comment「请跑 formatter 再提」,贡献者装了项目推荐的 prettier-plugin-sql 跑一遍,改了 200 行其他无关格式,reviewer 还得再 review 一次。统一大小写应该在 commit 前一次完成,不是 review 阶段反复折腾。
踩坑 3:字符串里的分号触发假 split(整段 SQL 切碎)
症状:一个 INSERT VALUES 包含字符串里带 ; 的脏数据(从用户输入来的 — 比如 notes = 'Error: connection refused; retry';),naive formatter 按 ; 切分语句,生成 2 个伪语句,粘回去执行,第二个「语句」是垃圾字符,数据库报 syntax error。
为什么:naive formatter 用 src.split(';') 切分多语句 SQL,完全没考虑引号状态 — 字符串里的 ; 跟语句结束符在文本上无法区分。正确做法是 tokenizer 先识别引号 / 注释 / 嵌套,再按 ; 切分,只在非字符串 / 非注释状态切。
修复:任何 SQL 处理工具的第一关都是 tokenizer。naive split(';') 在 demo 里跑得通,一上生产数据(含用户输入的脏字符串)立刻炸。用 Piick SQL 格式化工具的 tokenizer 是字符级扫描,识别 '...'、"..."(PG 标识符)、`...`(MySQL 标识符)、/* ... */ 嵌套注释、-- ... 行注释、# ...(MySQL)行注释,在所有这些上下文里 ; 都不是语句结束符。
真实场景:数据迁移脚本从 CSV 导出 INSERT,CSV 里有 ; 的用户输入是合法业务数据(比如 SQL 语句文本字段、CSV 描述字段)。naive 脚本直接炸,正确做法是用带 tokenizer 的工具切分,或者用 COPY 协议(不要拼 INSERT)。
踩坑 4:注释被 naive formatter 吞掉(where 条件消失)
症状:SELECT * FROM users WHERE active = 1 -- 只看活跃用户,naive formatter 跑完后,注释没了,只留 SELECT * FROM users WHERE active = 1,看着没毛病。但你回看 git diff,发现注释被默默删了 — 注释是为了「为什么 WHERE 这么写」的业务解释,删了之后 6 个月后的你看到这段 SQL 完全不知道为什么 active=1。
为什么:很多 formatter 把「格式化 = 删除注释 + 重新布局 token」两步合一。注释 token 不影响语义,所以被当垃圾扔掉。但注释是 schema 演进的关键证据(「为什么这个字段是硬编码 1 不是配置」、「为什么 JOIN 这个 deprecated 表」),删了等于删掉项目历史。
修复:formatter 必须把 comment token 作为一等公民,保留位置和内容,minify 模式才能删(因为 minify 的目的是给生产跑,不给人看)。Piick SQL 格式化工具在格式化模式下保留 -- 行注释和 /* */ 块注释(包嵌套),只在「Minify」模式下删除注释。
真实场景:慢查询日志分析时,你看到一段 SELECT * FROM orders WHERE created_at < '2020-01-01' -- 数据归档前临时查询,清理后删,formatter 跑一次后注释没了,你 6 个月后回看这条 SQL 不知道「为什么 created_at 写死 2020-01-01、是 bug 还是临时查询」,只能再问老同事。注释是项目时间机器,formatter 删注释等于删项目历史。
踩坑 5:标识符引号被自动改写(跨方言移植炸)
症状:PG 的 SELECT "userId" FROM "users",naive formatter 跑完变成 SELECT \userId` FROM `users“(用了 MySQL 反引号)。粘回 PG,报 syntax error — PG 不识别反引号是标识符引号。
为什么:formatter 看到双引号,按自己方言的「标识符引号规则」改写,但没考虑用户输入原本是哪种引号。如果用户原本就是 PG / Standard / SQLite 的双引号,改写成反引号就语法错误。如果用户原本是 MySQL 的反引号,改写成双引号在 MySQL 也错(双引号在 MySQL 是字符串,不是标识符,除非 ANSI_QUOTES SQL mode 开启)。
修复:formatter 默认保留输入引号字符,不主动改写。如果用户真要改写方言(比如 PG → MySQL),那是另一个工具的事 — schema migration tool(Prisma / SQLAlchemy / Flyway),不是 formatter。Piick SQL 格式化工具严格保留输入的引号字符 — 你贴什么引号,输出就是什么引号,永远不会改写。
真实场景:数据科学团队从 PG 导出 query 给 BI 工具,BI 工具用 MySQL 兼容引擎,数据科学家手动把 SELECT "userId" 改成 SELECT userId(去掉引号,因为小写不需要引号)。这是人工判断,不是 formatter 的工作。formatter 把已经正确的 SQL 改坏,是最低级的 bug。
踩坑 6:WITH (CTE) 没递归(多 CTE 堆一行)
症状:WITH active_users AS (SELECT id FROM users WHERE active=1), recent_orders AS (SELECT user_id FROM orders WHERE created_at > NOW() - INTERVAL '7 days') SELECT COUNT(*) FROM active_users a JOIN recent_orders r ON a.id = r.user_id,naive formatter 跑完,所有 CTE name 还在 WITH 后面第一行,CTE body 也没缩进,读起来跟没格式化一样。
为什么:WITH (CTE) 是 SQL 标准引入的子查询命名机制,一个 WITH 可以有多个 CTE name(逗号分隔),每个 CTE name 后面跟一个 (SELECT ...) 作为定义。naive formatter 把「WITH … SELECT」当一行布局,根本没考虑 CTE 是个列表。
修复:formatter 必须识别 CTE 列表,每个 CTE name 独立一行,body 缩进 1 级(SELECT 体本身再缩进 1 级)。多 CTE 嵌套时(CTE body 里又用 WITH),递归处理每一层。Piick SQL 格式化工具对 WITH 递归格式化,每个 CTE name 独立行 + body 缩进,嵌套 CTE 自动递归。
真实场景:数据团队的 analytics query 一般 5-10 个 CTE 串起来组成 pipeline,naive formatter 把它拍平成 3 行,reviewer 完全读不出来哪步做了什么。用递归 CTE 格式化后,每个 CTE 像一个独立小函数,可读性提升 10 倍。
踩坑 7:子查询全内联(derived table 看不见结构)
症状:SELECT u.name, o.total FROM users u JOIN (SELECT user_id, SUM(amount) AS total FROM orders GROUP BY user_id) o ON u.id = o.user_id,naive formatter 把 (SELECT user_id, ... GROUP BY user_id) 子查询 inline 在 JOIN 那行,右 pane 一行 80 字符根本显示不完。
为什么:子查询有两种语义 — 标量子查询(作为表达式的一部分,比如 WHERE id IN (SELECT ...))该 inline,因为它本身就是个值;派生表 / derived table(在 FROM 子句里 FROM (SELECT ...) AS t)该独立行,因为它是个表,独立行才能看清结构。naive formatter 不区分,全部 inline。
修复:formatter 必须区分标量子查询 vs 派生表 — 派生表出现在 FROM 子句时独立行 + 缩进,标量子查询 inline。Piick SQL 格式化工具按这个规则处理 — 派生表自动独立行,标量保留 inline。
真实场景:复杂的 BI query 里 FROM 子句有 3-4 个 derived table(每个对应一个 CTE 的 materialized version),inline 后整段 SQL 全挤在 80 字符外,得手动拉到屏幕外看。正确格式化后,每个 derived table 像一个迷你表,可读性瞬间提升。
踩坑 8:minify 失去可读性(debug 看不到结构)
症状:生产 SQL 用 ORM 生成,minify 后跑得没问题。但慢查询日志里捞出来一段 5KB 的单行 SQL,grep WHERE 找不到,因为 minified SQL 没换行符,人眼读出来是 5000 字符一行。
为什么:minify 的目的是减少网络传输字节 + 让数据库 parser 更快 parse(虽然这个收益在现代 DB 上微乎其微),不是给人读的。开发者在生产用 minified SQL 跑没问题,但debug 时一定要看 pretty-printed 版本才能定位问题。
修复:生产部署用 minified SQL(节省传输),debug 时用 pretty-printed 版本(可读)。这意味着你需要 2 个版本 — ORM 生成 minified,formatter 工具生成 pretty-printed 用于调试。日志系统也应该在记录慢查询前格式化,而不是记录原始 minified。Piick SQL 格式化工具支持 Minify 模式 + 5 项统计(statement / keyword / identifier / string / byte),粘贴生产 minified SQL 一键 pretty print + 看 statement 数和 keyword 数。
真实场景:DBA 排查慢查询,日志里捞出来一段 ORM 生成的 minified SQL,DBA 看不出哪个 JOIN 是 N+1。把这段 SQL 丢 Piick SQL 格式化工具,选 Standard SQL + 2-space indent + leading comma,1 秒看到结构,定位到 LEFT JOIN order_items oi ON o.id = oi.order_id WHERE oi.id IS NULL 是 N+1 源头。
工具选型决策
选项 A:编辑器内插件(SQLTools / DataGrip / DBeaver)
适合:每天写 SQL、已经用 IDE 的人。
优点:实时格式化、不离开编辑器、跟 schema 自动补全集成。缺点:只能本地 SQL、不能跨方言同步(你 IDE 里 MySQL formatter,生产环境 PG,改写引号炸)、不支持批量处理(团队 CI 自动格式化得另搭一套)。
选项 B:npm sql-formatter 包
适合:CI / build pipeline 里自动格式化、有 Node.js 环境的项目。
优点:可以集成到 CI、CI 失败直接报错、版本锁死保证团队一致。缺点:~200KB 依赖 + 拉一堆 transitive deps、版本升级偶尔 break 老 SQL、需要 Node 环境(纯前端项目用不上)、不能 syntax highlight + 错误位置点击跳转(纯文本输出)。
选项 C:Piick SQL 格式化工具
适合:跨方言处理(同一段 SQL 在 5 种方言间切换看哪个对)、不想装 npm 包(纯前端项目)、敏感数据(生产 DDL、含 PII 的 query)不想过任何第三方服务器、DBA / 团队 code review 时快速看结构。
优点:零依赖(纯前端 token + AST 处理)、5 种方言(Standard / MySQL / PostgreSQL / SQLite / T-SQL)、3 种关键字大小写、4 种缩进、leading/trailing 逗号、语法高亮、错误位置点击跳转、5 项统计、数据 100% 本地不上传。缺点:不能集成到 CI(用选项 B 补)、不能自动 save(纯前端工具,得手动复制)。
决策树:
-
已经在写项目级 SQL → 选项 A(IDE 插件)
-
CI 集成 + Node 环境 → 选项 B(npm 包)
-
跨方言调试 + 敏感数据 + 不装任何东西 → 选项 C(Piick)
5 个真实场景
场景 1:跨方言数据迁移(PG → MySQL)
PG 的 SELECT "userId", COUNT(*) FROM "users" WHERE "createdAt" > NOW() - INTERVAL '7 days' GROUP BY "userId",你想在 MySQL 上跑(测试环境 PG 没配,用 MySQL 临时测)。步骤:
-
用 Piick SQL 格式化工具选 PostgreSQL,先把 PG 版本格式化(保留双引号 + PG 关键字)
-
手动改 schema 相关:
"users" -> users(去引号,因为小写不需要引号),INTERVAL '7 days'改DATE_SUB(NOW(), INTERVAL 7 DAY) -
再用 Piick 选 MySQL,把改完的 SQL 重新格式化(让 MySQL 的关键字识别一致)
-
粘到 MySQL 执行
不要:用 naively 反向操作(formatter 自动把 PG 双引号改 MySQL 反引号),SQL 语法错 + 字段名错的叠加,debug 翻倍时间。
场景 2:code review 看不懂 JOIN 顺序
团队 PR 提了一段 SELECT * FROM a LEFT JOIN b ON a.id = b.a_id INNER JOIN c ON b.c_id = c.id WHERE ...,reviewer 看了 5 分钟不知道 JOIN 顺序的语义。用 Piick SQL 格式化工具选 Standard SQL + 4-space indent,贴进去 1 秒看到结构:
SELECT
FROM
a
LEFT JOIN b
ON a.id = b.a_id
INNER JOIN c
ON b.c_id = c.id
WHERE
…
reviewer 立刻看出「先 LEFT JOIN b(可能扩行)、再 INNER JOIN c(过滤 b 没匹配的行)」,性能问题一目了然。
场景 3:慢查询日志 minified SQL 还原
DBA 拿到 ORM 生成的一段 5KB 单行 SQL,看不出 N+1。粘贴 Piick SQL 格式化工具选 Minify → 切回 4-space indent,1 秒看到结构,定位到 LEFT JOIN order_items oi ON o.id = oi.order_id WHERE oi.id IS NULL 是 N+1 源头。配合 Piick 的 正则测试器 在 EXPLAIN ANALYZE 输出里 grep 关键字,定位具体慢的子查询。
场景 4:CTE 嵌套 pipeline(数据团队 analytics query)
数据团队的 query 是 8 个 CTE 串起来的 pipeline,naive 编辑器把它拍平成 80 字符一行,reviewer 完全读不出来哪步做了什么。用 Piick SQL 格式化工具对 WITH 递归格式化,每个 CTE 像一个独立小函数:
WITH
step1_active_users AS (
SELECT
id
FROM
users
WHERE
active = 1
),
step2_recent_orders AS (
…
),
step3_joined AS (
SELECT
…
FROM
step1_active_users a
JOIN step2_recent_orders r
ON a.id = r.user_id
)
SELECT
…
FROM
step3_joined
reviewer 可以像读代码一样读 pipeline。
场景 5:production DDL 不离开浏览器
数据团队要把 PG 的 CREATE TABLE users (...) 同步到 staging,DDL 含敏感字段(用户 ID 加密字段的 schema)。不能用在线 formatter(怕 SQL 上传),用 Piick SQL 格式化工具纯前端处理,数据 100% 本地,粘贴完复制结果关页面,SQL 不留任何痕迹。配合 Piick 的 URL 编码解码器 处理 DDL 里 URL 化的字段名,配合 JSON 格式化工具 处理 DDL 里 JSON / JSONB 默认值。
推荐做法
-
永远带方言选项的 formatter,5 种方言至少覆盖 Standard / MySQL / PG / SQLite / T-SQL。naive formatter 在跨方言项目里必然炸
-
永远保留输入的引号字符(双引号 / 反引号 / 方括号),不主动改写。改写是 schema migration tool 的事,不是 formatter
-
保留 comment 作为一等公民,formatter 不删注释 — 注释是项目时间机器,6 个月后回看 SQL 时唯一解释「为什么这么写」的证据
-
WITH (CTE) 递归处理,每个 CTE 独立行,body 缩进,嵌套 CTE 再递归
-
区分标量子查询 vs 派生表:标量 inline(它是值),派生表独立行(它是表)
-
生产用 minified,debug 用 pretty-printed,2 个版本各管各的用途
-
CI 集成:项目级 SQL 用
sql-formatternpm 包(或等价方案)统一格式化,避免 review 阶段反复改格式 -
敏感 DDL 用 Piick 这类纯前端工具:数据 100% 本地,粘贴 → 复制 → 关页面,不留痕迹
-
统计不可少:粘贴后看 5 项指标(语句 / 关键字 / 标识符 / 字符串 / 字节),快速 sanity check SQL 结构(比如 identifier 数突然暴涨,说明 FROM JOIN 多了一堆表,可能是 ORM 生成错了)
SQL 格式化是查询审计和 code review 的可读性基础设施,不是装饰。把 Piick SQL 格式化工具 收藏在书签栏,跨方言调试 / 慢查询日志还原 / production DDL 排查 时开一下 1 分钟走完,不浪费时间。配合 Piick JSON 格式化工具 处理 JSON / JSONB 列、Piick URL 编码解码器 处理 WHERE 子句里的 URL 字段、Piick 正则测试器 在 EXPLAIN 输出里 grep 关键字、Piick Cron 生成器 生成 SQL 调度的 cron 表达式,5 件套一起用覆盖 SQL 上下游的全场景。