凌晨三点三刻,我手机突然炸了 —— 电商运维小哥带着哭腔说:"哥,订单查询接口又超时了,用户都在骂,运营要疯了!" 我登上去一看,一条
SELECT * FROM order_info WHERE DATE(create_time) = '2025-11-11' AND user_id = 12345跑了 5.8 秒,执行计划里
type=ALL,全表扫描 300 万行。
这事儿我见多了:有人分完库分表还是慢,有人加了索引跟没加一样,还有人瞎改参数把 MySQL 改崩了。其实 MySQL 调优哪有那么玄乎?就靠 "三板斧"——索引优化、SQL 改写、参数调优,练熟了,90% 的慢查询都能搞定。
今天我就用 "电商订单查询" 这个真实场景,把这三板斧拆透,中间穿插点 "程序员专属吐槽",再给你画点 "灵魂示意图",保证你看完能直接拿去救急,再也不用凌晨被叫醒。
很多人调优先瞎改 SQL,其实得先明白:MySQL 慢,本质是 "做了太多无用功"—— 比如明明有索引却不用,非要扫全表;明明能走 3 层 B + 树,非要绕 5 层;明明内存能存下数据,非要频繁读磁盘。
举个例子:你去超市买可乐,正常流程是 "找饮料区→拿可乐→结账"(3 步);慢查询就是 "从超市入口开始,每个货架都看一遍,最后才找到可乐"(100 步)。我们调优,就是把 "100 步" 拧回 "3 步"。
先上张 "慢查询 vs 优查询" 的灵魂对比图,一眼看懂差距:
plaintext
【慢查询流程】
用户SQL → MySQL优化器:"这SQL没走索引啊,扫全表吧" → 硬盘读300万行数据 → 过滤出1行 → 返回(耗时5秒)
↓(像爬10层楼梯,每步都喘)
【优查询流程】
用户SQL → MySQL优化器:"走order_info_user_create索引,3步就到" → 内存读3行数据 → 直接返回(耗时0.1秒)
↓(像坐电梯,按个按钮就到)
很多人建索引跟 "囤货" 似的,以为越多越好,结果维护索引的时间比查询还长;还有人建了索引,写 SQL 时又把它 "废掉",典型的 "费力不讨好"。
电商有个慢 SQL:"查用户 12345 在 2025-11-11 下的订单",表
order_info建了索引
idx_user_create(user_id, create_time),结果执行计划还是全表扫描:
sql
-- 慢SQL:5.8秒
SELECT order_id, amount FROM order_info
WHERE user_id = 12345
AND DATE(create_time) = '2025-11-11';
我一看就笑了:
DATE(create_time)这函数把索引 "砍废了"!就像你建了 "姓名 + 生日" 的索引,结果查的时候非要 "姓名 + 生日的月份",索引根本认不出。
plaintext
【没优化前:索引失效】
idx_user_create索引(user_id, create_time)的B+树:
层1:user_id=1000 → 指向层2;user_id=2000 → 指向层2...
层2:user_id=12345 → create_time=2025-11-11 08:00 → 指向层3;create_time=2025-11-12...
层3:具体数据地址...
但SQL用了DATE(create_time),把create_time切成了'2025-11-11',B+树里存的是完整时间,根本匹配不上,只能全表扫!
【优化后:索引生效】
把SQL改成范围查询,不用函数:
SELECT order_id, amount FROM order_info
WHERE user_id = 12345
AND create_time >= '2025-11-11 00:00:00'
AND create_time < '2025-11-12 00:00:00';
此时B+树能精准匹配:先找user_id=12345,再找create_time在[2025-11-11, 2025-11-12)区间,3层就定位到数据,耗时0.1秒!
你点 "珍珠奶茶少糖",店员会先找珍珠,再找奶茶,最后调糖度;索引也一样,
idx_a_b_c(a,b,c),只能匹配 "a" "a+b" "a+b+c",不能跳着来。
比如你建了
idx_user_create(user_id, create_time),查
create_time='2025-11-11'会失效,查
user_id=12345 AND create_time='xxx'才生效。
很多人查
SELECT order_id, amount FROM ...,却建了只包含
user_id的索引,MySQL 得先查索引,再回表找
amount,像 "去快递站拿包裹,先找单号,再去仓库翻包裹"。
改成覆盖索引
idx_user_create_amount(user_id, create_time, amount),索引里直接有
amount,不用回表,速度快 2-3 倍。
有人给
order_status(1 - 已支付,2 - 取消,3 - 待付款)建索引,区分度才 30%,MySQL 觉得 "扫索引还不如全表快",直接不用。这种列别建索引,浪费空间还没用。
SELECT *:查 10 列,索引只覆盖 3 列,必回表;别囤索引:一张表超过 5 个索引,插入数据时维护索引的时间比查询还长;定期删无用索引:用
SELECT * FROM sys.schema_unused_indexes WHERE table_name = 'order_info'找半年没用到的索引,删!
"我这 SQL 逻辑没问题啊,为啥慢?"—— 很多程序员都这么说。但逻辑对不代表效率高,就像你从北京到上海,步行也能到,但不如坐飞机快。
DATE_SUB函数,索引失效sql
SELECT order_id FROM order_info
WHERE DATE(create_time) >= DATE_SUB(CURDATE(), INTERVAL 30 DAY);
sql
SELECT order_id FROM order_info
WHERE create_time >= CURDATE() - INTERVAL 30 DAY
AND create_time < CURDATE();
DATE(create_time)破坏了
create_time的有序性,B + 树没法定位;范围查询
>=和
<能精准匹配索引区间,就像你查 "1-30 号的快递",直接找对应货架,不用逐个翻。
IN代替
OR—— 别让 MySQL"左右为难"
WHERE user_id=123 OR user_id=456,如果
user_id有索引,MySQL 可能会全表扫;改成
WHERE user_id IN (123,456),会走索引,速度快 10 倍。
LIMIT 100000, 10会扫 100010 行,扔 100000 行,像 "翻 100 本书,只看最后 10 页"。
优化后:
sql
SELECT o.* FROM order_info o
JOIN (SELECT order_id FROM order_info LIMIT 100000, 10) t
ON o.order_id = t.order_id;
先查主键
order_id(小字段,快),再关联查全量数据,耗时从 2 秒变 0.2 秒。
NULL很敏感
WHERE user_name IS NULL会导致索引失效,改成
WHERE user_name = ''(提前用空字符串存空值),走索引。
循环 1000 次
INSERT,要跟 MySQL 建立 1000 次连接;改成
INSERT INTO ... VALUES (...),(...),1 次连接搞定,速度快 50 倍。
JOIN表按 "小表驱动大表"—— 别让大表 "当导游"
JOIN时,小表当驱动表(左边),大表当被驱动表(右边)。比如
users(10 万行)JOIN
order_info(300 万行),要
users在左,
order_info在右,避免大表循环 100 万次。
很多人调优最后一步才动参数,这是对的 —— 参数是 "锦上添花",不是 "雪中送炭"。先优化索引和 SQL,再调参数,不然参数调上天也没用。
innodb_buffer_pool_size——MySQL 的 "内存货架"这是最重要的参数,用来存常用数据和索引,设小了不够用,设大了占内存。
类比:超市把常用的货放货架上,不用每次都去仓库深处搬;buffer pool 就是 MySQL 的 "货架"。设置建议:物理内存的 70%(比如服务器 32G 内存,设 22G),别设 100%,留内存给系统。避坑:别设太小(比如 2G),300 万行数据存不下,频繁读磁盘,慢!
innodb_log_file_size——MySQL 的 "记账本大小"MySQL 写数据时,先写日志(redo log)再写磁盘,日志文件太小,会频繁切换日志,像 "记账本每页只写 10 个字,翻页翻到手软"。
设置建议:4G-8G(别超过 4G,太大了恢复时慢),比如设 4G,减少切换次数。避坑:别设太小(默认 48M),大事务时日志不够用,频繁刷盘。
innodb_flush_log_at_trx_commit——MySQL 的 "记账频率"
max_connections:别设太大(比如 10000),连接多了 MySQL 扛不住,设 500-1000 够了;
query_cache_type:MySQL 8.0 已经删了,别再查怎么开了;
sort_buffer_size:别设太大(默认 256K),设成 1M 会导致内存溢出,够用就行。
EXPLAIN,看
type是不是
ALL,
Extra有没有
Using filesort/
Using temporary,别上来就改;别信 "测试环境":测试环境 10 万数据,生产 300 万,能一样吗?调优要在生产环境的备份库上测;循序渐进:先优化索引,再改写 SQL,最后调参数,别一上来就梭哈改配置;定期监控:用 Prometheus+Grafana 监控慢查询数、QPS、buffer pool 命中率,别等出问题才补救。