半夜三点,手机震了。运维群的告警声比闹钟还刺耳,我迷迷糊糊摸到手机,一看,核心业务数据库的慢查询日志已经堆成山了。那条SQL跑一次要35秒,用户端早就开始卡顿,客服电话估计也快被打爆了。这种时候,你没法慢慢查文档,得立刻上手。我泡了杯浓茶,打开慢查询日志,看到那条SQL带了三个LEFT JOIN,嵌套子查询,还用了个LIKE '%keyword%'。第一反应就是:这玩意儿不光慢,而且是在找死。

我先把那条SQL捞出来,放到测试库跑了一遍。Explain一看,全表扫描,扫描行数直接奔着三百万行去了。索引呢?完全没有命中。这其实是个很典型的坑:业务方写SQL的时候,习惯把查询条件堆在一起,觉得能查到结果就行,根本不管数据库怎么干活。但数据库不是人,它不会智能地选最优路径,你给它什么,它就死心眼地全表扫。三百万行数据,每个JOIN都得扫一遍,不慢才怪。
拆开来看,问题出在一个订单查询页面上。用户想搜历史订单,条件里有订单号、客户名、下单时间,还有个模糊的商品名称。业务方图省事,直接把所有条件塞到一个SQL里,还用了LIKE '%xx%'。这种写法,MySQL根本没法用B+树索引,因为前缀模糊匹配意味着索引失效。你建再多的索引,它也只会老老实实全表扫描。这不是MySQL的锅,是写SQL的人没理解索引的工作原理。
我当时做了两步操作。第一步,把模糊查询拆出来。商品名称那种LIKE '%xx%',改成用全文索引或者Elasticsearch去处理,不让它拖累主库。第二步,把三个LEFT JOIN简化成两个INNER JOIN,因为业务逻辑里其实不需要保留NULL值的左表记录,用INNER JOIN就够,还能减少扫描行数。改完之后,Explain显示的行数从三百万降到两万,索引全部命中。
你以为这就完了?没那么简单。优化完SQL,测试环境跑得快,一上生产又卡了。我盯了下监控,发现并发量一上来,这条SQL虽然本身快了,但锁等待严重。因为订单表是高频写入的表,查询和写操作争抢行锁,导致查询被堵住。这就跟开车一样,路修好了,但路口没设红绿灯,车一多照样堵死。我只好把查询改成只读副本去跑,主库只负责写,彻底隔离读写压力。
改了副本之后,问题又冒出来。只读副本的数据延迟了大概两秒,用户查最新订单时,偶尔会漏掉刚下的单。业务方反馈说,财务对账时发现少了几笔记录,差点以为是丢数据。我赶紧把副本的同步模式从异步改成半同步,延迟降到毫秒级,这才稳住。这种细节,光靠调SQL是想不到的,得对整个架构有感觉。
最终那条SQL的执行时间从35秒降到了0.3秒,整整一百多倍。但说实话,最高兴的不是这个数字,而是后来业务方再也没半夜打电话。他们把慢查询监控接上了告警,每次新SQL上线前都会让我review一遍。我给他们定了个规矩:所有带LIKE的查询必须走搜索引擎,所有多表JOIN必须提前Explain,所有查询必须指定返回字段,不准用SELECT *。这些规矩看着简单,但执行起来,能把数据库的命续上很久。
回看这次优化,核心就一句话:别让数据库干它不擅长的事。模糊搜索、复杂关联、高频读写混在一起,这本身就是反模式。很多人觉得数据库优化是调参数、加内存、换SSD,其实最值钱的是改SQL写法。你把一条烂SQL改成好SQL,省下的钱够买好几台服务器。而且,这种优化不是一次性的,你得让团队养成习惯,每次写SQL前都想想:这条SQL会让数据库怎么干活?它会不会全表扫描?它会不会锁住其他事务?
我后来把那套优化思路写成了文档,发到团队群里。有人问,这么做是不是太折腾了?我说,你半夜被叫起来折腾一次,就明白值不值了。数据库运维这行,表面上是跟代码和服务器打交道,其实是跟人的习惯和懒惰较劲。你改一条SQL不难,难的是让所有人都不再写那种烂SQL。那次优化之后,我就再也没收到过那条慢查询的告警。偶尔翻到日志,看到那条SQL的执行计划里全是索引,心里还挺舒坦的。


