
你给查询加了索引,从5秒变成了2秒。你觉得优化到位了。但你可能不知道,同样的查询,改写一下SQL,能从2秒变成0.02秒。索引不是万能的。SQL怎么写,数据库怎么执行,差距比想象中大得多。
今天讲3个真实案例,每个都让查询快了几十倍甚至上百倍。不聊理论,直接上实战。
先看一个数据
某数据库团队统计,超过50%的SQL性能问题,靠加索引能解决一部分,但完全解决需要改写SQL。索引是让数据库少读数据,SQL改写是让数据库更聪明地读数据。两者结合,效果才最好。
案例一:深分页优化
问题场景:用户列表页,支持翻到100页以后。每页20条。
sql
SELECT * FROM users ORDER BY id LIMIT 200000, 20;
执行时间:约3秒。OFFSET 200000意味着MySQL要扫描20万行,然后扔掉前20万行,只留最后20行。越往后翻越慢。
优化方案:记住上一页最后一条的ID,下一页用WHERE id > 上一页最大ID。
sql
-- 第一页 SELECT * FROM users ORDER BY id LIMIT 20; -- 假设上一页最后一条id是10000,下一页这样查 SELECT * FROM users WHERE id > 10000 ORDER BY id LIMIT 20;
执行时间:0.01秒。快300倍。不管翻到第几页,永远只扫20行。但要求id是自增且连续,且不能跳页(用户只能点下一页,不能直接跳100页)。
业务改造建议:前台展示只允许“下一页”和“上一页”,后台管理保留“跳页”但限制最大页码(如只能前50页),超出强制用搜索功能替代。
案例二:COUNT(*) 优化
问题场景:统计某个条件的订单数量,条件字段没索引,或者有索引但数据量很大。
sql
SELECT COUNT(*) FROM orders WHERE status = 'pending';
假设status字段只有三个值(‘pending’,’paid’,’shipped’),区分度很低。即使加了索引,也要扫描大量行。
优化方案一:用估算值。业务上是否需要精确值?
sql
-- 用EXPLAIN估算行数 EXPLAIN SELECT * FROM orders WHERE status = 'pending';
rows列是估算值,误差可接受的话直接用。
优化方案二:用汇总表。每产生一条pending订单,在计数表里+1。查询时直接读汇总表。
sql
-- 计数表
CREATE TABLE order_stats (
status VARCHAR(20) PRIMARY KEY,
cnt INT
);
-- 下单时更新
UPDATE order_stats SET cnt = cnt + 1 WHERE status = 'pending';
-- 查询时直接读
SELECT cnt FROM order_stats WHERE status = 'pending';
执行时间:0.001秒。快几百倍。缺点是计数可能短暂不准,适合对实时性要求不高的统计。
优化方案三:COUNT(1) 和 COUNT(*) 性能相同,但COUNT(列) 不统计NULL,通常没必要用列名。
案例三:避免在索引列上使用函数
问题场景:统计2026年1月的订单。
sql
SELECT * FROM orders WHERE DATE(create_time) = '2026-01-01';
create_time上有索引,但DATE(create_time)函数导致索引失效。MySQL要对每一行执行DATE()函数,无法用索引快速定位。
执行时间:全表扫描,约5秒。
优化方案:用范围查询代替函数。
sql
SELECT * FROM orders WHERE create_time >= '2026-01-01 00:00:00' AND create_time < '2026-01-02 00:00:00';
执行时间:0.02秒,快250倍。
其他常见函数陷阱:
WHERE LEFT(phone, 3) = '138'→ 改成WHERE phone LIKE '138%'WHERE YEAR(create_time) = 2026→ 改成WHERE create_time BETWEEN '2026-01-01' AND '2026-12-31'WHERE id + 1 = 10000→ 改成WHERE id = 9999
反常识点:即使索引列参与了计算,只要不是把函数用在列上,有时仍能部分用到索引,但多数情况会失效。最佳实践:永远不要在WHERE的列上套函数。
附加案例:隐式类型转换
问题场景:手机号字段是VARCHAR,查询时用了数字。
sql
SELECT * FROM users WHERE phone = 13800138000;
phone字段是VARCHAR,传入的是数字。MySQL会隐式转换,把phone转成数字再比较,导致索引失效。
优化方案:类型保持一致。
sql
SELECT * FROM users WHERE phone = '13800138000';
执行时间:从全表扫描秒级降到索引查询毫秒级。
附加案例:用UNION代替OR
问题场景:查询两个不同条件的用户。
sql
SELECT * FROM users WHERE name = '张三' OR email = 'zhangsan@example.com';
如果name和email都有独立索引,MySQL可能只用其中一个,另一个条件全表扫。
优化方案:用UNION分别查两个索引,再合并。
sql
SELECT * FROM users WHERE name = '张三' UNION SELECT * FROM users WHERE email = 'zhangsan@example.com';
每条子查询都能用索引,再合并去重,效率通常更高。
真实案例:一条查询从8秒到0.02秒
一个后台报表,查询某月活跃用户数:
sql
SELECT COUNT(DISTINCT user_id) FROM user_actions WHERE action = 'login' AND DATE(login_time) BETWEEN '2026-01-01' AND '2026-01-31';
问题:
DATE(login_time)让索引失效- 全表扫描300万行
COUNT(DISTINCT)临时表消耗大
优化后:
sql
SELECT COUNT(DISTINCT user_id) FROM user_actions WHERE action = 'login' AND login_time >= '2026-01-01 00:00:00' AND login_time < '2026-02-01 00:00:00';
加上复合索引(action, login_time, user_id),查询直接从索引获取数据,覆盖索引,无需回表。
执行时间:8秒 → 0.02秒,快400倍。
最后一句
SQL优化三板斧:
- 看EXPLAIN:找到type=ALL、Extra带filesort/temporary的地方
- 改SQL:避免函数、避免深分页、类型匹配、用UNION代替OR
- 加索引:给WHERE、ORDER BY、GROUP BY的列建索引
下次遇到慢查询,先看EXPLAIN,再想怎么改写SQL。很多时候不是数据库不行,是你没告诉数据库该怎么做。




