简介
每位与数据库打交道的开发者都曾遇到过这样的情景:一堆未经格式化的 SQL 代码,关键字全是小写,没有换行,表名与 JOIN 条件紧密相连,全部挤在一行,向右无限延伸。这样的代码几乎无法调试,在 Pull Request 中无法审阅,也无法交接给同事。SQL 格式化不仅仅是外观上的优化——它是一种专业规范,能显著提升代码可读性、减少错误、统一团队风格,并大幅降低维护成本。
无论是编写一个简单的五行 SELECT 语句,还是包含 CTE 和窗口函数的百行分析管道,一致的格式化是让代码传递意图而非掩盖意图的关键所在。
SQL 简史
SQL 诞生于 20 世纪 70 年代初,起源于 IBM 位于圣何塞的研究实验室。Edgar F. Codd 的关系模型启发了 Donald D. Chamberlin 和 Raymond F. Boyce,他们开发了一种名为 SEQUEL(结构化英语查询语言)的语言,后来更名为 SQL。IBM 在 System R 中首次部署,Oracle 于 1979 年成为第一家销售商业 SQL 数据库的公司。
美国国家标准协会(ANSI)于 1986 年发布了第一个 SQL 标准,随后获得 ISO 认证。后续修订版 SQL-89、SQL-92、SQL:1999、SQL:2003、SQL:2008、SQL:2011、SQL:2016 逐步增加了触发器、存储过程、递归查询(CTE)、窗口函数和 JSON 支持等特性。
尽管有标准,各主要数据库厂商都引入了专有扩展,形成了各自的方言:MySQL、PostgreSQL、Microsoft SQL Server(T-SQL)、SQLite、Oracle PL/SQL 等。这些方言共享一个核心,但在字符串操作、日期函数、自增列等语法上存在差异。
SQL 语法基础
在格式化之前,了解基本构成要素很有帮助。标准 SELECT 语句遵循以下逻辑顺序:
SELECT -- 返回的列
FROM -- 数据源表
JOIN -- 关联表
WHERE -- 行级过滤
GROUP BY -- 聚合分组
HAVING -- 聚合级过滤
ORDER BY -- 排序顺序
LIMIT -- 行数限制
每个子句都有其明确的用途。WHERE 在聚合之前过滤;HAVING 在聚合之后过滤。GROUP BY 将多行折叠为每组一行。ORDER BY 最后执行(在 LIMIT 之前)。理解这个顺序有助于格式化查询,使每个子句都从新行开始,让逻辑流程一目了然。
格式化规范
没有单一的通用标准,但以下规范被广泛采用:
关键字大写
大多数风格指南建议将 SQL 关键字(SELECT、FROM、WHERE、JOIN、GROUP BY、HAVING、ORDER BY、LIMIT)写成大写。这与用户定义的标识符形成视觉对比。一些现代团队出于美观考虑偏好小写,dbt 等工具已使这种风格流行。核心原则:保持一致。
缩进
使用 4 个空格(或紧凑风格中的 2 个空格)缩进列列表、ON 子句和 WHERE 谓词。在共享代码库中应避免使用 Tab,因为它在不同编辑器中的渲染不一致。
换行
每个主要子句(SELECT、FROM、WHERE、GROUP BY 等)应从新行开始。SELECT 中的列列表应每列一行,在关键字下方缩进。这样便于添加、删除或注释单个列。
逗号位置 存在两个流派:尾随逗号(行末)和前置逗号(下一行开头)。尾随逗号对大多数程序员更自然;前置逗号便于一眼发现缺失的逗号。选择一种并坚持使用。
别名
使用别名时始终使用 AS 关键字(u AS user,而不是 u user)。这使意图更明确,避免复杂查询中的歧义。
格式化前后对比示例
-- 格式化前(未格式化)
select u.id,u.name,o.total from users u inner join orders o on u.id=o.user_id where o.total>100 and u.active=1 order by o.total desc limit 10
-- 格式化后
SELECT
u.id,
u.name,
o.total
FROM
users u
INNER JOIN orders o ON u.id = o.user_id
WHERE
o.total > 100
AND u.active = 1
ORDER BY
o.total DESC
LIMIT 10
格式化后的版本使表关系、过滤条件和排序顺序一目了然。
SQL 方言对比
虽然 SQL 已经标准化,但各方言在重要方面存在差异:
| 功能 | MySQL | PostgreSQL | SQL Server | SQLite |
|---|---|---|---|---|
| 字符串拼接 | CONCAT() |
|| 或 CONCAT() |
+ 或 CONCAT() |
|| |
| 自动递增 | AUTO_INCREMENT |
SERIAL / IDENTITY |
IDENTITY |
AUTOINCREMENT |
| 限制行数 | LIMIT n |
LIMIT n |
TOP n |
LIMIT n |
| 日期函数 | DATE(), NOW() |
CURRENT_DATE, NOW() |
GETDATE() |
DATE() |
| 大小写敏感 | 默认:不敏感 | 默认:敏感 | 默认:不敏感 | 可配置 |
在数据库之间迁移查询时,了解这些差异至关重要。能够识别目标方言的格式化工具即使对于特定方言的构造也能生成语法正确的输出。
SQL 风格指南
两个广泛引用的风格指南可帮助团队建立共识:
Simon Holywell 的 SQL 风格指南(sqlstyle.guide)
该指南强调根词对齐,使用空白"河流"在视觉上分隔子句,使用前置逗号,并对标识符使用 snake_case。它具有较强的主见性,但内容全面,在数据工程团队中广泛使用。
Google SQL 风格指南
Google 的内部风格指南(通过其 BigQuery 文档部分公开)倾向于使用尾随逗号、4 个空格缩进、大写关键字,并换行处理长 IN 列表。它与许多 Google 工程师熟悉的 Java 编码标准非常契合。
大多数团队会基于其中一个创建轻量级内部风格指南,并通过 CI 管道中的自动格式化来强制执行。
SQL 格式化工具的工作原理
SQL 格式化工具本质上是一个专门的编译器前端。其处理过程通常包含三个阶段:
1. 词法分析(Tokenization)
原始 SQL 字符串被分解为一个扁平的 token 流:关键字(SELECT、FROM)、标识符(users、u)、运算符(=、>)、字面量('completed'、100)、标点符号(,、;)以及空白符/注释。词法分析器不理解结构——它只对字符进行分类。
2. 语法分析(Parsing)
token 流被输入解析器,构建抽象语法树(AST)。AST 中的每个节点代表一个逻辑构造:SelectStatement 节点包含 ColumnList 节点、FromClause 节点、WhereClause 节点等。解析器执行语法规则并检测语法错误。
3. 重新生成(Pretty Printing) 格式化工具遍历 AST,根据格式化规则生成 SQL 文本:在子句关键字后添加换行,对子节点进行缩进,在运算符周围添加空格,转换关键字大小写。由于输出来自 AST 而非字符串操作,查询的语义意义被完美保留。
一些更简单的格式化工具跳过完整的 AST 构建,直接在 token 流上应用基于规则的转换。这种方式速度更快,但对于复杂的嵌套结构准确性较低。
常见 SQL 模式与示例
多表 JOIN
SELECT
c.name AS customer_name,
p.name AS product_name,
oi.quantity,
oi.unit_price
FROM
customers c
INNER JOIN orders o ON c.id = o.customer_id
INNER JOIN order_items oi ON o.id = oi.order_id
INNER JOIN products p ON oi.product_id = p.id
WHERE
o.status = 'shipped'
AND o.created_at >= '2024-01-01'
ORDER BY
c.name,
o.created_at DESC;
子查询
SELECT
department,
AVG(salary) AS avg_salary
FROM employees
WHERE salary > (
SELECT AVG(salary)
FROM employees
)
GROUP BY department
ORDER BY avg_salary DESC;
CTE(公共表表达式)
WITH monthly_revenue AS (
SELECT
DATE_TRUNC('month', created_at) AS month,
SUM(amount) AS revenue
FROM orders
WHERE status = 'completed'
GROUP BY 1
),
ranked_months AS (
SELECT
month,
revenue,
RANK() OVER (ORDER BY revenue DESC) AS rank
FROM monthly_revenue
)
SELECT *
FROM ranked_months
WHERE rank <= 3;
CTE 通过将复杂查询分解为命名的可重用构建块来提高可读性。它们在分析 SQL 中尤为有价值,其中中间聚合为进一步的计算提供基础。
窗口函数
SELECT
employee_id,
department,
salary,
AVG(salary) OVER (PARTITION BY department) AS dept_avg,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank
FROM employees;
窗口函数(带 OVER 的 ROW_NUMBER、RANK、LAG、LEAD、SUM、AVG)是 SQL 最强大的功能之一。适当的格式化——每个 OVER 子句与其函数在同一行,PARTITION BY 和 ORDER BY 清晰可见——对可读性至关重要。
带 HAVING 的聚合
SELECT
category,
COUNT(*) AS total_products,
AVG(price) AS avg_price,
MAX(price) AS max_price
FROM products
WHERE active = 1
GROUP BY category
HAVING COUNT(*) > 10
ORDER BY avg_price DESC;
与 IDE 和工具的集成
VS Code
sql-formatter npm 包为多个 VS Code 扩展提供支持。在 settings.json 中配置保存时格式化:
"[sql]": { "editor.defaultFormatter": "mtxr.sqltools" }
JetBrains DataGrip
DataGrip 在 代码 → 重新格式化代码(Ctrl+Alt+L)下内置了 SQL 格式化功能。可在 设置 → 编辑器 → 代码风格 → SQL 下按数据源方言配置样式选项。
DBeaver DBeaver 通过 SQL 编辑器 → 格式化 SQL 支持 SQL 格式化,并允许在 首选项 → 编辑器 → SQL 编辑器 → 格式化 中进行自定义格式化配置。
命令行
sql-formatter npm 包可用作 CLI 工具:
npx sql-formatter --language postgresql --indent 4 query.sql
最佳实践
- 尽早格式化,经常格式化。 在每次提交前运行格式化工具,而不仅仅是在代码审查前。
- 首先就风格指南达成一致。 没有任何格式化工具能替代团队对规范的共识。
- 一致使用别名。 简单查询用短别名(1-2 个字母)即可;复杂查询中描述性别名更佳。
- 描述性地命名 CTE。
monthly_revenue比cte1或tmp好得多。 - 注释复杂逻辑。 带有内联注释解释非显而易见的业务规则的格式良好的查询,远比聪明但沉默的 SQL 更易维护。
- 在 CI 中检查 SQL 格式。
sqlfluff等工具可在 Pull Request 流程中自动执行格式化规则。 - 保持 CASE WHEN 可读。 将长
CASE WHEN链分散到多行,每个分支一行。 - 限定所有列引用。 在多表查询中,始终用表别名前缀列名(
u.id而非id)。
常见问题解答
格式化会改变查询的行为吗? 不会。格式化工具只改变空白符、换行和关键字大小写。逻辑结构和执行计划保持不变。
格式化工具能修复语法错误吗?
不能。格式化工具需要语法有效(或基本有效)的 SQL 才能正确解析。请使用 sqlfluff 等 linter 检测和解释错误。
应该使用哪种关键字大小写风格? 大写关键字是 SQL 社区和风格指南中最传统、最广泛推荐的规范。不过,在 dbt 和现代数据工具中,小写越来越流行。为您的团队选择一种并自动执行。
它支持存储过程和 PL/SQL 块吗?
支持程度不一。大多数格式化工具能很好地处理 DML(SELECT、INSERT、UPDATE、DELETE)。过程扩展(PL/SQL、T-SQL 批处理、BEGIN...END 块)需要方言感知解析器,支持质量参差不齐。
使用在线格式化工具时,我的 SQL 数据安全吗? 好的在线格式化工具使用 JavaScript 在浏览器中完全在客户端处理所有内容。您的查询永远不会离开您的机器。在粘贴敏感 schema 信息之前,请务必在工具的隐私政策中验证这一点。
格式化工具和 linter 有什么区别?
格式化工具在不改变其含义的情况下转换 SQL 的呈现方式。linter(sqlfluff、SQLCheck)分析 SQL 中的风格违规、反模式和潜在错误,报告问题而不是自动修复。