正在加载,请稍候…

SQL 格式化:为何重要以及如何正确操作

学习 SQL 格式化最佳实践,提升可读性、性能和可维护性。通过实际示例和专家技巧,避免常见反模式。

显示器上的 SQL 代码

SQL 是数据的通用语言,但格式混乱的 SQL 可能成为阅读、调试和维护的噩梦。除了美观,格式化直接影响性能——像 SELECT *、隐藏的类型转换或深 LIMIT 偏移等草率的写法会悄悄拖垮你的数据库。本指南涵盖 SQL 格式化的 为什么如何做,重点关注可读性、性能和可维护性,并深入探讨常见的反模式。

在我们的 SQL 格式化工具 中试试,立即清理你的查询。

为什么格式化很重要

  • 可读性:一致的缩进和换行使复杂查询一目了然。未来的你(和你的队友)会感谢你。
  • 调试:结构良好的 SQL 更容易发现缺失的 JOIN、错误的过滤条件或括号位置不当。
  • 性能意识:格式化促使你思考数据库实际做了什么。例如,写 SELECT * 而不是显式列名,通常表明对 I/O 和索引缺乏考虑。
  • 代码审查:干净的 SQL 更容易审查。审查者可以专注于逻辑,而不是解读一堆文字。

关键格式化原则

1. 使用显式列列表

始终指定你需要的列。SELECT * 是一个臭名昭著的反模式:

-- 差:读取所有列,难以索引覆盖,对 schema 变更脆弱
SELECT * FROM orders WHERE user_id = 1001;

-- 好:只获取所需,可以利用覆盖索引
SELECT order_id, order_no, total_amount, created_at
FROM orders
WHERE user_id = 1001;

2. 大写 SQL 关键字

使用大写表示 SQL 关键字(SELECTFROMWHEREJOIN 等),小写表示表名/列名。这种约定提高了可扫描性:

SELECT o.order_id, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'PAID';

3. 垂直对齐子句

每个主要子句另起一行,并用缩进对齐子子句:

SELECT
    o.order_id,
    o.order_no,
    o.total_amount
FROM
    orders o
WHERE
    o.created_at >= '2026-01-01'
    AND o.status IN ('PAID', 'SHIPPED')
ORDER BY
    o.created_at DESC
LIMIT 20;

4. 清晰格式化 JOIN

JOIN 条件放在 ON 关键字同一行,并缩进 ON 子句:

SELECT
    u.id,
    u.name,
    o.order_id
FROM
    users u
    JOIN orders o ON o.user_id = u.id
WHERE
    u.created_at >= '2025-01-01';

影响性能的常见反模式

1. 在 WHERE 中对索引列使用函数

对列包裹函数(例如 DATE(created_at))通常会禁用索引使用:

-- 差:created_at 上的索引无法使用
SELECT * FROM orders WHERE DATE(created_at) = '2026-03-26';

-- 好:范围查询使用索引
SELECT order_id, created_at
FROM orders
WHERE created_at >= '2026-03-26 00:00:00'
  AND created_at < '2026-03-27 00:00:00';

经验法则:转换常量,而不是列。

2. 深度 OFFSET 分页

使用 LIMIT offset, size 且偏移量很大时,会迫使数据库扫描并丢弃大量行:

-- 差:扫描 100,020 行,丢弃 100,000 行
SELECT * FROM orders ORDER BY id LIMIT 100000, 20;

-- 好:键集分页使用索引直接跳过
SELECT order_id, id
FROM orders
WHERE id > 100000
ORDER BY id
LIMIT 20;

3. 带子查询的 NOT IN(NULL 陷阱)

如果子查询结果包含任何 NULLNOT IN 会返回零行:

-- 差:如果 orders.user_id 有 NULL,此查询返回空
SELECT * FROM customer WHERE id NOT IN (SELECT customer_id FROM orders);

-- 好:NOT EXISTS 是安全的
SELECT * FROM customer c
WHERE NOT EXISTS (
    SELECT 1 FROM orders o WHERE o.customer_id = c.id
);

4. 破坏索引使用的 OR 条件

OR 可能阻止优化器有效使用复合索引:

-- 差:可能无法最优使用索引
SELECT * FROM orders WHERE status = 'PAID' OR user_id = 1001;

-- 更好:拆分为 UNION ALL
SELECT * FROM orders WHERE status = 'PAID'
UNION ALL
SELECT * FROM orders WHERE user_id = 1001 AND status <> 'PAID';

5. 不带 WHERE 或 LIMIT 的 UPDATE/DELETE

