混乱的 SQL 难以调试、审查痛苦、维护更是噩梦。无论你是独立开发还是团队协作,一致的 SQL 格式化都能将难以阅读的文本墙转变为清晰、有逻辑的语句。本指南涵盖 SQL 风格的方方面面——从关键字大小写和缩进到 CTE 格式化和命名约定——附有即学即用的前后对比示例。
1. 为什么 SQL 格式化很重要
SQL 常被当作"写一次"的语言对待,但生产环境的查询会被阅读几十次。良好的格式化在三个关键领域带来回报:
- 可读性——格式良好的 SQL 让你一眼就能扫描查询结构。你可以立即看到连接了哪些表、应用了什么过滤条件以及选择了哪些列。
- 代码审查——审查者可以专注于逻辑而不是解读结构。当每个子句占据独立的行时,diff 视图才有意义。
- 调试——当查询返回错误结果时,格式化的 SQL 让你可以隔离每个子句并独立测试。找到放错位置的 AND 或错误的 JOIN 条件只需几秒而不是几分钟。
- 可维护性——六个月后,你(或你的队友)需要修改那个查询。格式化的 SQL 使修改安全且可预测。
2. 关键字:大写 vs 小写
SQL 关键字最常见的两种约定:
风格 A:大写关键字(最常见)
大写关键字在视觉上将 SQL 结构与数据标识符分开。这是最广泛采用的约定,推荐团队使用。
SELECT
user_id,
first_name,
last_name,
email
FROM users
WHERE is_active = TRUE
ORDER BY created_at DESC
LIMIT 100;风格 B:小写关键字
一些开发者偏好小写以获得更现代的感觉。这在现代编辑器的语法高亮下效果很好。
select
user_id,
first_name,
last_name,
email
from users
where is_active = true
order by created_at desc
limit 100;结论:选择一种并在团队中强制执行。大写是行业默认标准,在纯文本环境(日志、Slack、邮件)中没有语法高亮时效果最佳。
3. 缩进:Tab vs 空格,一行一个子句
黄金法则:每行一个主要子句。这使 diff 保持干净,子句易于注释。
- 使用一致的缩进(2 或 4 个空格;避免使用 Tab 以确保可移植性)。
- 每个主要关键字(SELECT、FROM、WHERE、GROUP BY、HAVING、ORDER BY、LIMIT)在同一缩进级别的新行开始。
- 子子句(JOIN 条件、AND/OR 过滤器)缩进一级。
- 当能提高可读性时,垂直对齐相关元素。
-- Each major clause on its own line, sub-clauses indented
SELECT
u.user_id,
u.email,
COUNT(o.order_id) AS order_count
FROM users AS u
LEFT JOIN orders AS o
ON u.user_id = o.user_id
WHERE u.is_active = TRUE
AND u.created_at >= '2024-01-01'
GROUP BY
u.user_id,
u.email
HAVING COUNT(o.order_id) > 5
ORDER BY order_count DESC
LIMIT 20;4. SELECT:一行一列,尾部逗号 vs 前导逗号
每行列出一列使添加、删除或重新排序列变得轻而易举。逗号位置的争论分为两个阵营:
尾部逗号(更常见)
SELECT
user_id,
first_name,
last_name,
email,
created_at
FROM users;前导逗号
SELECT
user_id
, first_name
, last_name
, email
, created_at
FROM users;前导逗号更容易发现缺失的逗号,也更容易注释掉最后一列。但尾部逗号更自然,也是更常见的选择。
提示:无论选择哪种,在生产查询中始终将每列放在独立的行上。不要在一行中挤入多列。
5. JOIN:格式化多表查询
JOIN 是格式化回报最大的地方。格式良好的 JOIN 块清楚地展示了表之间的关系。
- 每个 JOIN 在与 FROM 相同级别的新行开始。
- ON 子句缩进在其 JOIN 下方。
- 多条件 ON 子句在单独缩进的行上使用 AND。
- 始终使用显式 JOIN 语法(永远不要在 FROM 中使用逗号分隔的表)。
SELECT
o.order_id,
o.order_date,
c.customer_name,
p.product_name,
oi.quantity,
oi.unit_price
FROM orders AS o
INNER JOIN customers AS c
ON o.customer_id = c.customer_id
INNER JOIN order_items AS oi
ON o.order_id = oi.order_id
INNER JOIN products AS p
ON oi.product_id = p.product_id
AND p.is_active = TRUE
WHERE o.order_date >= '2024-01-01'
ORDER BY o.order_date DESC;6. WHERE:AND/OR 对齐,子查询缩进
WHERE 子句可能很快变得复杂。适当的格式化保持逻辑树可见。
- 每个 AND/OR 在新行开始,与第一个条件对齐或缩进在 WHERE 下方。
- 对混合的 AND/OR 逻辑显式使用括号。
- 将子查询缩进为嵌套块。
- 将运算符(AND/OR)放在行首,而不是行尾。
-- Simple WHERE with AND
WHERE u.is_active = TRUE
AND u.email_verified = TRUE
AND u.created_at >= '2024-01-01'
-- Mixed AND/OR with explicit parentheses
WHERE u.is_active = TRUE
AND (
u.role = 'admin'
OR u.role = 'moderator'
)
AND u.last_login >= '2024-06-01'
-- Subquery in WHERE
WHERE u.user_id IN (
SELECT o.user_id
FROM orders AS o
WHERE o.total_amount > 1000
AND o.order_date >= '2024-01-01'
GROUP BY o.user_id
HAVING COUNT(*) >= 3
)7. CTE(WITH):格式化公共表表达式
CTE 是提高查询可读性的最佳工具之一。良好的格式化会放大其好处。
- WITH 在顶层开始。
- 每个 CTE 名称和 AS 在同一行。
- CTE 主体在括号内缩进。
- 用空行分隔多个 CTE 以便视觉分组。
- 最终的 SELECT 与 WITH 在同一级别开始。
WITH monthly_revenue AS (
SELECT
DATE_TRUNC('month', order_date) AS month,
SUM(total_amount) AS revenue
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY DATE_TRUNC('month', order_date)
),
revenue_growth AS (
SELECT
month,
revenue,
LAG(revenue) OVER (ORDER BY month) AS prev_revenue,
ROUND(
(revenue - LAG(revenue) OVER (ORDER BY month))
/ LAG(revenue) OVER (ORDER BY month) * 100,
2
) AS growth_pct
FROM monthly_revenue
)
SELECT
month,
revenue,
prev_revenue,
growth_pct
FROM revenue_growth
ORDER BY month;8. 命名约定
一致的命名消除猜测并减少错误。
- 表——使用 snake_case,复数名词:users、order_items、product_categories。
- 列——使用 snake_case,描述性名称:created_at、is_active、total_amount。
- 别名——使用有意义的短别名,而不是单个字母:ord 而不是 o,usr 而不是 u。例外:单字母在简单的两表 JOIN 中显而易见时。
- 避免前缀——不要用表缩写前缀列名(tbl_users、col_name)。表上下文在查询中已经很清楚。
- 布尔值——前缀使用 is_、has_ 或 can_:is_active、has_subscription、can_edit。
- 主键——主键使用 id。外键应为 table_singular_id:user_id、order_id。
-- Good naming example
SELECT
usr.user_id,
usr.first_name,
usr.is_active,
ord.order_id,
ord.total_amount,
ord.created_at AS order_date
FROM users AS usr
INNER JOIN orders AS ord
ON usr.user_id = ord.user_id
WHERE usr.is_active = TRUE
AND usr.has_subscription = TRUE;9. 差 vs 好:5 个前后对比
示例 1:简单 SELECT
差:
select id,name,email,created_at from users where is_active=1 and role='admin' order by created_at desc limit 50;好:
SELECT
id,
name,
email,
created_at
FROM users
WHERE is_active = 1
AND role = 'admin'
ORDER BY created_at DESC
LIMIT 50;示例 2:多表 JOIN
差:
select o.id,c.name,p.title,oi.qty,oi.price from orders o join customers c on o.customer_id=c.id join order_items oi on o.id=oi.order_id join products p on oi.product_id=p.id where o.status='completed' and o.created_at>='2024-01-01';好:
SELECT
o.id AS order_id,
c.name AS customer_name,
p.title AS product_title,
oi.qty,
oi.price
FROM orders AS o
INNER JOIN customers AS c
ON o.customer_id = c.id
INNER JOIN order_items AS oi
ON o.id = oi.order_id
INNER JOIN products AS p
ON oi.product_id = p.id
WHERE o.status = 'completed'
AND o.created_at >= '2024-01-01';示例 3:复杂 WHERE
差:
select * from users where (role='admin' or role='moderator') and is_active=1 and (last_login>='2024-01-01' or created_at>='2024-06-01') and email like '%@company.com' order by last_login desc;好:
SELECT
user_id,
email,
role,
last_login,
created_at
FROM users
WHERE (
role = 'admin'
OR role = 'moderator'
)
AND is_active = 1
AND (
last_login >= '2024-01-01'
OR created_at >= '2024-06-01'
)
AND email LIKE '%@company.com'
ORDER BY last_login DESC;示例 4:子查询
差:
select u.name,u.email from users u where u.id in (select o.user_id from orders o where o.total>500 group by o.user_id having count(*)>3) and u.is_active=1;好:
SELECT
u.name,
u.email
FROM users AS u
WHERE u.id IN (
SELECT o.user_id
FROM orders AS o
WHERE o.total > 500
GROUP BY o.user_id
HAVING COUNT(*) > 3
)
AND u.is_active = 1;示例 5:带聚合的 CTE
差:
with user_totals as (select user_id,sum(amount) as total from payments where status='completed' group by user_id), user_ranks as (select user_id,total,rank() over (order by total desc) as rnk from user_totals) select u.name,ur.total,ur.rnk from user_ranks ur join users u on ur.user_id=u.id where ur.rnk<=10;好:
WITH user_totals AS (
SELECT
user_id,
SUM(amount) AS total
FROM payments
WHERE status = 'completed'
GROUP BY user_id
),
user_ranks AS (
SELECT
user_id,
total,
RANK() OVER (ORDER BY total DESC) AS rnk
FROM user_totals
)
SELECT
u.name,
ur.total,
ur.rnk
FROM user_ranks AS ur
INNER JOIN users AS u
ON ur.user_id = u.id
WHERE ur.rnk <= 10;10. SQL 代码检查器与格式化工具
自动化格式化以消除风格争论并确保一致性。
| 工具 | 类型 | 最适合 |
|---|---|---|
| sqlfluff | Python CLI | CI/CD 管道、团队标准化 |
| pgFormatter | Perl CLI / Web | PostgreSQL 项目 |
| sql-formatter | npm package | Node.js / Web 应用 |
基于 Python 的代码检查/格式化工具。最全面的选项,有 60+ 可配置规则。支持多种 SQL 方言(PostgreSQL、MySQL、BigQuery、Snowflake)。
sqlfluff 用法:
# Install
pip install sqlfluff
# Lint a file
sqlfluff lint query.sql --dialect postgres
# Auto-fix formatting
sqlfluff fix query.sql --dialect postgres
# Configuration (.sqlfluff in project root)
[sqlfluff]
dialect = postgres
templater = raw
max_line_length = 120
[sqlfluff:rules:capitalisation.keywords]
capitalisation_policy = upper
[sqlfluff:rules:capitalisation.identifiers]
capitalisation_policy = lower
[sqlfluff:indentation]
indent_unit = space
tab_space_size = 4基于 Perl 的格式化器,专注于 PostgreSQL。快速、简单,输出整洁。可作为 CLI 工具和 Web 界面使用。
pgFormatter 用法:
# Install (macOS)
brew install pgformatter
# Format a file
pg_format -u 1 -s 4 query.sql -o formatted.sql
# Options:
# -u 1 : uppercase keywords
# -s 4 : 4-space indent
# -f 1 : function-style formatting
# -W 5 : wrap after 5 columns in SELECTJavaScript/npm 包。易于集成到 Node.js 项目、CI 管道和 Web 应用中。支持多种方言。
sql-formatter (npm) 用法:
// Install
npm install sql-formatter
// Usage in JavaScript/TypeScript
import { format } from 'sql-formatter';
const formatted = format(
'select id,name from users where active=1',
{
language: 'postgresql',
keywordCase: 'upper',
tabWidth: 4,
linesBetweenQueries: 2,
}
);
console.log(formatted);
// SELECT
// id,
// name
// FROM
// users
// WHERE
// active = 111. IDE 设置
配置编辑器以自动格式化 SQL。
VS Code
安装 "SQL Formatter" 扩展(adpyke.vscode-sql-formatter)。添加到 settings.json:
// .vscode/settings.json
{
"sql-formatter.uppercase": true,
"sql-formatter.indent": " ",
"sql-formatter.linesBetweenQueries": 2,
"[sql]": {
"editor.defaultFormatter": "adpyke.vscode-sql-formatter",
"editor.formatOnSave": true,
"editor.tabSize": 4,
"editor.insertSpaces": true
}
}DataGrip / JetBrains
DataGrip 有内置的 SQL 格式化。进入 Settings > Editor > Code Style > SQL 进行配置。关键设置:
Settings > Editor > Code Style > SQL:
- Word Case: Keywords = UPPER
- Word Case: Identifiers = lower
- Tabs and Indents:
- Use tab character: OFF
- Tab size: 4
- Indent size: 4
- Wrapping:
- Wrap long lines: ON
- Place commas: After item (trailing)
- Clauses:
- Place SELECT on new line: ON
- Place FROM on new line: ON
- Place ON in JOIN on new line: ONDBeaver
DBeaver 使用自己的格式化器。进入 Window > Preferences > SQL Editor > SQL Formatting。启用 "Insert spaces for tabs" 并将关键字大小写设为 UPPER。
Window > Preferences > SQL Editor > SQL Formatting:
- Keyword case: UPPER
- Indent type: Spaces
- Indent size: 4
- SELECT: Column list on new line
- JOIN: ON clause on new line
Shortcut: Ctrl+Shift+F (format selected SQL)12. 团队风格指南模板
复制并调整此模板以适合你的团队。将其作为 SQL_STYLE_GUIDE.md 存储在你的代码仓库中。
# SQL Style Guide
## Keywords
- UPPERCASE for all SQL keywords (SELECT, FROM, WHERE, JOIN, etc.)
## Indentation
- 4 spaces (no tabs)
- One clause per line (SELECT, FROM, WHERE, GROUP BY, ORDER BY)
- Sub-clauses indented one level (JOIN ON, AND, OR)
## SELECT
- One column per line
- Trailing commas
- Always alias computed columns
## FROM / JOIN
- Explicit JOIN syntax (never comma joins)
- Always use AS for table aliases
- ON clause indented under its JOIN
- Multi-condition ON: each AND on its own line
## WHERE
- AND/OR at the beginning of lines (not the end)
- Explicit parentheses for mixed AND/OR
- Subqueries indented as nested blocks
## CTEs
- Preferred over subqueries for readability
- Blank line between CTEs
- Final SELECT at the same level as WITH
## Naming
- Tables: snake_case, plural (users, order_items)
- Columns: snake_case, descriptive (created_at, is_active)
- Aliases: meaningful abbreviations (ord, usr, prod)
- Booleans: is_, has_, can_ prefix
- Primary keys: id
- Foreign keys: {table_singular}_id
## Enforcement
- Pre-commit hook: sqlfluff lint
- CI check: sqlfluff lint --dialect postgres
- Config: .sqlfluff in repo root常见问题
SQL 格式化会影响查询性能吗?
不会。SQL 格式化纯粹是视觉上的。无论空格、换行或关键字大小写如何,数据库引擎都以相同方式解析和优化查询。为人类格式化,而不是为机器。
应该使用尾部逗号还是前导逗号?
两者都有效。尾部逗号(column1, column2,)更自然,更广泛采用。前导逗号(,column1 ,column2)更容易注释列。选择一种并保持一致。
如何在团队中强制执行 SQL 格式化?
在 CI/CD 管道中使用 sqlfluff 等自动化工具。添加运行格式化器的 pre-commit 钩子。记录你的风格指南并在仓库 README 中链接。自动化执行远比代码审查评论有效。
大写还是小写关键字——哪个更专业?
大写关键字是传统的行业标准,在没有语法高亮的环境(纯文本、邮件、文档)中效果最佳。小写在有良好语法高亮的现代开发环境中越来越流行。两者都不"更专业"——一致性比具体选择更重要。
如何格式化动态 SQL 或 ORM 生成的查询?
对于动态 SQL,像格式化普通查询一样格式化模板字符串。对于 ORM 生成的查询,大多数 ORM 有 .toSQL() 或 .toString() 方法——将输出通过格式化器处理以便调试。在日志中,使用 sqlfluff fix --stdin 等格式化器自动格式化捕获的查询。