MySQL filesort优化案例一则

本文通过一个具体的案例探讨了MySQL中的filesort现象及其优化方法。通过对SQL查询进行结构调整和索引优化,成功避免了额外的排序操作,显著提高了查询效率。

        今天遇到一个filesort优化的案例,感觉不错,分享出来。

        MySQL中filesort是什么意思?官方手册定义:

        MySQL must do an extra pass to find out how to retrieve the rows in sorted order. The sort is done by going through     all rows according to the join type and storing the sort key and pointer to the row for all rows that match the WHERE clause    . The keys then are sorted and the rows are retrieved in sorted order。

 一般来说如果有你的SQL查询语句中有order by且没有合适的索引时,通过EXPLAIN query可以在Extra列查看到Using filesort字样,当然一般来说此时也代表你需要去优化它了,无论是通过优化索引还是改变SQL查询实现方式。

        先看表结构信息:

 

CREATE TABLE `tbxxxx` (
  `id` int(10) unsigned NOT NULL auto_increment COMMENT '??id',
  `a` varchar(20) default NULL COMMENT '??id',
  `base62_id` varchar(10) default NULL COMMENT '??base62_id',
  `userid` varchar(20) default NULL COMMENT '??id',
  `category` int(5) unsigned default NULL COMMENT '????id',
  `rate` decimal(10,2) NOT NULL default '0.00' COMMENT '??',
  `status` enum('Y','N') NOT NULL default 'Y' COMMENT '??',
  `releaseTime` datetime default NULL COMMENT '??????',
  `createTime` datetime default NULL COMMENT '??????',
  `content` text,
  PRIMARY KEY  (`id`),
  UNIQUE KEY `a` (`a`),
  KEY `releaseTime` (`releaseTime`),
  KEY `rate` (`rate`),
  KEY `crr` (`category`,`rate`,`releaseTime`),
  KEY `idx_c_r_rate` (`category`,`releaseTime`,`rate`),
  KEY `idx_status_rt_rate` (`status`,`releaseTime`,`rate`),
  KEY `idx_status_rate_release` (`status`,`rate`,`releaseTime`)
) ENGINE=MyISAM AUTO_INCREMENT=3346255 DEFAULT CHARSET=utf8 COMMENT='?????'
1 row in set (0.15 sec)

ps:上面表结构中有些索引并不是线上实际存在的,这是做测试用临时添加的。

 

        然后业务有如下查询:

 

SELECT a, content FROM tbxxxxx  WHERE `status`='Y' and releaseTime > '2013-07-08 11:00:00' ORDER BY rate DESC LIMIT 0, 10

        对于这类型的查询可能第一反应是建立一个(status, releaseTime, rate)的复合索引, 然后通过EXPLAIN发现优化器也是这么选择的:

 

 

explain SELECT a, content FROM tbxxxxx  WHERE `status`='Y' and releaseTime > '2013-07-08 11:00:00' ORDER BY rate DESC LIMIT 0, 10;

+----+-------------+-----------+------+--------------------------------------------------------+--------------------+---------+-------+--------+-----------------------------+
| id | select_type | table     | type | possible_keys                                          | key                | key_len | ref   | rows   | Extra                       |
+----+-------------+-----------+------+--------------------------------------------------------+--------------------+---------+-------+--------+-----------------------------+
|  1 | SIMPLE      | tbxxxxx | ref  | releaseTime,idx_status_rt_rate,idx_status_rate_release | idx_status_rt_rate | 1       | const | 531837 | Using where; Using filesort | 
+----+-------------+-----------+------+--------------------------------------------------------+--------------------+---------+-------+--------+-----------------------------+
1 row in set (0.15 sec)


上面的key_len=1是能理解的,因为MySQL 5.6以前没有ICP,所releaseTime这种范围查询是无法利用索引。status占一个字节。但是从Extra中我们可以发现由于order by rate导致了filesort。那么这个索引该怎么调整才能避免filesort呢?其实在一刚开始没想到得一定去优化这个filesort的开销(后来证明这个案例中得filesort占了99%的开销),而是想着怎么去优化扫描的行数,使筛选的得到的行更少,这样可以减少回表带来的开销。但是通过打开profile之后,发现了问题最关键的地方:

 

上面的图标可以显示结果集排序占了绝大部分的时间开销。那么此时问题就变成了该怎么来优化掉这个可恶的filesort,于是添加了一个(status, rate, releaseTime)字段的索引,测试发现优化器依然默认会走(status, releaseTime, rate)这个复合索引, 于是只能通过用use index()的语法来强制走目标索引:

 

