你刷新页面,进度条转了八秒——咖啡凉了,老板路过时多看了你一眼,运营在群里发了个“系统卡吗?”的问号。

别硬扛。慢查询不是玄学,是能摸到、能改、今天就能见效的事。

为什么你的查询越来越慢?定位瓶颈比埋头改SQL重要十倍

一慢就加索引?先停手。我见过加了12个索引,查询只快了一点点的案例。真正该盯的,是数据库在“喘不过气”还是“饿着肚子”。

上个月帮一个本地生活平台查订单页卡顿。开发说SQL早优化过了,索引也全了。我连上去看监控:CPU不到30%,磁盘I/O却飙到95%。原来临时表还在用老机械盘,一查大字段就堵死。换到SSD后,页面打开快了不少。

具体做法:用SHOW PROCESSLIST扫一眼当前正在跑的查询。如果一堆卡在“Sending data”,大概率是磁盘或网络拖慢;如果卡在“Creating sort index”,说明排序没走索引,得看WHERE和ORDER BY字段有没有覆盖好。

索引加再多,不如这3个地方查一次

索引不是贴纸,乱贴反而让数据库更懵。比如给is_deleted这种只有0/1的字段建索引,基本等于没建——数据库扫一半数据,还不如直接全表扫。

之前看过一个报表查询,JOIN了7张表,每张都建了索引,结果执行计划里一张千万级的订单表还是全表扫描。原因很简单:驱动表选错了。优化器挑了数据量最大的那张当主表,其他表索引再好也白搭。

3个必须检查的索引细节

  1. 联合索引的字段顺序:把区分度高的字段放前面。比如status只有3个值,created_at每天都不一样,那WHERE status=1 AND created_at > '2024-01-01',索引应该建在(created_at, status),而不是反过来。
  2. 覆盖索引:让查询不用回表。比如SELECT id, name FROM users WHERE status = 1,建(status, id, name)索引,整条SQL就在索引里跑完,不碰磁盘。
  3. 索引下推(ICP):MySQL 5.6+默认开启,只要WHERE里的字段都在索引里,过滤会提前到存储引擎层做,少几次回表。你不用额外操作,但得确保索引字段包住了WHERE条件。

改写SQL的3个换血级操作,让响应时间直接腰斩

有些SQL慢,真不是数据量的问题,就是写法卡住了数据库的脖子。我亲眼见过一条SQL,只是把WHERE DATE(created_at) = ...改成范围查询,响应从秒级掉到毫秒级。

操作1:别在WHERE里对字段用函数

-- 错误写法
SELECT * FROM orders WHERE DATE(created_at) = '2024-01-01';
-- 正确写法
SELECT * FROM orders WHERE created_at >= '2024-01-01 00:00:00' AND created_at < '2024-01-02 00:00:00';

DATE()一加,索引直接失效。换成范围,索引立刻生效。

操作2:IN子查询太猛?试试EXISTS

-- 慢写法
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount > 1000);
-- 快写法
SELECT * FROM users WHERE EXISTS (SELECT 1 FROM orders WHERE orders.user_id = users.id AND amount > 1000);

IN会先把子查询结果全捞出来再匹配;EXISTS是“找到一个就停”,尤其外层数据多、内层匹配少的时候,快得多。当然,如果子查询结果就三五条,IN也没问题——别教条,看EXPLAIN

操作3:分页别死磕OFFSET

-- 慢写法
SELECT * FROM articles ORDER BY id LIMIT 100000, 20;
-- 快写法
SELECT * FROM articles WHERE id > 100000 ORDER BY id LIMIT 20;

OFFSET越大,数据库越要数着行跳过去。记住上一页最后一条的id,下一页直接从它后面查,速度稳得很。

缓存不是万能药,但用对地方能救命

缓存不是开关一按就万事大吉。我见过团队把所有接口结果塞Redis,结果运营改了条商品信息,缓存全崩,数据库瞬间被压到报警。

缓存该用在哪?

  • 查询结果缓存:适合几乎不变的数据,比如城市列表、支付状态字典。用Redis存,设个5分钟过期,够用又不至于太旧。
  • 查询计划缓存:MySQL 8.0以后没Query Cache了,但像ProxySQL这类代理层可以缓存执行计划,省掉SQL解析和优化步骤。
  • 页面静态化:高流量但更新不勤的页面,比如活动首页、产品详情页,后台生成HTML文件,Nginx直出,连PHP/Java都不走。

一个真实案例:某本地资讯站首页聚合5张表数据,每次打开近2秒。我把整个首页JSON缓存进Redis,30秒过期。30秒内所有请求读缓存,响应压到10毫秒左右。内容更新时,后台主动删掉对应key就行。

连接池和事务隔离——被忽视的响应时间杀手

SQL优化完了,索引也加了,缓存也上了,可偶尔还是卡一下?大概率不是SQL的问题,是连接不够用,或者事务锁得太久。

连接池问题:MySQL默认max_connections常设151,小项目可能够,一上量就排队。看下Threads_connectedThreads_running,如果前者远大于后者,说明大量连接在等空闲连接。生产环境建议调到200–500,根据内存情况定。

事务隔离级别:MySQL默认REPEATABLE READ,并发高时容易锁住整行甚至整范围。比如一个长事务在更新用户余额,另一个查询等着读同一行,就卡住了。

具体检查方法:运行SHOW ENGINE INNODB STATUS,搜LOCK WAIT。如果看到一堆等待记录,就是锁冲突。解法很实在:

  • 把大事务拆小,只留必要操作;
  • 如果业务允许,把隔离级别降到READ COMMITTED
  • 给高频更新字段加索引,缩小锁影响范围。

今天就能执行的一个操作:用慢查询日志抓出最拖垮系统的3条SQL

别等下次告警。现在打开你天天用的数据库终端,照着做:

  1. 确保慢查询日志开着(没开就开):

    SET GLOBAL slow_query_log = ON;
    SET GLOBAL long_query_time = 2;
    
  2. 让它跑半天或一天,然后用自带工具筛出最慢的3条:

    mysqldumpslow -s t -t 3 /var/lib/mysql/your-hostname-slow.log
    
  3. 拿到这3条SQL,挨个执行EXPLAIN,重点看:

    • type是不是ALLindex
    • rows预估扫描行数是不是离谱?
    • Extra里有没有Using filesortUsing temporary
  4. 就从第一条开始改:加索引、重写SQL、缩事务——按文章里说的三个方向,挑一个最痛的下手。

做完这四步,你会明显感觉到“卡顿少了”。先治最疼的,其他的,明天继续。