欢迎光临
我们一直在努力

扫描行数降1200万倍:优化天花板

扫描行数降1200万倍:优化天花板

你有没有遇到过这种情况——线上一个查询跑了8秒,整个页面白屏,老板在群里@你,用户在投诉,而你盯着那条SQL却不知道从哪下手?别慌,这种事我经历过太多次了。今天这篇文章,我不讲空洞的理论,直接拿真实案例,从定位慢查询、分析执行计划、到索引策略调整,一步一步带你走完整个SQL优化的全流程。看完这篇,下次遇到慢查询,你心里就有谱了。

一、先别急着加索引——慢查询的根因往往不在索引上

很多人一看到慢查询,第一反应就是"加个索引不就完了?"这话对,但只对了一半。

我之前接手过一个订单系统的优化项目,有一条查询语句耗时超过12秒:

sql

SELECT o.id, o.order_no, u.nickname, p.product_name

FROM orders o

LEFT JOIN users u ON o.user_id = u.id

LEFT JOIN products p ON o.product_id = p.id

WHERE o.status = 3 AND o.created_at > '2025-01-01'

ORDER BY o.created_at DESC

LIMIT 20;

表面上看,这条SQL涉及三张表的关联,orders表有800万条数据,users表200万条,products表50万条。团队第一反应就是给orders.status和orders.created_at加联合索引。

加完之后呢?查询时间从12秒降到了9秒。

没错,加了索引反而只快了3秒。问题出在哪?

后来用EXPLAIN一看才发现:这条SQL虽然走了索引,但因为LEFT JOIN的存在,MySQL需要先扫描orders表中所有status=3且created_at > '2025-01-01'的记录,然后逐条去users表和products表做关联。数据量一大,嵌套循环的代价就非常高。

所以第一个教训:加索引之前,先看执行计划,搞清楚瓶颈到底在哪。

二、EXPLAIN到底怎么看?别只盯着type列

很多教程教你看EXPLAIN,就说看type列,ref比ALL好,range比ref好。这话没错,但远远不够。

我来拆解一下EXPLAIN输出中真正值得关注的几个字段:

字段名 含义 关注点

id 查询的执行序号 id相同表示同时执行,id越大越先执行

select_type 查询类型 SIMPLE最好,DERIVED表示有子查询,要警惕

type 访问类型 ALL是全表扫描,必须优化;index是全索引扫描,也不理想

key 实际使用的索引 和possible_keys对比,看有没有用上合适的索引

key_len 索引使用的字节长度 越短越好,说明索引利用率高

rows 预估扫描行数 这个数字越接近真实返回行数越好,差太多说明统计信息不准

Extra 额外信息 Using filesort、Using temporary都是性能杀手

回到刚才那个案例,EXPLAIN的输出是这样的:

id select_type table type possible_keys key key_len rows Extra

1 SIMPLE o range idx_status_created idx_status_created 5 320000 Using where; Using filesort

1 SIMPLE u eq_ref PRIMARY PRIMARY 4 1 NULL

1 SIMPLE p eq_ref PRIMARY PRIMARY 4 1 NULL

问题一目了然:orders表扫描了32万行,而且出现了Using filesort——也就是说,虽然走了索引,但MySQL还是额外做了一次文件排序来处理ORDER BY created_at DESC。

这就是为什么加了索引只快了3秒的原因:索引解决了WHERE条件的过滤问题,但没有解决ORDER BY的排序问题。

三、索引策略不是越多越好——这三条原则请记住

做了这么多年数据库优化,我总结了三条索引设计的核心原则:

1、联合索引要遵循最左前缀原则,而且列的顺序决定了索引的效率。

比如idx_status_created (status, created_at)这个联合索引,对于WHERE status = 3 AND created_at > '2025-01-01'这种查询是完美匹配的。但如果你的查询条件变成了WHERE created_at > '2025-01-01' AND status = 3,虽然结果一样,但从索引利用的角度,顺序不影响最终效果。可如果查询条件只有created_at > '2025-01-01',这个联合索引照样能用上——因为created_at是第二列,按照最左前缀,第一列status没出现在条件里,索引其实只能用到一部分。

更关键的是,如果你的查询是WHERE status = 3 ORDER BY created_at DESC,而索引是(status, created_at),那么这个索引可以同时解决过滤和排序,避免filesort。但如果索引是(created_at, status),虽然也能用,但排序方向可能和索引顺序不一致,MySQL还是可能退化为文件排序。

2、覆盖索引能极大减少回表开销。

所谓覆盖索引,就是查询需要的所有字段都包含在索引里,MySQL不需要再回主键索引去查数据。

刚才那个案例,如果我们建一个覆盖索引:

sql

ALTER TABLE orders ADD INDEX idx_status_created_cover

(status, created_at, id, order_no);

注意这里把id和order_no也加进去了。这样EXPLAIN的Extra字段就会出现Using index,表示完全走覆盖索引,不需要回表。

改完之后,查询时间直接从9秒降到了0.3秒。

3、不要给区分度低的列单独建索引。

比如status字段只有0、1、2、3四个值,区分度极低。单独给status建索引,MySQL大概率不会用,因为走索引的代价比全表扫描还高。但如果和created_at组成联合索引,情况就完全不同了——因为联合索引的区分度是各列区分度的乘积,一下子就上去了。

四、一个真实的查询优化案例——从45秒到200毫秒

