一条SQL拖垮整个系统:我用50毫秒替你踩完了所有坑

线上系统上线三个月,用户反馈"页面加载越来越慢",排查后发现核心订单查询从200ms飙升到了8秒。问题不在代码逻辑,不在服务器硬件,而藏在那几条我们写完就忘的SQL语句里。这篇文章不讲理论框架,只聊真实踩坑后总结出来的SQL优化方法论,附带完整案例和可直接复用的代码示例。

一、SQL优化:那些被忽视的性能杀手

很多开发同学觉得SQL优化是DBA的事,自己写完能跑就行。这话放在数据量几千条的时候没毛病,但当单表突破百万级,你写的每一个JOIN、每一个WHERE条件,都在悄悄决定系统的生死。

SQL优化的本质其实就一件事:让数据库用最少的代价,找到你要的数据。 所谓"代价",就是磁盘I/O次数、内存占用、CPU计算量。我们要做的,就是把这些代价压到最低。

常见的性能杀手有这么几个:

1、全表扫描:没有索引或者索引失效,数据库只能一行一行地扫,数据量一大直接崩盘。

2、隐式类型转换:比如字段是varchar类型,你却用数字去比较,索引直接作废。

3、SELECT *:取了一堆根本用不上的字段,网络传输和内存全浪费了。

4、深分页:LIMIT 1000000, 20这种写法,数据库要先扫描100万条再丢掉,极其消耗资源。

5、OR连接条件:有时候会导致索引失效,具体要看场景,但大多数情况下建议拆成UNION。

这些问题听起来简单,但在实际项目里,往往是好几个叠加在一起,才把系统拖垮的。

二、索引策略示例:不是建了索引就快了

索引是SQL优化里最核心的手段,但也是最容易用错的。很多人的习惯是"哪个字段经常出现在WHERE里就给它建索引",结果索引建了一堆,查询反而更慢了。

索引不是越多越好,而是越精准越好。

举个实际的例子。我们有一张订单表 orders,字段包括 id、user_id、status、create_time、amount。业务上最常见的查询是:

sql

SELECT id, create_time, amount

FROM orders

WHERE user_id = 10086 AND status = 2

ORDER BY create_time DESC

LIMIT 20;

如果你只给 user_id 建了索引,数据库会先找到这个用户的所有订单,然后再逐行过滤 status = 2,最后排序。当这个用户有几十万条订单时,性能依然很差。

正确的做法是建联合索引:

sql

CREATE INDEX idx_user_status_time ON orders(user_id, status, create_time);

这里有个关键原则叫最左前缀原则:联合索引 (a, b, c) 只能有效支持以 a 开头、a+b 开头、a+b+c 开头的查询条件。如果你的WHERE里只有 b 和 c,这个索引是用不上的。

所以上面那个查询,user_id 排在最前面是因为它的区分度最高,status 排第二用来过滤,create_time 排第三刚好满足ORDER BY,避免了额外的排序操作。

再看一个反面案例。有人给 status 单独建了索引,觉得"状态就那几个值,建索引应该很快"。但实际上 status 的区分度太低了,可能90%的数据都是 status = 1,数据库用这个索引还不如直接全表扫描。

判断要不要建索引,核心看区分度。 区分度 = 不重复值的数量 / 总行数。区分度低于0.1的字段,单独建索引基本没意义。

三、查询优化案例:一条SQL从8秒到50毫秒

下面这个案例来自真实项目,做了脱敏处理。

背景: 电商平台的商品搜索接口,支持按关键词模糊搜索 + 分类筛选 + 价格区间 + 排序,数据量大约500万条商品。

原始SQL(简化版):

sql

SELECT id, title, price, category_id

FROM products

WHERE title LIKE '%手机%'

AND category_id = 5

AND price BETWEEN 1000 AND 5000

ORDER BY sales DESC

LIMIT 20;

这条SQL的执行时间是8.3秒,用户根本等不了。

第一步:分析Execute Plan

用 EXPLAIN 看了一下执行计划,发现 title LIKE '%手机%' 导致了全表扫描,因为模糊查询以通配符开头,B+树索引根本用不上。

第二步:优化思路

