SQL 风格指南:可读查询的格式化最佳实践
SQL 风格指南是一套让查询易读、让团队 diff 保持一致的约定。SQL 本身并不在意这些:关键字不区分大小写,空白也被忽略,所以 SELECT、select 和 SeLeCt 运行结果完全相同,一行 200 字符的单行查询与铺成二十行缩进的同一查询返回的行也分毫不差。风格纯粹是为了之后阅读查询的人。
如果你现在只想要一段干净的查询,把它粘进 SQL 格式化,选好方言,复制结果即可。但理解这份输出背后的规则,才能让你制定团队标准,而不是在每个 pull request 里反复争论。本指南会逐一讲清那些要紧的选择:关键字大小写、缩进与换行、命名、方言特有的怪癖,以及如何把整件事自动化。
进入细节之前先做个铺垫。因为 SQL 忽略空白和大小写,这些规则没有一条是数据库强制的,它们存在完全是为了服务阅读、审查、维护查询的人。这带来两个后果。第一,很少存在唯一「正确」的答案;这些决定大多是挑一个合理约定,然后到处一致地应用,本指南也会指出权衡所在,而不是假装某种风格全面胜出。第二,因为这些规则是约定而非要求,它们只有在被一致应用时才产生价值,所以每一节最终都指向同一个结论:决定一次,然后让工具来强制执行。
为什么 SQL 格式化很重要
支持格式化的最清晰论据出现在 code review 里。ORM 或构建步骤常把查询输出成一行不换行的字符串:
select u.id,u.name,count(o.id) as orders from users u left join orders o on o.user_id=u.id where u.active=true group by u.id,u.name order by orders desc
没人能审查这种东西。重新格式化后,结构一目了然,diff 也能逐行审查:
SELECT
u.id,
u.name,
COUNT(o.id) AS orders
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.active = true
GROUP BY u.id, u.name
ORDER BY orders DESC;
调试同样受益。当你从慢查询日志里复制出一行查询,里面有三个 join 和一团乱麻的 WHERE,先格式化一下,就能把「bug 在哪儿」变成三十秒的扫读。出错的谓词独占一行,join 层层堆叠,一个意外的笛卡尔积或一个被遗漏的过滤条件突然变得可见,而不再埋没在一堵文字墙里。读别的系统生成的 SQL 时同样管用,查询构造器和报表工具向来以输出正确但不可读著称。
一致性是更安静的赢面,长期来看也最有价值。当所有人都用同一种方式格式化,diff 只显示真正改了什么,比如一个新列、一处调整过的过滤条件,而不是某人的空格偏好和另一人的偏好相互打架产生的噪音。审查者的注意力有限,把它花在重新排版的空白上是浪费。一致的格式化也让 onboarding 更轻松:新人读到的查询长相都一样,把团队的「形状」学一次,就能到处套用。这一切都不需要任何人手工格式化,这正是最后一节的主题:你定规则,工具来应用。
有一条不变量支撑着这一切,值得明白地写出来,因为它会贯穿全文反复出现:**格式化只改变空白、换行、关键字大小写和注释,它绝不改变查询的逻辑或结果。**格式化后的查询还是同一个查询。所以你可以放心地把上面那段杂乱的版本丢进 SQL 格式化,而不必担心它返回什么。
关键字大小写:UPPERCASE 还是 lowercase
任何 SQL 风格指南里最古老的争论,都是保留关键字该用 UPPERCASE 还是 lowercase。两者都成立,因为 SQL 的关键字不区分大小写。分歧在于可读性,挑边之前值得先理解两边的道理。
支持 UPPERCASE 关键字的理由
传统派的论据是视觉对比。把 SELECT、FROM、WHERE、JOIN 和 GROUP BY 写成大写,能让关键字从小写的表名和列名中跳出来,于是你不必依赖编辑器上色,就能扫读一段查询的形状,看清它的子句和结构。
这件事比听上去更重要,因为 SQL 会流经许多没有语法高亮的地方:日志文件、邮件往来、pull request 描述、纯文本 diff、一条 Slack 消息、一块监控面板、一段堆栈跟踪。在这些场景里,大写关键字是让结构保持可读的少数依靠之一。去掉高亮,select id from users where active 就是一锅小写单词的糊糊,而 SELECT id FROM users WHERE active 仍然一眼看上去就是一段查询。这是你会在大多数老牌风格指南、数据库文档以及几乎每一本 SQL 教科书里见到的约定,也是为什么不少开发者即便编辑器本来会上色,仍然觉得大写关键字更顺眼。
支持 lowercase 关键字的理由
现代派的反驳是:语法高亮已经解决了对比问题。每个编辑器和 IDE 都会把关键字着上不同的颜色,所以把它们大写是多余的,而且在某些读者眼里,全大写读起来像在喊。lowercase 打起来也略快一些,不用每个关键字都去够 shift 键。
lowercase 风格在分析工程(analytics-engineering)圈子里有不小的势头。dbt 社区和几份被广泛引用的团队风格指南都默认 lowercase 关键字,逻辑是高亮承担了视觉分量,lowercase 让查询读起来更平静。还有一个更微妙的点对他们有利:lowercase 关键字和你的 snake_case 表名、列名处在同一视觉层级,于是整段查询读起来像一块一致的文本,而不是大喊的关键字和安静的标识符两种声调争夺注意力。这究竟算特性还是缺陷,恰恰是团队会各执一词的那类问题,也把我们引向那个站得住脚的结论。
结论:一致性胜过具体选哪个
关键的一点是:你挑哪个,远不如挑定一个并强制执行重要。一个代码库里一半查询大喊 SELECT、另一半轻声 select,是最糟的结果,因为不一致本身就成了噪音。单条查询里混用大小写更糟。
一致性取胜的原因是机械的,不是审美的。不一致的大小写会让 diff 撒谎:审查者看到一行「改动」,其实只是有人重新排版了一个关键字,而真正的改动藏在噪音之中。当同一个关键字以三种大小写出现时,grep 和搜索也变得不那么可靠。一种被强制的统一风格,用一次决定的代价消除了所有这些开销。所以团队商量好、写下来,让工具去强制执行,而不是靠自律。SQL 格式化有一个 Keywords 控制项,三个选项 UPPERCASE、lowercase 和 Preserve,所以你可以一键把一大堆历史查询统一成一种风格。同一条查询,两种渲染:
-- UPPERCASE
SELECT id, email FROM users WHERE active = true ORDER BY created_at DESC;
-- lowercase
select id, email from users where active = true order by created_at desc;
挑你团队偏好的那个。重点是你所有的查询都和它一致。
缩进与换行
大小写决定关键字长什么样。缩进与换行决定查询的逻辑如何映射到页面上,而可读性的大头就住在这里。
「河流」风格 vs 块状风格
Simon Holywell 那份知名的 sqlstyle.guide 推广了「河流」(river)风格,关键字右对齐,于是一条垂直的空白通道顺着查询中间流下来:
SELECT id,
email,
created_at
FROM users
WHERE active = true
ORDER BY created_at DESC;
它的吸引力在于 SELECT、FROM 和 WHERE 在右边缘对齐,列清单干净地排在河流右侧。不过缺点很实际。这种对齐取决于你最长关键字的长度,所以加一个 LEFT JOIN 就可能逼你重新缩进所有行,手工维护很痛苦,而且它产生噪音很大的 diff,因为改动一个关键字的长度会挪动相邻行的空白。
块状(或左对齐)风格让每个主要子句从左边距开始独占一行,并缩进该子句的内容:
SELECT
id,
email,
created_at
FROM users
WHERE active = true
ORDER BY created_at DESC;
这是主流默认值,也是大多数工具产出的样子,正因为它稳定:加一个子句永远不会让上面的行重新排版,所以 diff 保持精简,布局也能挺过自动格式化。河流风格为「一段成品查询孤立看上去如何」而优化,块状风格为「查询如何随时间变化、如何在版本控制里被审查」而优化。对任何活在代码仓库里、会被编辑的查询来说,块状风格是更稳妥的押注,本指南余下部分也都默认采用它。
用几个空格:2 vs 4 vs tab
一旦缩进,你就得决定缩多远。三个常见答案各有各的道理:
- 2 个空格:最常见的默认值。它让 diff 保持紧凑,也防止嵌套查询一路挤出屏幕右缘。
- 4 个空格:给每一层嵌套更多视觉分隔,对带深层子查询或多层 CTE 的查询有帮助。
- Tab:让每个开发者自选显示宽度,而不改动文件本身。
这里没有放之四海皆准的答案,正因如此 SQL 格式化提供了一个 Indent 控制项,三种都有(2 spaces、4 spaces、Tab)。选一种,到处应用。
在哪里换行
缩进宽度是容易的部分。影响更大的决定是在哪里插入换行:
SELECT列:任何不算琐碎的情况都一列一行,这样加一列或删一列在 diff 里恰好只动一行。非常短的查询可以留在一行。FROM和JOIN:每个 join 起新行,ON条件要么跟在后面、要么缩进到它下方。这让 join 图保持可读。WHERE:把每个AND/OR放到自己的一行,让布尔逻辑从上到下读下来。对于混用AND/OR的条件,加括号并缩进各组,让优先级是显式的,而不是让读者自己去推。
这些是指引,不是法律。一句琐碎的 SELECT id FROM users WHERE id = 1 不需要五行,硬把它撑成五行反而损害可读性。判断的尺度大致是:当查询有超过一两列、超过一张表、或超过一个条件时就换行。低于这个门槛单行更清晰,高于它就大胆换行。一个好的格式化器会替你编码一个合理的门槛,但理解其中的原则仍然值得,这样输出就不会让你意外。
把它套用到前面那行杂乱的单行查询上,这些规则产出一种每个子句、每个 join 都一目了然的布局:
SELECT
u.id,
u.name,
COUNT(o.id) AS orders
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.active = true
GROUP BY u.id, u.name
ORDER BY orders DESC;
行首逗号 vs 行尾逗号
一个更小但持续存在的问题:在多行列清单里,逗号该放哪儿?
-- Leading commas
SELECT
id
, email
, created_at
FROM users;
-- Trailing commas
SELECT
id,
email,
created_at
FROM users;
行首逗号(leading)有个实在的好处:加一列或删一列只改动一行,而且漏掉的逗号容易被发现,因为出问题的那行很扎眼。行尾逗号(trailing)读起来更自然,实践中也常见得多。两者都行,挑一个,让格式化器去应用,这样谁也不用再去想它。
表与列的命名约定
格式化管的是空白;命名管的是标识符本身,一份风格指南少了它就不完整。
SQL 标识符的事实标准是 snake_case,全小写,单词用下划线分隔:user_id、created_at、order_items。它配得上这个地位是有具体原因的,不只是习惯:snake_case 标识符永远不需要加引号,并且在各方言间行为一致,而 camelCase(在应用代码里常见)会和数据库折叠大小写的方式冲突,这一点我们马上会讲到。
值得明确说说为什么这和应用代码不同。在大多数编程语言里,周围的代码控制着标识符,camelCase 或 PascalCase 是常态。而 SQL 标识符是由数据库自身的大小写折叠规则来解释的,这些规则恰恰就是让混合大小写名称变脆弱的元凶。snake_case 整个绕开了这个问题:没有大小写可折叠,没有理由加引号,也没有任何东西在不同引擎间行为不一致。
还有几条几乎出现在每一份 SQL 风格指南里的约定:
- 表名用单数还是复数确实存在分歧。
users(复数,「这张表装着 users」)和user(单数,「每一行是一个 user」)各有拥护者。和大小写一样,选哪个不如对每张表都一致应用重要。 - **避免用保留字做标识符。**把一个列命名为
order、user或table,会逼你到处给它加引号,还招来令人困惑的报错。改用order_id或account吧。 - **保持键命名一致。**叫
id的主键和命名为<referenced_table>_id(比如user_id)的外键,让 join 可预测、自带说明。
有一个陷阱值得明确点出来,因为它会咬到那些按命名应用变量的方式来命名数据库列的团队。在 PostgreSQL 里,未加引号的标识符会被折叠成小写,所以 SELECT userId FROM t 实际上找的是一个叫 userid 的列。而一旦你给它加上引号 "userId",数据库就保留大小写,并把 "userId" 和 userid 当成两个不同的列:
-- 创建一个真实名称为小写 "userid" 的列
CREATE TABLE t (userId integer);
-- 这两句都能用——名称已被折叠为小写
SELECT userId FROM t;
SELECT userid FROM t;
-- 这一句失败:"column \"userId\" does not exist"
-- 引号强制要求精确、区分大小写的匹配
SELECT "userId" FROM t;
注意不同数据库折叠大小写的方向不同:Oracle 把未加引号的标识符折叠成大写,另外几个折叠成小写,所以混合大小写的加引号标识符甚至不可移植。干净的出路是彻底避免加引号的混合大小写标识符,坚持用 snake_case,这绕开了整个问题,也让你的 schema 在每种方言里都保持可读。
要更深入地对比 camelCase、snake_case 和 kebab-case,包括在代码和数据两端各自何时才是正确选择,参见命名约定指南。
跨 SQL 方言的格式化
到目前为止的一切大体上与方言无关:大小写、缩进、换行和命名,无论你面向哪个数据库都适用。但「把这段 SQL 格式化」一旦你的查询用上某个数据库特有的语法,就会撞上一堵墙,因为不认识那语法的通用 parser 会把它弄乱:它可能在错误的地方切断一个 token、误读一个运算符,或者把一个引用字符当成字符串分隔符,吞掉半句查询。这正是方言感知(dialect-aware)格式化体现价值的地方,也是格式化器要你先选数据库、而不是去猜的原因。下面这些差异是你在日常查询里最常碰到的。
四种最常见的方言分歧操作,对照一览如下:
| 操作 | PostgreSQL | MySQL / MariaDB | SQL Server (T-SQL) | Oracle | Standard SQL |
|---|---|---|---|---|---|
| 字符串拼接 | || 或 CONCAT() | CONCAT() | + 或 CONCAT() | || 或 CONCAT() | || |
| NULL 回退 | COALESCE() | COALESCE() / IFNULL() | COALESCE() / ISNULL() | COALESCE() / NVL() | COALESCE() |
| 限制行数 | LIMIT | LIMIT | TOP / OFFSET … FETCH | FETCH FIRST | FETCH FIRST |
| 标识符引用 | 双引号("…") | 反引号 | 方括号([…]) | 双引号("…") | 双引号("…") |
下面各节会逐一展示它们在可运行上下文中的样子。
字符串拼接与 NULL 处理
两种最常见的日常操作,在各方言里拼写不同。
字符串拼接:
-- PostgreSQL, Oracle, SQLite (standard operator)
SELECT first_name || ' ' || last_name AS full_name FROM users;
-- SQL Server (T-SQL uses +)
SELECT first_name + ' ' + last_name AS full_name FROM users;
-- Portable across dialects
SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users;
NULL 回退:
-- Standard SQL (works everywhere)
SELECT COALESCE(nickname, name) AS display_name FROM users;
-- SQL Server only
SELECT ISNULL(nickname, name) AS display_name FROM users;
-- MySQL / MariaDB only
SELECT IFNULL(nickname, name) AS display_name FROM users;
一个设成错误方言的格式化器可能不理解 ISNULL 或 || 运算符,从而误解析周围的查询。
限行与标识符引用
限制结果行数是最因方言而异的语法之一:
-- PostgreSQL, MySQL, SQLite
SELECT id, name FROM users ORDER BY created_at DESC LIMIT 10;
-- SQL Server (T-SQL)
SELECT TOP 10 id, name FROM users ORDER BY created_at DESC;
-- Standard SQL / Oracle
SELECT id, name FROM users ORDER BY created_at DESC FETCH FIRST 10 ROWS ONLY;
标识符引用也分成三路。当你必须给一个标识符加引号时,通常是为了用保留字或保留大小写,此时分隔符取决于数据库:
-- MySQL / MariaDB use backticks
SELECT `order`, `user` FROM `select`;
-- SQL Server (T-SQL) uses square brackets
SELECT [order], [user] FROM [select];
-- Standard SQL (PostgreSQL, Oracle, SQLite) uses double quotes
SELECT "order", "user" FROM "select";
一个以为 MySQL 反引号是字符串分隔符、或把 T-SQL 方括号当成别的东西的格式化器,会产出坏掉的输出。方言设置就是告诉它哪个是哪个的东西。这也是为什么在数据库之间复制粘贴查询很少是一次干净的替换:同一个逻辑意图,比如拼接两个字符串、回退到 NULL、限制为十行、给保留字加引号,在各方言里写成四种不同的样子,只有认识你数据库的 parser 才能在不损坏它的前提下重新格式化。
为什么方言感知的格式化很重要
这正是为什么 SQL 格式化内置九种方言,包括 PostgreSQL、MySQL、SQL Server (T-SQL)、BigQuery、Snowflake、Oracle、SQLite、MariaDB 和 Standard SQL,而不是单一的通用模式。选对那一个,意味着 parser 能正确处理 PostgreSQL 的美元引用字符串(dollar-quoted strings)和 :: cast、T-SQL 的方括号标识符和 TOP、BigQuery 与 Snowflake 里仓库特有的函数,以及上面那些引用规则,而不是靠猜、然后猜错。格式化前先从下拉框里选好你实际用的数据库,输出回来就是正确而地道的。
自动化 SQL 格式化
读规则是一回事;没人应该手工去套用它们。风格指南的全部意义就在于由机器来强制执行。有三个地方可以接入格式化,取决于你想消除多少摩擦。
在编辑器里(保存时格式化)
最省力的选项是每次保存时自动格式化。VS Code 有能在保存时运行的 SQL 格式化扩展,JetBrains DataGrip 和其他 IDE 里的数据库工具自带一个格式化器,你可以把它绑到某个按键或保存钩子上。一旦配好,你的查询就根本不会处于未格式化状态,就像给 JavaScript 用 Prettier、给 Go 用 gofmt 一样。问题在于编辑器设置住在每个开发者自己的机器上,所以保存时格式化只让你的 SQL 保持整洁,单凭它本身并不保证团队其他人的也整洁。要做到那个,你需要下一层。
在 CI 里用 linter
要在整个团队范围强制风格,把检查搬进持续集成(CI)。像 sqlfluff 这样的 SQL linter 既能 lint 又能自动修复:你把方言、关键字大小写、缩进、逗号位置这些规则编码进一个 .sqlfluff 配置文件,跑 sqlfluff lint 标出违规、跑 sqlfluff fix 修复它们,并让 CI 把任何偏离约定风格的 pull request 判为失败。这和 ESLint 或 Prettier 把守前端仓库是同一个思路:风格不再是某人得记着留下的一句审查评论,而变成机器永不遗忘的一项通过或失败的检查。回报是风格争论只发生一次,在你写配置的时候,而不是在每个 pull request 里。
一次性在线格式化
有时你就只有一条丑陋的查询,又不想安装任何东西:日志里的一段、同事 Slack 消息里的一段、你正往文档里粘的一段查询。对此,把它粘进 SQL 格式化,选好方言、大小写和缩进,复制干净的结果。
这里隐私这一点很要紧,而且容易被忽略。许多在线格式化器会把你粘进去的文本发到服务器上去处理,这意味着你查询的一份副本,包括表名、列名、有时还有来自一次生产事故的字面值,离开了你的机器。SQL 格式化完全在你的浏览器里运行,所以你的 SQL 从不会被上传到任何地方。这让它可以安全地格式化触及生产 schema 或专有逻辑的查询,而那恰恰是你最想要干净格式化、又最不想把查询交给第三方的情形。如果你在同一个工作流里还要捣鼓别的格式,兄弟工具 JSON 格式化工作方式相同:一样的浏览器内处理,一样的一键复制。
这三种办法并不互斥,最好的搭配通常把它们结合起来:写代码时用保存时格式化跑快速内循环,用 CI linter 作为强制团队标准的后盾,再用在线格式化器处理那些永远不会进仓库的临时片段。无论你伸手拿哪一个,最后再记一次那条不变量:这些工具没有一个会改变你的查询做什么。它们重排空白、换行、大小写和注释,仅此而已。
常见问题
SQL 关键字该用大写还是小写?
两者都成立,因为 SQL 关键字不区分大小写。UPPERCASE 让关键字在没有语法高亮的环境中突出,比如日志和 diff;lowercase 更易打字,也契合本就给关键字上色的现代编辑器。要紧的是全队挑定一个、并由格式化器强制执行,混用才是最糟的选择。
SQL 最好的缩进是什么?
两个空格是最常见的默认值,能让 diff 保持紧凑;四个空格让深层嵌套的查询更易读;tab 让每个开发者自选显示宽度。没有唯一正确的答案,挑一个并在团队内一致应用即可。多数 SQL 格式化器,包括本工具,三种选项都支持。
我该如何自动格式化 SQL?
自动格式化 SQL 有三种方式:编辑器里的保存时格式化(VS Code 或 DataGrip)、CI 里像 sqlfluff 这样能自动修复风格的 linter,或一个用于一次性粘贴的在线 SQL 格式化器。在线这条路最快,因为无需安装,只要粘贴、选好方言、复制结果即可。
SQL 里我该用行首逗号还是行尾逗号?
行首逗号(每行开头的逗号)在增删列时给出更干净的 diff,也让漏掉的逗号容易被发现;行尾逗号(行末的逗号)读起来更自然、也更常见。两者在任何 SQL 风格指南里都可接受,关键是挑定一个,并让格式化器自动应用。
格式化 SQL 会改变查询的运行方式吗?
不会。格式化 SQL 只改变空白、换行、关键字大小写和注释,从不改变查询的逻辑。格式化后的查询返回与原查询完全相同的结果,所以哪怕是生产查询,在审查或运行前美化它也完全安全。
SQL 表和列该用什么命名约定?
snake_case,即全小写加下划线,是 SQL 表名和列名的事实标准,因为它避免加引号、在各方言间保持安全。让主键(id)和外键(user_id)命名保持一致,并避免用 order 或 user 这样的保留字做标识符,以免招来引号方面的麻烦。
我该如何为 PostgreSQL 或 T-SQL 这类特定方言格式化 SQL?
先在格式化器里选对方言。PostgreSQL 模式能正确处理 :: cast 和美元引用字符串;SQL Server (T-SQL) 模式理解方括号 [identifiers] 和 TOP。选错方言会让通用 parser 弄乱方言特有的语法,所以格式化前永远先把它设成你真实用的数据库。
有没有一份标准的 SQL 风格指南?
没有官方标准,但有几份被广泛引用的:Simon Holywell 的 sqlstyle.guide,以及 Mozilla、dbt 社区等团队的公开风格指南。它们共有的共识,包括一致的缩进、snake_case 标识符、每个主要子句前换行,正是本指南所归纳的,而格式化器可以替你强制执行。