数据库运维培训SQL优化案例分析

4 阅读5分钟

说真的,我刚带运维新人那会儿,最怕听到一句话:“库挂了?慢?不关我事啊,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 运营同事在群里的对话内容、老运维的口头禅等场景描述。

以上案例为假设的教学示例,用于说明真实存在的技术原理。文中所有技术结论均可从上述公开文档中查证。