标题里的「五年」有点保守,实际上从我第一次写 SELECT * FROM users 到现在大概七年了,只是前两年写的东西不太配叫经验,更像事故。下面十条,每条都对应过至少一次真实的疼痛。
一、别用 SELECT *
老生常谈,但理由可能和你想的不一样。除了浪费网络和内存之外,我遇到过更麻烦的:有人给表加了一个 remark 大字段,结果十几个用了 SELECT * 的接口响应体积翻倍,其中一个还因为 ORM 映射不到新字段直接报错。SELECT * 让表结构变更从「安全操作」变成了「危险操作」。
二、理解 NULL 的三值逻辑
这是我早年最痛的一课。WHERE status != 1 查不出 status 为 NULL 的行,因为 NULL != 1 的结果是 NULL 而不是 true。同理 NOT IN (1, 2, NULL) 永远返回空集。
我曾经因为这个漏掉了一批订单,对账少了 3000 多条。现在我的习惯是:能设 NOT NULL 就设 NOT NULL,用空字符串或者 0 或者 -1 作为默认值,比在每条 SQL 里处理 NULL 省心得多。
三、大表删除要分批
DELETE FROM logs WHERE created_at < '2025-01-01',一条语句删 800 万行。后果是:事务日志暴涨、主从延迟 40 分钟、锁等待超时把整个业务拖垮。我干过一次,印象深刻。
正确做法是循环分批,每批几千行,中间 sleep 几十毫秒给从库喘息机会:
DELETE FROM logs WHERE created_at < ? ORDER BY id LIMIT 3000;如果是要清空绝大部分数据,更好的办法是「建新表 + 导入要保留的数据 + rename」,秒级完成。
四、EXPLAIN 要看 rows,不是只看 type
很多人看到 type 是 ref 就放心了。但 ref 也可能扫 80 万行。真正反映代价的是 rows(预估扫描行数)和 filtered(过滤后剩余比例)。两者相乘才是最终参与后续处理的行数。另外 Extra 里的 Using filesort 和 Using temporary 是两个危险信号,看到就要警惕。
五、时间范围永远用半开区间
写 BETWEEN '2026-08-01' AND '2026-08-31' 会漏掉 8 月 31 日当天有时分秒的记录。写 <= '2026-08-31 23:59:59' 又会漏掉毫秒。统一用 >= 起 AND < 止,止是下个周期的开始。这条规则简单到不值一提,但我见过至少四个团队栽在上面。
六、OR 常常需要拆成 UNION
WHERE a = 1 OR b = 2,即使 a 和 b 各有索引,优化器也经常放弃走索引(index merge 不是每次都触发)。改成两个查询 UNION 起来,各自走各自的索引,往往快一个数量级。我们有条报表 SQL 就是这么从 9 秒优化到 400 毫秒的。
七、警惕隐式类型转换
字段是 varchar,你传了整数,MySQL 会把整列做类型转换再比较,索引直接失效。这个坑的可怕之处在于:SQL 语法正确、结果也正确,只是慢了一千倍,你根本不会怀疑它。
更隐蔽的是 JOIN 时两表字符集或排序规则不一致,关联字段的索引直接废掉。我排查过一次,花了整整一天。
八、COUNT 的那些说法大多是过时的
「count(1) 比 count(*) 快」这个说法在现代 MySQL 里已经不成立了,优化器会做同样的处理。真正有区别的是 count(字段)——它会跳过 NULL 值,语义完全不同。
另外,只需要判断「有没有」时,用 LIMIT 1 而不是 count。
九、子查询能改 JOIN 就改 JOIN
尤其是 WHERE id IN (SELECT ...) 这种。老版本 MySQL 会把它优化成相关子查询,外层每一行都执行一次内层,行为极其糟糕。改成 JOIN 或者先把子查询结果查出来再作为常量列表传入,通常能有数量级的提升。
反过来,只关心「存不存在」时 EXISTS 更好,它找到第一条就返回。
十、线上改表结构,先想清楚锁多久
不同的 DDL 操作代价天差地别。加一个可为空的列通常是瞬间完成的(instant algorithm),但改列类型、加索引、改字符集可能要重建整张表。千万行的表重建,可能锁写半小时。
我的流程是:任何线上 DDL,先在结构相同的测试库上跑一遍计时,再用在线变更工具执行,并且约定在低峰期。有一次我图省事直接跑了,锁了 18 分钟,那 18 分钟里所有下单都失败了。
一个额外的习惯
我现在写完任何一条要上线的 SQL,都会做三件事:EXPLAIN 一遍、在预发环境用真实数据量跑一遍计时、检查一遍 WHERE 条件是不是可能匹配到远超预期的行数。
前两件是技术活,第三件是心理活——一条语法完全正确的 SQL,可能因为少个 WHERE 就更新了整张表。我认识的人里至少三个干过这事。所以我在客户端配了强制的安全更新模式,没有 WHERE 的 UPDATE 和 DELETE 直接拒绝执行。这个设置从来没妨碍过我,但至少救过我两次。