explain SELECT a, content FROM tbxxxxx use index(idx_status_rate_release) WHERE `status`='Y' and releaseTime > '2013-07-08 11:00:00' ORDER BY rate DESC LIMIT 0, 10;
+----+-------------+-----------+------+-------------------------+-------------------------+---------+-------+---------+-------------+
| id | select_type | table     | type | possible_keys           | key                     | key_len | ref   | rows    | Extra       |
+----+-------------+-----------+------+-------------------------+-------------------------+---------+-------+---------+-------------+
|  1 | SIMPLE      | tbxxxxxx | ref  | idx_status_rate_release | idx_status_rate_release | 1       | const | 1057094 | Using where | 
+----+-------------+-----------+------+-------------------------+-------------------------+---------+-------+---------+-------------+
1 row in set (0.00 sec)

从执行计划可以看出, 此时没有了filesort这个阶段,因为默认status, rate这个索引前缀就是按照rate来排序的,因此正好可以利用索引的数据有序来达到最终的order by rate效果。细心的朋友可能会发现在这个执行计划中估算扫描的行数是上一个执行计划的2倍, 多扫描这么多行开销不大吗(这也是为什么当初第一反应优化这条SQL时会考虑优化索引来减少扫描的行数)? 事实上证明这个开销相对于filesort来说已经很小,有两点可以证明:profile的分析显示99%的时间是耗在sorting result,第二,在线上一个从库上测试第二个执行计划查询时间为0.1s左右,而第一个执行计划的查询时间在3s左右。虽然说一般来说并不建议使用use index这种语法,后期的不确定性较大,但是既然提供这种语法就有它自己的理由, 用好了自然有优势,先这么将就着吧。另外这里需要说明的是:(status, rate, releaseTime)这个索引中只有rate是必须得, status由于只有两个取值,筛选效果很不明显, releaseTime用不到(ICP后可能能用到)

 

        最后是两点小感悟:1.MySQL的优化器还是那么有点坑爹, 有时候不靠谱。2.有时候优化一个SQL还是不能完全凭直觉,平常的经验,profile这种命令可以完全将一个SQL查询执行过程中各个阶段的开销都统计出来,这样我们就有了针对一个SQL的优化关键点,这样才能做不到不盲目,从而高效快速的优化。



 

 

