SQL 从“能跑”到“可长期运行”的进化路径

索引并非万能,函数、隐式转换、OR 多非索引字段均致失效;应使用 EXPLAIN 检查执行计划,避免在索引列上运算,改用范围查询替代函数操作。

为什么加了索引查询还是慢

索引不是万能的,WHERE 条件里用了函数、类型隐式转换、或 OR 连接多个非索引字段,都可能导致索引失效。比如 WHERE YEAR(created_at) = 2025,MySQL 无法用 created_at 的索引;又比如 WHERE user_id = '123'user_idINT),触发隐式转换,也可能跳过索引。

实操建议:

  • EXPLAINtype 是否为 ref/rangekey 是否命中预期索引,rows 是否明显偏大
  • 避免在索引列上做计算或函数操作;改写为 WHERE created_at >= '2025-01-01' AND created_at
  • 字符串比较务必保证类型一致:user_idINT,就传整数,别传字符串
  • 多个 OR 条件尽量拆成 UNION 或改用 IN(前提是语义等价)

UPDATE/DELETE 没加 WHERE 或条件太宽会怎样

这类语句在开发环境可能“能跑”,但上线后可能锁表、拖垮主从延迟、甚至误删全量数据。尤其在 InnoDB 中,UPDATE 若未命中索引,会升级为表级间隙锁,阻塞其他写入。

实操建议:

  • 所有线上 DML 必须先用 SELECT COUNT(*) 验证条件范围,确认行数合理再执行
  • 强制要求 WHERE 中至少有一个高选择性索引字段,且该索引已存在
  • 在应用层封装写操作时,加默认限制(如 LIMIT 1000),并记录被截断的 warning
  • 运维侧配置 sql_safe_updates=1(MySQL),可阻止无 WHERE 或无键条件的 UPDATE/DELETE

长事务导致主从延迟和锁堆积

一个跑了 5 分钟的事务,不仅持有行锁/间隙锁不放,还会让 binlog 日志积压,从库重放滞后,监控里 Seconds_Behind_Master 突增,还可能触发 innodb_lock_wait_timeout 报错。

实操建议:

  • 业务逻辑中拆分大事务:比如批量更新 10 万条,改成每次 1000 条 + COMMIT,中间加毫秒级休眠防抖
  • information_schema.INNODB_TRX 定期抓取运行超 30 秒的事务,自动告警
  • 避免在事务里做 RPC 调用、文件读写、sleep 等外部耗时操作
  • 设置会话级超时:SET innodb_lock_wait_timeout = 10,比全局值更可控

没做归档,历史数据膨胀拖慢所有查询

订单表三年积累 2 亿行,即使加了索引,SELECT 扫描成本也远高于 200 万行的表。更麻烦的是,ANALYZE TABLE 变慢、备份时间翻倍、DDL 变得不敢动。

实操建议:

  • 按时间维度分区(如 PARTITION BY RANGE (TO_DAYS(created_at))),方便快速 DROP PARTITION
  • 冷数据迁移走后,用 CREATE TABLE ... SELECT 构建只读归档库,业务查历史数据走归档库
  • 关键表必须定义生命周期策略:比如订单完成 90 天后转入归档,180 天后仅保留摘要
  • 禁止直接 DELETE FROM huge_table WHERE ...;改用分批 DELETE + pt-archiver 工具控制节奏

真正难的不是写出能返回结果的 SQL,而是让这条 SQL 在数据量翻十倍、并发涨五倍、字段加三列之后,依然不报警、不锁表、不拖慢监控。这些细节藏在 EXPLAINkey_len 里,在 INFORMATION_SCHEMATRX_STATE 里,在凌晨三点的慢查日志滚动窗口里。