必须明确写出 SELECT 字段而非用 *,JOIN 时尤需避免;ORDER BY 须在 WHERE 和 LIMIT 前且字段应有索引;WHERE 中 IN 适合小集合,EXISTS 适合外小内大场景;深分页应改用游标分页。

SELECT 字段列表别写 *,尤其在 JOIN 场景下
用 * 看似省事,实际会让数据库多做三件事:解析所有列、传输冗余数据、阻断索引覆盖优化。JOIN 时更危险——比如 SELECT * FROM users u JOIN orders o ON u.id = o.user_id,可能把 orders.description(大文本字段)也拖进来,拖慢整个查询。
实操建议:
- 明确写出需要的字段,如
SELECT u.id, u.name, o.created_at - 给表加别名后,所有字段必须带别名前缀,避免歧义和后续改字段时报错
- 如果字段名重复(如两个表都有
id),不加别名会直接报错Column 'id' in field list is ambiguous
ORDER BY 必须放在 WHERE 和 LIMIT 之前,且慎用函数包裹字段
ORDER BY 的执行顺序在 WHERE 之后、LIMIT 之前,这点直接影响性能。如果排序字段没索引,数据库就得对全量结果集排序;而如果写成 ORDER BY UPPER(name),哪怕 name 有索引,也无法命中。
常见错误现象:
- 查询变慢,
EXPLAIN显示Using filesort -
LIMIT 10返回结果不稳定(没ORDER BY时数据库不保证顺序) - 加了
ORDER BY created_at DESC却发现新数据总排不到最前——其实是created_at允许 NULL,NULL 值被排在最前(MySQL 默认行为)
实操建议:
- 确保
ORDER BY字段有单列索引或复合索引的最左前缀 - 避免在排序字段上套函数,如需大小写不敏感排序,建函数索引(MySQL 8.0+)或用生成列
- 显式处理 NULL:
ORDER BY created_at DESC NULLS LAST(PostgreSQL 支持),MySQL 可用ORDER BY created_at IS NULL, created_at DESC
WHERE 条件里用 IN 还是 EXISTS?看子查询结果集大小
字段过滤不只是写 WHERE,更是选对过滤逻辑。用 IN 还是 EXISTS 不是语法偏好问题,而是执行计划差异问题。MySQL 对 IN (subquery) 在子查询返回大量结果时可能放弃使用索引,转为临时表;而 EXISTS 总是走半连接(semi-join),适合外层小、内层大的场景。
使用场景对比:
- 查“有订单的用户”:
SELECT * FROM users WHERE EXISTS (SELECT 1 FROM orders WHERE orders.user_id = users.id)—— 推荐,users表小,orders表大 - 查“指定 ID 列表的用户”:
SELECT * FROM users WHERE id IN (101, 205, 333)—— 推荐,固定小集合,走索引快 - 查“订单数 > 5 的用户”:
SELECT user_id FROM orders GROUP BY user_id HAVING COUNT(*) > 5,再 JOIN,别硬套IN或EXISTS
注意:IN 遇到 NULL 会整体失效(1 IN (1, 2, NULL) 返回 NULL 而非 TRUE),生产环境务必检查字段是否允许 NULL。
复杂排序 + 分页时,OFFSET 深度越大越慢,用游标分页替代
当写 ORDER BY created_at DESC LIMIT 20 OFFSET 10000,数据库仍要扫描前 10020 行才取最后 20 条。这不是语法问题,是 B+ 树索引的物理限制——它不支持跳过中间节点直接定位。
实操建议:
- 前端传上一页最后一条的
created_at和id(组合防重复),下一页查:WHERE created_at - 游标值必须来自当前页实际返回字段,不能用服务端时间伪造,否则漏数据
- 复合排序字段必须全部出现在
WHERE条件中,且顺序、方向一致,否则索引失效
最容易被忽略的一点:游标分页无法跳转任意页,但这是有意为之的设计取舍——你要的是“下一页”,不是“第 100 页”。真要支持跳页,得另建物化视图或缓存偏移映射,别指望 SQL 一把梭。