既然 LIKE '%关键词%' 无法走索引,那就换个思路——用全文索引(FullText Index)替代。

sql

ALTER TABLE products ADD FULLTEXT INDEX ft_title(title);

然后把查询改成:

sql

SELECT id, title, price, category_id

FROM products

WHERE MATCH(title) AGAINST('手机' IN NATURAL LANGUAGE MODE)

AND category_id = 5

AND price BETWEEN 1000 AND 5000

ORDER BY sales DESC

LIMIT 20;

执行时间降到了1.2秒,有进步但还不够。

第三步:继续深挖

看执行计划发现,ORDER BY sales DESC 导致了 Using filesort,也就是数据库在内存里做了一次排序。加上 LIMIT 20,理论上可以用覆盖索引来避免排序。

于是建了一个新的联合索引:

sql

CREATE INDEX idx_cat_price_sales ON products(category_id, price, sales DESC);

最终SQL:

sql

SELECT id, title, price, category_id

FROM products

WHERE MATCH(title) AGAINST('手机' IN NATURAL LANGUAGE MODE)

AND category_id = 5

AND price BETWEEN 1000 AND 5000

ORDER BY sales DESC

LIMIT 20;

执行时间:52毫秒。从8秒到50毫秒,提升了160倍。

这个案例说明一个道理:优化不是一步到位的,是一层一层剥洋葱,每次解决一个瓶颈。

四、Explain对比:学会看执行计划,比猜快100倍

很多人写完SQL就直接上线,从来不看 EXPLAIN 的输出。其实 EXPLAIN 就是数据库给你的"体检报告",学会看它,能避免80%的性能问题。

下面用一个对比表格来说明关键字段的含义:

字段 含义 好的值 差的值

type 访问类型 ref、range、index ALL(全表扫描)

possible_keys 可能用到的索引 有值 NULL

key 实际使用的索引 命中了预期索引 NULL 或错误索引

rows 预估扫描行数 越少越好 几万甚至几十万

Extra 额外信息 Using index(覆盖索引) Using filesort、Using temporary

举个对比的例子:

优化前的执行计划:

字段 值

type ALL

possible_keys NULL

key NULL

rows 4873621

Extra Using where; Using filesort

优化后的执行计划:

字段 值

type ref

possible_keys idx_user_status_time

key idx_user_status_time

rows 38

Extra Using index condition

type 从 ALL 变成了 ref,rows 从487万变成了38,这就是优化前后的差距。

特别注意 Extra 字段里的两个坑:

1、Using filesort:说明排序没用到索引,需要额外的排序操作,数据量大时很慢。

2、Using temporary:说明用了临时表,通常出现在GROUP BY或者DISTINCT的场景里,要警惕。

如果你的 EXPLAIN 结果里出现了这两个,基本可以确定这条SQL需要优化了。

五、几条拿来就能用的优化建议

1、能用 = 就别用 LIKE,能用 LIKE 'abc%' 就别用 LIKE '%abc%'。前者走索引,后者不走。

2、小表驱动大表:JOIN的时候,让数据量小的表当驱动表,减少循环次数。

3、避免在WHERE里对字段做函数操作:比如 WHERE YEAR(create_time) = 2025,这会让索引失效。改成 WHERE create_time >= '2025-01-01' AND create_time < '2026-01-01'。

4、深分页用延迟关联:

sql

-- 不推荐

SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;

-- 推荐:先查ID,再关联回表

SELECT o.*

FROM orders o

INNER JOIN (

SELECT id FROM orders ORDER BY id LIMIT 1000000, 20

) tmp ON o.id = tmp.id;

5、COUNT(*) 优化:在InnoDB引擎下,COUNT(*) 和 COUNT(1) 性能基本一样,都是优化过的。但 COUNT(字段) 会多一步判断NULL的操作,能用 COUNT(*) 就别指定字段。

六、写在最后

SQL优化这件事,说到底不是什么高深技术,而是一种对数据的敬畏心。你多看一眼执行计划,多想一层索引的顺序,线上就可能少一次故障。

那些看似不起眼的几百毫秒差距,累积起来就是用户体验的天壤之别。

不要等到出了问题才去优化,最好的时机是写SQL的那一刻。

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

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

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

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

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

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

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