说个我去年处理过的真实案例。某电商平台的商品搜索接口,核心SQL大概长这样:

sql

SELECT p.id, p.name, p.price, c.category_name

FROM products p

INNER JOIN categories c ON p.category_id = c.id

WHERE p.name LIKE '%手机%'

AND p.is_active = 1

ORDER BY p.sales DESC

LIMIT 20;

这条SQL的问题非常典型:

LIKE '%手机%'是前置模糊匹配,普通B+Tree索引完全失效。

ORDER BY sales DESC需要排序。

products表有1200万条数据。

原始查询耗时45秒。

我的优化思路分三步走:

第一步,把LIKE '%手机%'改成全文检索。MySQL自带的FULLTEXT索引支持自然语言搜索:

sql

ALTER TABLE products ADD FULLTEXT INDEX ft_name (name);

然后把查询改成:

sql

WHERE MATCH(p.name) AGAINST('手机' IN NATURAL LANGUAGE MODE)

这一步直接把全表扫描变成了全文索引扫描,扫描行数从1200万降到了不到5000。

第二步,给(is_active, sales)建联合索引,解决WHERE过滤和ORDER BY排序的问题:

sql

ALTER TABLE products ADD INDEX idx_active_sales (is_active, sales DESC);

第三步,考虑到categories表只有不到200条数据,JOIN的代价极低,不需要额外优化。

最终优化后的SQL:

sql

SELECT p.id, p.name, p.price, c.category_name

FROM products p

INNER JOIN categories c ON p.category_id = c.id

WHERE MATCH(p.name) AGAINST('手机' IN NATURAL LANGUAGE MODE)

AND p.is_active = 1

ORDER BY p.sales DESC

LIMIT 20;

结果:查询时间从45秒降到了200毫秒,提升了225倍。

五、Explain对比——优化前后的执行计划差异有多大

光说数字可能没感觉,我把优化前后的EXPLAIN结果做个对比:

优化前:

id select_type table type key rows Extra

1 SIMPLE p ALL NULL 12000000 Using where; Using filesort

1 SIMPLE c eq_ref PRIMARY 1 NULL

优化后:

id select_type table type key rows Extra

1 SIMPLE p fulltext ft_name 1 Using where; Using filesort

1 SIMPLE c eq_ref PRIMARY 1 NULL

看到区别了吗?type从ALL(全表扫描)变成了fulltext(全文索引扫描),rows从1200万变成了1。虽然Extra里还有Using filesort,但因为扫描的行数极少,排序的代价几乎可以忽略。

六、几个容易被忽略的优化细节

1、LIMIT偏移量过大时,性能会急剧下降。

sql

SELECT * FROM orders ORDER BY created_at DESC LIMIT 1000000, 20;

这条SQL要先扫描1000020行,然后丢弃前1000000行,只返回20行。优化方案是用WHERE id > 上一页最大ID来代替LIMIT偏移:

sql

SELECT * FROM orders

WHERE id < 上一页最小ID

ORDER BY id DESC LIMIT 20;

2、JOIN的顺序很重要。

MySQL的优化器会自动调整JOIN顺序,但如果你用的是STRAIGHT_JOIN强制指定顺序,或者优化器统计信息不准,就可能出现小表驱动大表的情况,性能会非常差。定期执行ANALYZE TABLE来更新统计信息,是个好习惯。

3、批量操作别用循环,用SET代替。

sql

— 慢

FOR each id IN (SELECT id FROM orders WHERE status = 0):

UPDATE orders SET status = 1 WHERE id = id;

— 快

UPDATE orders SET status = 1 WHERE status = 0;

这个看似简单,但我见过太多人在代码里写循环单条更新,一条SQL能解决的事,愣是跑了几千次。

七、总结:SQL优化的核心不是背八股文,而是建立分析思维

回过头来看,SQL优化这件事,说到底就是三个字:看、想、改。

看:用EXPLAIN看执行计划,找到真正的瓶颈。

想:想清楚数据分布、索引原理、MySQL的执行逻辑,再动手。

改:改SQL、改索引、改表结构,每一步改完都要对比执行计划,用数据说话。

别迷信任何一个"银弹"技巧。索引不是越多越好,联合索引不是随便建的,EXPLAIN不是看一眼就完事的。真正的优化能力,来自于你对每一条慢查询的认真分析,而不是对某个参数的盲目调整。

希望这篇文章能帮你在下次遇到慢查询时,少走一些弯路。

💡注意:本文所介绍的软件及功能均基于公开信息整理,仅供用户参考。在使用任何软件时,请务必遵守相关法律法规及软件使用协议。同时,本文不涉及任何商业推广或引流行为,仅为用户提供一个了解和使用该工具的渠道。

你在生活中时遇到了哪些问题?你是如何解决的?欢迎在评论区分享你的经验和心得!

希望这篇文章能够满足您的需求,如果您有任何修改意见或需要进一步的帮助,请随时告诉我!

感谢各位支持,可以关注我的个人主页,找到你所需要的宝贝。

 博文入口:山峰哥-CSDN博客 复制到【浏览器】打开即可,宝贝入口:常用软件 宝贝:精品文件

作者郑重声明,本文内容为本人原创文章,纯净无利益纠葛,如有不妥之处,请及时联系修改或删除。诚邀各位读者秉持理性态度交流,共筑和谐讨论氛围~

赞(0)
未经允许不得转载:171主机测评 » 扫描行数降1200万倍:优化天花板
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址