SQL优化实战:3个案例让查询快100倍

SQL优化实战:3个案例让查询快100倍

你给查询加了索引,从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';

如果nameemail都有独立索引,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';

问题:

  1. DATE(login_time)让索引失效
  2. 全表扫描300万行
  3. 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优化三板斧:

  1. 看EXPLAIN:找到type=ALL、Extra带filesort/temporary的地方
  2. 改SQL:避免函数、避免深分页、类型匹配、用UNION代替OR
  3. 加索引:给WHERE、ORDER BY、GROUP BY的列建索引

下次遇到慢查询,先看EXPLAIN,再想怎么改写SQL。很多时候不是数据库不行,是你没告诉数据库该怎么做。

知识库

MySQL EXPLAIN详解:一眼看穿慢SQL

2026-6-5 16:31:08

实操指南知识库

云计算安全框架:从基础到高级的多层次防护策略

2024-12-26 17:12:33

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