在执行破坏性语句之前,始终确认受影响的行:

-- 危险:更新所有行
UPDATE users SET status = 'inactive';

-- 更安全:先预览
SELECT COUNT(*) FROM users WHERE last_login_at < '2025-01-01';
-- 然后执行,如果支持则加 LIMIT
UPDATE users SET status = 'inactive'
WHERE last_login_at < '2025-01-01'
LIMIT 1000;

实战示例:重构混乱查询

让我们拿一个写得糟糕的查询,逐步重构。

原始(可读性差,反模式):

SELECT * FROM operation WHERE type='SQLStats' AND name='SlowLog' ORDER BY create_time LIMIT 1000,10;

步骤 1:格式化以提高可读性

SELECT *
FROM operation
WHERE type = 'SQLStats'
  AND name = 'SlowLog'
ORDER BY create_time
LIMIT 1000, 10;

步骤 2:用显式列替换 SELECT *

SELECT id, type, name, create_time, status
FROM operation
WHERE type = 'SQLStats'
  AND name = 'SlowLog'
ORDER BY create_time
LIMIT 1000, 10;

步骤 3:用键集分页修复深度分页

假设我们在 (type, name, create_time) 上有一个复合索引。

SELECT id, type, name, create_time, status
FROM operation
WHERE type = 'SQLStats'
  AND name = 'SlowLog'
  AND create_time > '2017-03-16 14:00:00'  -- 上一页的最大 create_time
ORDER BY create_time
LIMIT 10;

步骤 4:用 EXPLAIN 验证

EXPLAIN
SELECT id, type, name, create_time, status
FROM operation
WHERE type = 'SQLStats'
  AND name = 'SlowLog'
  AND create_time > '2017-03-16 14:00:00'
ORDER BY create_time
LIMIT 10;

检查 type 是否为 rangerefkey 显示索引,且 rows 很小。

对比:IN vs EXISTS vs JOIN

模式 最佳使用场景 NULL 安全性 性能说明
IN (值列表) 小型静态集合(如状态) 安全(无子查询) 优化器直接使用索引
IN (子查询) 小型子查询结果 如果子查询返回 NULL 则不安全 可能被重写为半连接
EXISTS 存在性检查(如是否有订单) 安全 找到第一个匹配即停止;适合大子查询
NOT IN (子查询) 避免当子查询可能含 NULL 危险 – 如果存在 NULL 则返回空 通常较慢;优先用 NOT EXISTS
NOT EXISTS 不存在性检查(如无订单) 安全 通常最适合反连接
LEFT JOIN ... IS NULL 不存在性,需要两张表的列 安全 可能等价于 NOT EXISTS;检查执行计划

常见陷阱

  • 在生产代码中使用 SELECT * – 增加 I/O 并在 schema 变更时出错。
  • WHERE 中对索引列应用函数 – 禁用索引使用。
  • NOT IN (SELECT ...) 而不检查 NULL – 静默逻辑错误。
  • 使用深度 LIMIT offset 进行分页 – 性能随页数增加而下降。
  • 不加分析查询模式就添加索引 – 创建冗余索引,拖慢写入。
  • 凭猜测优化而不是阅读 EXPLAIN 计划。

FAQ

SQL 格式化真的影响性能吗?

不——格式化本身不会改变执行计划。但良好格式化带来的 习惯(显式列、不在列上使用函数、正确的分页)直接提升性能。格式化使这些习惯更容易被发现。

我应该总是用 NOT EXISTS 替代 NOT IN 吗?

对于子查询,是的——NOT EXISTS 避免了 NULL 陷阱,并且通常性能更好。对于静态值列表,NOT IN 没问题(例如 WHERE status NOT IN ('CANCELLED', 'REFUNDED'))。

如何在 SQL 中高效分页?

使用 键集分页(也称为 seek 方法):根据最后看到的行的唯一列进行过滤,而不是使用 OFFSET。例如:WHERE id > 1000 ORDER BY id LIMIT 20。这对于排序的唯一列效果很好。

格式化长 SQL 查询的最佳方式是什么?

使用一致的风格:大写关键字,每个子句另起一行,缩进子子句,并对齐 ON 条件。许多团队采用风格指南(例如 SQL 风格指南)。我们的 SQL 格式化工具 可以帮助自动化此过程。

如何检查我的查询是否使用了索引?

在查询前运行 EXPLAIN。查看 type 列(目标是 consteq_refrefrange——避免 ALL),key(应显示索引名称),以及 rows(相对于表大小应较小)。