Mysql 调优记: INNER JOIN查询 Using temporary; Using filesort 问题优化

本文分享了一次SQL性能优化的经验,针对一个执行缓慢的查询进行了详细分析。通过EXPLAIN发现使用了临时表和文件排序,这通常意味着性能瓶颈。文章深入探讨了Using temporary和Using filesort的原因,以及如何通过合理调整SQL语句和索引策略来优化这些问题,最终显著提升了查询速度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

近期笔者在生产环境中发现一条执行非常慢的sql。大概时间为5s左右,于是乎对改SQL场景进行EXPLAIN 分析,发现一个在执行过程中出现对“Using temporary; Using filesort ”。即在执行过程中产生了临时表来存储结果,并在排序时根据连接类型以及存储排序键值和匹配条件的全部行的行指针来排序。经过合理对优化,最终EXPLAIN后,没有再发现明细对SQL性能瓶颈,并且SQL执行时间在1s以内。遂将这点经验记录下来。

先看看SQL和大致的表结构和数据量:

订单表 order 表:大概有15w数据

CREATE TABLE `order` (
  `id` char(32) NOT NULL COMMENT 'id',
  `order_status` varchar(5) NOT NULL DEFAULT '0' COMMENT '订单状态',
  `order_time` datetime DEFAULT NULL COMMENT '下单时间',
   PRIMARY KEY (`id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单数据';

与订单表关联的付款表:大概11w数据

CREATE TABLE `order_payment` (
  `id` char(32) NOT NULL COMMENT 'id',
  `order_id` char(32) NOT NULL DEFAULT '' COMMENT '订单id',
  `money` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '付款金额',
 PRIMARY KEY (`id`),
 KEY `i_order_id` (`order_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单付款数据';

大概了解表结构后,来看一个很普通sql查询场景,查询所有运单状态未完成的订单,并获取其订单ID和付款金额,并按下单时间倒序。

SQL 很简单:

select o.id, op.money from `order` o 
inner join order_payment op on o.id = op.order_id 
where o.order_status = '4' order by o.order_time DESC;

然后通过EXPLAIN 分析,发现:

select_typetabletypepossible_keykeyrowsExtra
SIMPLEopALLi_order_idNULL110000Using temporary; Using filesort
SIMPLEoeq_refPRIMARYPRIMARY150000Using where

对于上述EXPLAIN的各列含义本文不在多赘述,有兴趣的可以查阅笔者的其他博文有介绍。

首先,了解Using temporary; Using filesort 到底是什么,为什么会成为性能杀手。

Using temporary

表示由于排序没有走索引、使用union、子查询连接查询、使用某些视图等原因,因此创建了一个内部临时表。注意这里的临时表可能是内存上的临时表,也有可能是硬盘上的临时表,理所当然基于内存的临时表的时间消耗肯定要比基于硬盘的临时表的实际消耗小。
当mysql需要创建临时表时,选择内存临时表还是硬盘临时表取决于参数tmp_table_size和max_heap_table_size,内存临时表的最大容量为tmp_table_size和max_heap_table_size值的最小值,当所需临时表的容量大于两者的最小值时,mysql就会使用硬盘临时表存放数据。

无论是基于内存,还是基于硬盘。构建临时表的过程是比较耗时的一个操作

Using filesort

Using filesort的含义不能望字生意,它并不代表使用文件排序。仅仅表示没有使用索引的排序。
filesort使用的算法是QuickSort,即对需要排序的记录生成元数据进行分块排序,然后再使用mergesort方法合并块。其中filesort可以使用的内存空间大小为参数sort_buffer_size的值,默认为2M。当排序记录太多sort_buffer_size不够用时,mysql会使用临时文件来存放各个分块,然后各个分块排序后再多次合并分块最终全局完成排序。
笔者个人的理解是,Using filesort并不一定是性能杀手。因为要优化掉Using filesort的唯一办法就是让排序走索引字段,但这情况要根据实际情况来取舍,毕竟建立索引也是有代价的。

了解了上述两个问题产生的原因后,回看之前的sql为和为出现这两个问题。

Using filesort就不用说了,没有走索引导致的。重点来看Using temporary。为何会产生临时表。

MYSQL优化器:JOIN中的顺序选择

Mysql在遇到inner join联接语句时,MySQL表关联的算法是 Nest Loop Join(嵌套联接循环),Nest Loop Join就是通过两层循环手段进行依次的匹配操作,最后返回结果集合。SQL语句只是描述出希望连接的对象和规则,而执行计划和执行操作要切实际将一行行的记录进行匹配。Nest Loop Join的操作过程很简单,很像我们最简单的排序检索算法,两层循环结构。进行连接的两个数据集合(数据表)分别称为外侧表(驱动表)和内侧表(非驱动表)。Mysql又会怎样去确定,哪张表是驱动表,哪张表又是非非驱动表呢?mysql它以表中数据最小的一张表作为驱动表(也就是基表),而另一张表就叫做非驱动表,首先处理驱动表中每一行符合条件的数据,之后的每一行数据和非驱动表进行连接匹配操作,直到循环结束,最后合并结果、返回结果给用户。对于驱动表的字段它是可以直接排序的,然而对于非驱动表的字段排序需要通过循环查询的合并结果(临时表)进行排序,因此,order by o.order_time 时,就先产生了 using temporary(使用临时表)。

前面我们知道 order_payment 的数据量只有11w,那么理所当然的order_payment是驱动表。所以,为了避免 using temporary,就必须使用order作为驱动表,这个时候STRAIGHT_JOIN关键字就来了。

STRAIGHT_JOIN 强指定驱动表

explain select o.id, op.money from `order` o straight_join order_payment op on o.id = op.order_id  where o.order_status = '4' order by o.order_time DESC;

分析结果,你会发现Using temporary 已经消失了。

最好解决掉Using filesort就很简单了,只需要给order_time增加索引就好了。

但是注意,在这个查询场景下,不能只对order_time一个字段增加索引,这样是避免不了 Using filesort的。因为innerDB索引的特性,B TREE 树的叶子节点存储的是索引列数据和主键ID。当只有一个order_time索引时,因为还有查询条件order_status在,所以无法使用到该索引

最佳索引方案

create index i_order_status_time on `order`(`order_status`,`order_time`)

这样B TREE树的叶子节点存储的会是索引列数据包含(order_status和order_time)和主键ID。该索引既能排序又能走查询。在上述条件查询中做到最佳性能。

最终SQL分析:

select_typetabletypepossible_keykeyrowsExtra
SIMPLEorefPRIMARY,i_order_status_timei_order_status_time150000Using where; Using index
SIMPLEoprefi_order_idi_order_id110000

查询时候大幅提升。

<think>好的,我现在需要帮用户优化MySQL中涉及60万和130万数据表的关联查询性能。首先,我得理清楚用户的问题场景。他们有两个表,一个60万行,另一个130万行,进行关联查询时速度不够理想,想要提升性能。 首先,我应该考虑索引的情况。关联查询的关键在于连接字段是否有合适的索引。如果用户没有在连接字段上建立索引,或者索引类型不合适,比如用了错误的索引类型,比如该用复合索引却只用了单列索引,这可能会导致全表扫描,性能自然低下。需要检查两个表的连接字段是否都有索引,尤其是被驱动表的连接字段必须有索引,否则会出现全表扫描,影响性能。 接下来是执行计划分析。用户可能没有查看过EXPLAIN的结果,所以需要引导他们使用EXPLAIN来查看查询的执行计划,确认是否真正用到了索引,以及是否有不必要的全表扫描。执行计划中的type字段如果是ALL,就说明全表扫描,需要优化。此外,查看Extra列是否有Using filesortUsing temporary,这些都可能影响性能,需要查询索引。 然后,表结构设计是否合理。比如,两个表的字符集和排序规则是否一致,如果不一致,可能会导致隐式转换,使索引失效。另外,字段类型是否恰当,比如用VARCHAR存储数字,或者字段长度过大,这都会影响索引的效率。此外,是否可以考虑冗余一些字段,减少关联查询的需要,或者使用覆盖索引,让查询只需要访问索引,而不需要回表,这样可以提升速度。 关联查询的类型也需要考虑。如果用户使用的是LEFT JOIN,但实际业务允许使用INNER JOIN,可能会更高效,因为LEFT JOIN需要处理更多的数据。此外,关联条件是否有其他过滤条件,能否将过滤条件下推到驱动表,减少参与关联的数据量。比如,在驱动表上先过滤掉大部分数据,再进行关联,这样被驱动表需要处理的数据量就会减少。 如果数据量实在太大,可能需要考虑分库分表或者分区。比如,按时间范围或者业务键进行分区,使得每次查询只需要扫描部分分区,而不是全表。此外,是否可以使用临时表,将部分数据先筛选出来,再进行关联,减少关联的数据量。 缓存的应用也是一个方向。如果查询结果不经常变化,可以使用应用层缓存,比如Redis,避免每次查询都访问数据库。或者使用MySQL查询缓存,不过需要注意查询缓存的适用场景,比如表更新频繁的话,查询缓存可能效果不佳。 硬件和配置优化方面,需要检查MySQL的配置参数是否合理。比如,join_buffer_size的大小是否足够,如果关联查询使用了BNL(Block Nested Loop)算法,增大join_buffer_size可以提高性能。另外,内存配置是否足够,确保InnoDB缓冲池足够大,能够缓存更多的数据和索引,减少磁盘IO。 应用层优化可能包括批量处理,减少单次查询的数据量,或者使用异步处理,避免前端长时间等待。此外,是否有不必要的重复查询,可以通过缓存或者重构业务逻辑来减少查询次数。 如果上述方法还不够,可能需要考虑读写分离,将查询分流到只读副本,减轻主库的压力。或者使用垂直拆分,将大表拆分为多个小表,减少单表的数据量。 最后,如果所有优化手段都用了,还是无法满足性能需求,可能需要提升硬件,比如使用更快的SSD,增加内存,或者升级CPU。不过硬件升级应该是最后的选择,先从架构和查询优化入手。 在回答的时候,需要按照逻辑顺序,逐步引导用户检查索引、执行计划、表结构,然后考虑查询优化、分区分表、缓存、配置整等。同时,结合用户提供的引用内容,比如引用[1]提到的分区分表策略,引用[3]提到的缓存势,引用[2]中的慢查询日志整,这些都可以作为优化建议的参考。需要确保建议具体可行,并且有实际的操作步骤,比如如何创建索引,如何分析执行计划,如何整配置参数等。</think>### 优化 MySQL 大量数据关联查询的解决方案 --- #### 一、索引优化(核心手段) 1. **关联字段必须创建索引** - 确保两个表的关联字段(如`user_id`)都创建了**B+树索引**,且类型一致(如`INT`与`BIGINT`不兼容会导致索引失效) - 示例:若表A(60w)与表B(130w)通过`order_id`关联,需执行: ```sql ALTER TABLE table_A ADD INDEX idx_order_id(order_id); ALTER TABLE table_B ADD INDEX idx_order_id(order_id); ``` - **注意**:被驱动表(通常是数据量更大的表)的关联字段必须建索引[^1] 2. **复合索引覆盖查询条件** - 如果查询包含`WHERE`过滤条件,需将过滤字段与关联字段组合成**复合索引** - 例如查询`SELECT * FROM A JOIN B ON A.id=B.a_id WHERE A.status=1`,应为表A创建`(status, id)`的复合索引 --- #### 二、执行计划分析 1. **使用`EXPLAIN`诊断性能瓶颈** ```sql EXPLAIN SELECT * FROM table_A a JOIN table_B b ON a.order_id = b.order_id WHERE a.create_time > '2023-01-01'; ``` - 关键指标检查: - `type`列:应出现`ref`或`eq_ref`,若为`ALL`则存在全表扫描 - `rows`列:估算扫描行数应明显小于表总量 - `Extra`列:若出现`Using temporary; Using filesort`需优化排序条件[^2] --- #### 三、查询语句优化 1. **减少结果集大小** - 避免`SELECT *`,明确列出所需字段 - 添加时间范围过滤(如`WHERE create_time > NOW() - INTERVAL 7 DAY`) 2. **分批次处理** ```sql -- 分页处理示例 SELECT * FROM table_A a JOIN table_B b ON a.order_id = b.order_id LIMIT 0, 50000; ``` 3. **改写关联顺序** - 将数据量小的表作为驱动表(可通过`STRAIGHT_JOIN`强制指定) ```sql SELECT /*+ STRAIGHT_JOIN */ * FROM table_A a JOIN table_B b ON a.order_id = b.order_id; ``` --- #### 四、表结构优化 1. **分区表策略** - 对130w的大表按时间范围分区: ```sql ALTER TABLE table_B PARTITION BY RANGE (YEAR(create_time)) ( PARTITION p2022 VALUES LESS THAN (2023), PARTITION p2023 VALUES LESS THAN (2024) ); ``` 2. **字段类型优化** - 将`VARCHAR(255)`的关联字段改为`INT UNSIGNED`(可减少30%索引存储空间) --- #### 五、服务器参数 1. **整关键参数(需重启生效)** ```ini [mysqld] join_buffer_size = 256M # 默认256K,提升关联计算效率 innodb_buffer_pool_size = 12G # 建议设为物理内存的70% tmp_table_size = 512M # 防止磁盘临时表 ``` 2. **监控慢查询日志** ```sql SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1; # 录超过1秒的查询 ``` --- #### 六、架构级优化 1. **读写分离** - 将报表类查询分流到只读副本 2. **数据冗余设计** - 在表B中冗余表A的常用字段,避免关联查询 3. **缓存层加持** - 使用Redis缓存热点数据(如最近3天的关联结果)[^3] --- #### 性能对比预估 | 优化手段 | 预计响应时间下降比例 | 实施难度 | |--------------------|----------------------|----------| | 添加正确索引 | 70%-90% | ★★☆☆☆ | | 查询语句重构 | 30%-50% | ★★★☆☆ | | 分区表 | 40%-60% | ★★★★☆ | | 参数 | 10%-20% | ★★★☆☆ | ---
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值