简介
SQL 是数据的通用语言。然而许多开发者将其视为“一次编写,一次运行”的脚本。在生产环境中,一条 SQL 语句可能存活数年,被多人阅读、修改和调试。格式混乱的 SQL 不仅碍眼,更是维护负担和性能隐患。
本文超越基本语法,探讨一致的格式化、命名规范和结构清晰度如何直接影响可读性和执行效率。我们将介绍常见反模式、如何避免它们,并通过一个真实世界的优化示例演示如何使用我们的 SQL 格式化工具。

为什么格式化影响性能
一个常见误解是格式化纯粹是装饰性的。虽然数据库引擎忽略空白和换行,但人类读者不会。格式良好的查询能一目了然地揭示其逻辑结构,从而更容易发现:
- 缺失的 JOIN 条件(导致笛卡尔积)
- 低效的过滤条件位置
- 不必要的子查询或冗余列
- 潜在的索引使用(或缺失)
此外,格式化纪律鼓励模块化思考。当你将复杂查询分解为 CTE(公用表表达式)或清晰分离 JOIN 和 WHERE 子句时,你会自然考虑每一步的数据流和基数。这通常能带来更好的执行计划。
关键格式化原则
1. 一致的关键字大小写
选择一种风格——SQL 关键字用大写,标识符用小写——并在整个代码库中保持一致。混用大小写(例如 SELECT ... where)会增加认知负担。
-- 好
SELECT u.id, u.name, COUNT(o.id) AS order_count
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE u.status = 'ACTIVE'
GROUP BY u.id, u.name;
-- 差
select u.id, u.name, count(o.id) as order_count
from users u
inner join orders o on u.id = o.user_id
where u.status = 'ACTIVE'
group by u.id, u.name;
2. 每列单独一行
水平排列列名使得难以看清选择了什么,尤其当列很多或表达式复杂时。垂直排列能提高版本控制中的差异可读性。
-- 好
SELECT
u.id,
u.name,
u.email,
COUNT(o.id) AS order_count,
SUM(o.amount) AS total_spent
FROM ...
-- 差
SELECT u.id, u.name, u.email, COUNT(o.id) AS order_count, SUM(o.amount) AS total_spent FROM ...
3. 对齐 JOIN 和 ON 子句
每个 JOIN 应另起一行,其 ON 条件缩进。这样使连接链视觉上清晰。
SELECT *
FROM users u
INNER JOIN orders o
ON u.id = o.user_id
LEFT JOIN payments p
ON o.id = p.order_id AND p.status = 'COMPLETED'
4. 使用有意义的别名
像 a、b、c 这样的短别名毫无意义。使用能反映表用途的缩写(例如 u 表示 users,ord 表示 orders)。对于子查询或 CTE,给它们描述性名称。
-- 好
SELECT u.name, ord.total
FROM users u
INNER JOIN (
SELECT user_id, SUM(amount) AS total
FROM orders
GROUP BY user_id
) ord ON u.id = ord.user_id;
-- 差
SELECT a.name, b.total
FROM users a
INNER JOIN (
SELECT user_id, SUM(amount) AS total
FROM orders
GROUP BY user_id
) b ON a.id = b.user_id;
5. 对复杂逻辑使用 CTE
不要使用深度嵌套的子查询,而是使用 CTE 逐步分解问题。CTE 提高可读性,并且通常允许优化器物化中间结果。
WITH user_orders AS (
SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id
),
active_users AS (
SELECT id, name
FROM users
WHERE status = 'ACTIVE'
)
SELECT au.name, uo.order_count
FROM active_users au
LEFT JOIN user_orders uo ON au.id = uo.user_id;
常见陷阱(反模式)
- **生产代码中使用 SELECT ***:返回不必要的列,增加 I/O,并且如果表结构变更会出错。始终列出你需要的列。
- 隐式类型转换:当
user_id是 VARCHAR 类型时写WHERE user_id = 1001会强制全表扫描。应使用引号:WHERE user_id = '1001'。 - 对索引列使用函数:
WHERE TO_CHAR(create_time, 'YYYY-MM-DD') = '2025-06-15'阻止索引使用。应使用范围条件:WHERE create_time >= DATE '2025-06-15' AND create_time < DATE '2025-06-16'。 - 缺失 JOIN 条件:没有 ON 子句的 INNER JOIN 会产生笛卡尔积,在大表上可能是灾难性的。
- 过度嵌套:嵌套超过 2-3 层的子查询变得难以阅读。重构为 CTE 或临时表。
- 不一致的格式化:在同一查询或项目中混用风格会增加认知负担,使代码审查更困难。
完整示例:从混乱到优化
让我们从一个真实世界的混乱查询开始,该查询检索月活跃用户及其订单统计:
select a.id,a.name,count(b.id),sum(b.amount) from users a,orders b where a.id=b.user_id and a.status='ACTIVE' and b.created_at>='2025-01-01' group by a.id,a.name order by sum(b.amount) desc;
这个查询有多个问题:
- 没有格式化
- 隐式连接(逗号分隔)而不是显式 JOIN
- 聚合列没有别名
- 相当于
SELECT *(从a中选择所有列)
步骤 1:使用显式 JOIN 格式化
使用我们的 SQL 格式化工具,我们可以快速重写它:
SELECT
u.id,
u.name,
COUNT(o.id) AS order_count,
SUM(o.amount) AS total_spent
FROM users u
INNER JOIN orders o
ON u.id = o.user_id
WHERE
u.status = 'ACTIVE'
AND o.created_at >= '2025-01-01'
GROUP BY
u.id,
u.name
ORDER BY total_spent DESC;
步骤 2:分析性能
即使格式化了,如果 users 和 orders 表很大,这个查询可能仍然很慢。让我们检查执行计划(假设):
| id | select_type | table | type | possible_keys | key | rows | Extra |
|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | u | ALL | PRIMARY | NULL | 100K | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | o | ALL | idx_user_id | NULL | 1M | Using where; Using join buffer (Block Nested Loop) |
两张表都被全表扫描——明显缺少索引。
步骤 3:添加合适的索引
根据查询模式,我们需要:
CREATE INDEX idx_users_status ON users(status);
CREATE INDEX idx_orders_user_created ON orders(user_id, created_at);
现在计划变了:
| id | select_type | table | type | key | rows | Extra |
|---|---|---|---|---|---|---|
| 1 | SIMPLE | u | ref | idx_users_status | 10K | Using where; Using index |
| 1 | SIMPLE | o | ref | idx_orders_user_created | 50 | Using where; Using index |
性能显著提升:从扫描 110 万行减少到约 10K + 每个用户 50 行。
步骤 4:使用 CTE 进一步提高清晰度
如果我们需要重用这个逻辑,CTE 使其自文档化:
WITH active_user_orders AS (
SELECT
u.id,
u.name,
COUNT(o.id) AS order_count,
SUM(o.amount) AS total_spent
FROM users u
INNER JOIN orders o
ON u.id = o.user_id
WHERE
u.status = 'ACTIVE'
AND o.created_at >= '2025-01-01'
GROUP BY
u.id,
u.name
)
SELECT *
FROM active_user_orders
ORDER BY total_spent DESC
LIMIT 20;
格式化风格对比
| 风格 | 可读性 | 差异友好性 | 可维护性 |
|---|---|---|---|
| 全小写,单行 | 低 | 低 | 低 |
| 关键字大写,每列一行 | 高 | 高 | 高 |
| 大小写混用,不一致 | 低 | 低 | 低 |
| 使用 CTE 和注释 | 非常高 | 高 | 非常高 |
常见问题
为什么应该使用显式 JOIN 语法而不是逗号分隔的表?
显式 JOIN 使连接条件清晰,并防止意外的笛卡尔积。它们还将连接逻辑与过滤逻辑分离,提高可读性,并使更改连接类型(例如从 INNER 改为 LEFT)更容易。
格式化会影响查询性能吗?
不会,数据库忽略空白和格式化。然而,格式化通过揭示逻辑结构帮助人类编写更好的查询,这间接通过合适的索引和连接策略带来更好的性能。
如何在子查询和 CTE 之间选择?
当子查询被多次引用或你想将复杂查询分解为逻辑步骤时,使用 CTE。CTE 也可以被物化(在某些数据库中)以避免重复执行。对于简单的、一次性使用的子查询,子查询就足够了。
格式化长 WHERE 子句的最佳方式是什么?
将每个条件放在单独的行上,在 WHERE 下缩进。使用括号对相关条件分组。例如:
WHERE
(status = 'ACTIVE' OR status = 'PENDING')
AND created_at >= '2025-01-01'
AND user_id IN (SELECT id FROM vip_users)
逗号应该放在行首还是行尾?
两种方式都有使用。关键是团队内部保持一致。行首逗号更容易注释掉一行,而行尾逗号更传统。选择一种并强制执行。