说真的,我刚带运维新人那会儿,最怕听到一句话:“库挂了?慢?不关我事啊,SQL又不是我写的。”
但你我都知道——数据库慢,十有八九是SQL在作妖。
今天我就拿一个虚构但特别典型的案例,跟你聊聊怎么从一条慢SQL里找出病根,再把它治得服服帖帖。
先交代一下背景。假设我们管着一个电商库,里面有一张叫 orders 的订单表。
这张表已经跑了一年多,数据量从几十万窜到了八百多万行。
结构大致是这样(我简化给你看):
sql
CREATE TABLE orders (
id INT PRIMARY KEY,
order_no VARCHAR(32),
user_id INT,
status VARCHAR(10),
created_at DATETIME,
-- 还有其他字段,不重要
INDEX idx_created (created_at)
);
一天下午,运营同事突然在群里吼:“后台订单列表转不出来了!卡死!”
你赶紧查数据库,发现一条SQL跑了快40秒:
sql
SELECT * FROM orders
WHERE status = 'PAID'
AND DATE(created_at) = '2025-12-01';
这个案例是为教学目的构造的假设场景,但里面的原理——你放心——来自MySQL官方文档和真实生产常见的坑。
第一步:别慌,先看执行计划
我跟你讲,很多新人一听说慢查询,第一反应是“加索引”。
加索引没错,但加哪儿?怎么加?你总得先知道数据库是怎么执行这条SQL的。
连上数据库,敲一句:
sql
EXPLAIN SELECT * FROM orders
WHERE status = 'PAID' AND DATE(created_at) = '2025-12-01';
结果一看:type = ALL,rows = 8.2 million,Extra = Using where。
翻译成人话:全表扫描,八百万行一个不落全翻一遍。
这不是慢,这是灾难。
为什么索引没生效?
表上明明有 idx_created 在 created_at 字段上,可你在 WHERE 里用了 DATE(created_at)。
根据MySQL官方文档(MySQL 8.0 Reference Manual, “How MySQL Uses Indexes”),对索引字段使用函数会使索引失效,数据库只能老老实实做全表扫描。
(引用来源:Oracle, MySQL 8.0 Reference Manual, Section 8.3.1, “How MySQL Uses Indexes”,2025, dev.mysql.com/doc/refman/…
第二步:动手改SQL,别小看这层窗户纸
你可能会问:那我想查某一天的数据怎么办?
很简单,改成范围条件。别用函数包裹字段,把函数挪到等号另一边。
原句:
sql
WHERE DATE(created_at) = '2025-12-01'
改成:
sql
WHERE created_at >= '2025-12-01 00:00:00'
AND created_at < '2025-12-02 00:00:00'
逻辑完全一样,但这一改,created_at 上的索引立马就能用上。
我亲眼见过一个老运维一边改一边念叨:“别在索引上耍花活,你耍它,它耍你。”
改了之后再跑 EXPLAIN,type 变成了 range,rows 从八百万降到了几千。
实际执行时间从40秒掉到0.3秒。
你猜运营同事什么反应?—— “好了好了,不卡了!” 就这十一个字,比加一个月班还舒坦。
第三步:别忘了还有另一个条件 status = 'PAID'
优化到这儿,一般新人就觉得完事了。
但我多嘴问你一句:如果某一天订单量特别大,光用 created_at 范围筛完还有几十万行,那 status 条件怎么处理?
这时候就要考虑联合索引。
你现有的索引是 (created_at)。如果查询里同时有 status 和 created_at 两个条件,可以建一个 (status, created_at) 的联合索引。
注意顺序。按MySQL官方建议(引用同上):
把等值查询的列放前面,范围查询的列放后面。
status = 'PAID' 是等值,created_at 是范围,所以索引 (status, created_at) 比较合适。
建索引的语句:
sql
ALTER TABLE orders ADD INDEX idx_status_created (status, created_at);
建好之后,如果你把查询写成 WHERE status = 'PAID' AND created_at BETWEEN ...,数据库能用索引快速定位到 status='PAID' 的那一批行,再按 created_at 范围扫。
如果反过来先范围后等值,效果会差一些。
第四步:抽样验证,别靠猜
每次改完SQL或索引,别直接上生产。
先在测试环境用同样的数据量跑一遍 EXPLAIN,再看实际执行时间。
我还见过一个挺实在的做法:把慢查询日志打开,设置 long_query_time = 1,跑一跑业务,看优化后的SQL还会不会落进日志。
(事实依据:MySQL官方文档《The Slow Query Log》,明确说明该参数的使用方式。引用:Oracle, MySQL 8.0 Reference Manual, Section 5.4.5, “The Slow Query Log”, 2025。)
写在最后:SQL优化没那么玄,但也别太自信
说句掏心窝的话:我刚入行那两年,也写过上面这种 DATE(column) 的笨SQL。
每个人都是从踩坑里爬出来的。关键不是你背了多少优化口诀,而是——遇到慢查询,第一件事永远是看执行计划,不是你猜。
我给你总结三个最基础但最实用的习惯:
l 对索引字段不使用函数或运算。
l 用 EXPLAIN 看 type、rows、Extra。
l 范围查询的列放在联合索引的后面。
这三个习惯不能让你成为高手,但能让你少背80%的锅。
【事实与推测声明】
1、可验证事实部分(均来自公开文档):
l MySQL中在索引字段上使用函数会导致索引失效(来源:MySQL 8.0 Reference Manual, Section 8.3.1)。
l EXPLAIN 的输出字段含义如 type=ALL 表示全表扫描,type=range 表示范围扫描(来源:同一手册 Section 8.8.2)。
l 联合索引中等值查询列在前、范围查询列在后的建议(来源:同一手册 Section 8.3.6)。
l 慢查询日志参数 long_query_time 的使用方法(来源:同一手册 Section 5.4.5)。
2、构造/推测部分(已明确标识为教学案例):
l orders 表结构与八百万行数据规模。
l 慢SQL从40秒优化到0.3秒的具体数值。
l 运营同事在群里的对话内容、老运维的口头禅等场景描述。
以上案例为假设的教学示例,用于说明真实存在的技术原理。文中所有技术结论均可从上述公开文档中查证。