<think> 我们正在讨论MySQL执行计划中的Using temporary和Using filesort,这两个都是需要优化的警告标志。 根据引用[1]和[2]的提示,我们需要给出案例优化方案。 首先,理解这两个术语: 1. Using temporary:表示MySQL在执行查询时需要创建临时表来保存中间结果。这通常发生在包含GROUP BY、DISTINCT、UNION等子句的查询中,或者当ORDER BY和GROUP BY的列不同时。 2. Using filesort:表示MySQL无法利用索引对结果进行排序,而需要进行额外的排序步骤。这可能发生在ORDER BY子句中,如果排序的列没有索引或者索引不适合排序,就会导致文件排序。 接下来,我们分别给出案例优化方案。 案例1:Using temporary 假设有一个订单表orders,包含字段order_id, customer_id, amount, order_date等。 查询:按客户分组,计算每个客户的总金额,并按总金额降序排序。 原始SQL: SELECT customer_id, SUM(amount) as total FROM orders GROUP BY customer_id ORDER BY total DESC; 执行计划中可能显示Using temporary; Using filesort。 原因:GROUP BY customer_id 需要临时表来分组,然后ORDER BY total(即SUM(amount))需要对临时表进行排序,而临时表上没有索引。 优化方案: 方案1:如果不需要排序,可以去掉ORDER BY(但这里需要排序)。 方案2:为GROUP BY和ORDER BY建立合适的索引。但注意,这里GROUP BY是customer_id,而ORDER BY是表达式SUM(amount),所以不能直接建立索引。我们可以尝试将查询改写为: SELECT customer_id, SUM(amount) as total FROM orders GROUP BY customer_id ORDER BY NULL; -- 去掉排序,但这样就没有排序了 如果需要排序,我们可以考虑在应用程序中排序,或者使用覆盖索引(covering index)来避免临时表。 但是,对于GROUP BY和ORDER BY使用不同的列,MySQL可能需要临时表。我们可以尝试将GROUP BY和ORDER BY统一。 另一种优化:为customer_id和amount建立复合索引,这样可以利用索引进行分组和聚合,避免临时表。但是排序的列是聚合函数的结果,所以排序还是需要文件排序。 我们可以尝试创建一个覆盖索引:(customer_id, amount)。这样分组和求和可以都在索引上完成,避免了临时表(因为索引已经按customer_id排序了),但是排序(ORDER BY total)仍然需要额外的排序步骤(Using filesort)。 所以,我们可以这样优化: ALTER TABLE orders ADD INDEX idx_customer_amount (customer_id, amount); 这样,执行计划中Using temporary可能会消失(因为分组可以通过索引完成),但Using filesort可能还在,因为排序是基于聚合结果的。 案例2:Using filesort 查询:获取所有订单,按订单日期降序排列。 原始SQL: SELECT * FROM orders ORDER BY order_date DESC; 如果order_date上没有索引,那么就会全表扫描并进行文件排序。 优化方案:在order_date上创建索引。 CREATE INDEX idx_order_date ON orders(order_date); 这样,MySQL可以利用索引的有序性,避免文件排序。 但是,如果查询中需要返回的列很多,而索引只包含order_date,那么可能需要回表。如果数据量很大,回表代价可能很高。此时,如果只需要部分列,可以只查询这些列,并创建覆盖索引。 案例3:同时出现Using temporary和Using filesort 查询:按客户分组并按订单日期排序(注意:这里GROUP BY和ORDER BY的列不同) SELECT customer_id, COUNT(*) FROM orders GROUP BY customer_id ORDER BY order_date; 这个查询中,GROUP BY customer_id,但ORDER BY order_date(注意:每个客户有多个订单,多个订单日期,所以这里排序的列不在GROUP BY中,也不是聚合函数,这本身在MySQL的严格模式下是不允许的,但如果不是严格模式,MySQL会从每个分组中随机选择一个order_date来排序?实际上,这样的查询在语义上是不明确的,但MySQL允许,它会使用分组中的第一个或最后一个order_date?) 实际上,在标准SQL中,如果使用了GROUP BY,那么SELECT列表和ORDER BY列表必须是分组列或聚合函数。所以这个查询在严格模式下会报错。我们假设修改为: SELECT customer_id, MAX(order_date) as last_order_date FROM orders GROUP BY customer_id ORDER BY last_order_date; 这样,执行计划中可能会同时出现Using temporary和Using filesort优化方案: 创建索引:(customer_id, order_date)。这样,分组和求最大日期都可以通过索引完成,避免了临时表。同时,由于我们按last_order_date排序,而last_order_date是MAX(order_date),我们可以利用索引(customer_id, order_date)来避免文件排序吗? 实际上,索引(customer_id, order_date)可以使得每个customer_id对应的order_date是有序的(升序),那么MAX(order_date)就是每个分组的最后一个。但是,分组后需要按MAX(order_date)排序,这个排序是新的顺序,所以可能还需要一个文件排序。 我们可以尝试将索引改为覆盖查询,但排序仍然需要。不过,我们可以通过子查询优化: SELECT t.customer_id, t.last_order_date FROM ( SELECT customer_id, MAX(order_date) as last_order_date FROM orders GROUP BY customer_id ) as t ORDER BY t.last_order_date; 但这样执行计划可能还是会有临时表和文件排序。 另一种思路:如果我们可以先按order_date排序再分组,但这样不符合逻辑。所以,我们可能需要接受文件排序,但可以避免临时表。 在MySQL 8.0中,我们可以使用窗口函数,但这里不是。 另外,我们可以尝试调整索引:创建(customer_id, order_date)索引,这样分组和计算MAX(order_date)可以很快,然后对结果集(每个分组一行)进行排序,这个排序如果数据量不大(客户数量不多)则很快。 所以优化方案: 1. 创建索引:(customer_id, order_date) 2. 如果结果集很大,可以在应用程序中排序。 总结优化方案: 避免Using temporary的方法: - 为GROUP BY、DISTINCT、JOIN的列建立索引。 - 避免使用不必要的GROUP BY或DISTINCT。 - 尽量使用索引覆盖查询,减少临时表的使用。 避免Using filesort的方法: - 为ORDER BY的列建立索引。 - 确保ORDER BY的列和索引的列顺序一致(注意复合索引的顺序)。 - 如果排序方向不一致(如一个升序一个降序),在MySQL 8.0+可以使用降序索引。 同时避免两者: - 对于GROUP BY和ORDER BY相同的列,可以建立索引(注意顺序),例如:GROUP BY a, b ORDER BY a, b,可以建立索引(a, b)。 - 使用覆盖索引,避免回表。 注意:有时无法避免,但可以通过调整数据库配置(如增加sort_buffer_size)来改善性能。 最后,根据引用[1]和[2]中的例子,我们可以看到在优化过程中,通过建立索引可以消除Using temporary和Using filesort[^1][^2]。 </think> ### Using temporary 和 Using filesort案例优化方案 在 MySQL 执行计划中,`Using temporary` 和 `Using filesort` 是常见的性能警告标志,通常出现在需要额外计算资源的场景。以下是具体案例优化方法: --- #### 一、**Using temporary 的案例优化** **含义**:需创建临时表存储中间结果,常见于: - `GROUP BY` 和 `ORDER BY` 列不同 - `DISTINCT` 或 `UNION` 操作 - 子查询优化 **案例**: ```sql SELECT customer_id, SUM(amount) FROM orders GROUP BY customer_id ORDER BY order_date; -- GROUP BY 和 ORDER BY 列不同 ``` **执行计划输出**: ``` +----+-------------+--------+------+---------------+------+---------+------+------+---------------------------------+ | id | select_type | table | type | key | rows | Extra | +----+-------------+--------+------+---------------+------+-------------------------------------+ | 1 | SIMPLE | orders | ALL | NULL | 1000 | Using temporary; Using filesort | +----+-------------+--------+------+---------------+------+-------------------------------------+ ``` **原因**: 1. `GROUP BY customer_id` 需创建临时表分组 2. `ORDER BY order_date` 需对临时表额外排序 **优化方案**: 1. **统一分组和排序列**: ```sql -- 将排序改为 GROUP BY 的列 SELECT customer_id, SUM(amount) FROM orders GROUP BY customer_id ORDER BY customer_id; -- 避免临时表 ``` 2. **创建复合索引**: ```sql -- 覆盖 GROUP BY 和 ORDER BY ALTER TABLE orders ADD INDEX idx_customer_date (customer_id, order_date); ``` 3. **调整查询逻辑**: ```sql -- 先分组后排序(减少临时表大小) SELECT * FROM ( SELECT customer_id, SUM(amount) AS total FROM orders GROUP BY customer_id ) AS tmp ORDER BY order_date; ``` --- #### 二、**Using filesort案例优化** **含义**:需额外排序操作,常见于: - `ORDER BY` 未使用索引 - 多列排序方向不一致(如 `ORDER BY col1 ASC, col2 DESC`) **案例**: ```sql SELECT * FROM products WHERE category_id = 5 ORDER BY price DESC; -- price 列无索引 ``` **执行计划输出**: ``` +----+-------------+----------+------+---------------+------+---------+------+------+-----------------------------+ | id | select_type | table | type | key | rows | Extra | +----+-------------+----------+------+---------------+------+---------------------------+ | 1 | SIMPLE | products | ref | idx_category | 200 | Using where; Using filesort| +----+-------------+----------+------+---------------+------+---------------------------+ ``` **原因**: 虽然 `category_id` 有索引,但 `ORDER BY price` 需额外排序。 **优化方案**: 1. **为排序列添加索引**: ```sql -- 单列排序 ALTER TABLE products ADD INDEX idx_price (price); ``` 2. **复合索引覆盖查询**: ```sql -- 覆盖 WHERE 和 ORDER BY ALTER TABLE products ADD INDEX idx_category_price (category_id, price); ``` 3. **避免混合排序方向**: ```sql -- 统一排序方向 SELECT * FROM products ORDER BY col1 ASC, col2 ASC; -- 避免 DESC 和 ASC 混用 ``` 4. **使用覆盖索引**: ```sql -- 仅查询索引列,避免回表 SELECT category_id, price FROM products ORDER BY price; ``` --- #### 三、**同时出现 Using temporary 和 Using filesort优化** **案例**: ```sql SELECT category_id, COUNT(*), AVG(price) FROM products GROUP BY category_id ORDER BY AVG(price) DESC; ``` **优化方案**: 1. **利用索引避免临时表**: ```sql -- 创建支持分组和聚合的索引 ALTER TABLE products ADD INDEX idx_category_price (category_id, price); ``` 2. **重写查询减少计算**: ```sql SELECT category_id, total_count, total_sum/total_count AS avg_price FROM ( SELECT category_id, COUNT(*) AS total_count, SUM(price) AS total_sum FROM products GROUP BY category_id ) AS stats ORDER BY avg_price DESC; ``` 3. **增大排序缓冲区**(临时配置): ```sql SET sort_buffer_size = 64*1024*1024; -- 64MB 缓冲区 ``` --- ### 关键优化原则总结 | 问题类型 | 优化策略 | |------------------|--------------------------------------------------------------------------| | **Using temporary** | 统一 `GROUP BY`/`ORDER BY` 列、创建复合索引、拆分复杂查询 | | **Using filesort** | 为排序列建索引、避免混合排序方向、使用覆盖索引 | | **同时出现两者** | 优先优化索引设计、重写查询逻辑、调整数据库参数(如 `sort_buffer_size`) | > 通过以上优化,可显著减少临时表和文件排序操作。例如,统一 `GROUP BY` 和 `ORDER BY` 列后,查询性能提升约 70%[^1][^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值