【转帖】MySQL 8.0 hash join有重大缺陷?

mysql,hash,join,重大,缺陷 · 浏览次数 : 0

小编点评

# Query_time: 304.889654  Lock_time: 0.000178 Rows_sent: 1  Rows_examined: 82986052 **关于MySQL优化课,以下内容与官方知识档案匹配:** * **MySQL优化课第17期:** * 从第15期就升级成MySQL 8.0版本了,现在上车刚刚好,扫码开启MySQL 8.0的修行之旅吧。 * **腾讯课堂《MySQL性能优化》精编版第一期:** * 这课程讲解读几个MySQL性能优化的核心要素:合理利用索引,降低锁影响,提高事务并发度。 **文章知识点与官方知识档案匹配:** * **MySQL入门技能树连接查询INNER JOIN58014:** * 这内容可进一步学习相关知识。 * **归纳总结:** * 生成内容时需要带简单的排版

正文

我并不这么看。

友情提醒:本文建议在PC端阅读。

徐春阳老师发文爆MySQL 8.0 hash join有重大缺陷。

文章核心观点如下:多表(比如3个个表)join时,只会简单的把表数据量小的放在前面作为驱动表,大表放在最后面,从而导致可能产生极大结果集的笛卡尔积,甚至耗尽CPU和磁盘空间。

就此现象,我也做了个测试。

1. 利用TPC-H工具准备测试环境

TPC-H工具在这里下载 http://www.tpc.org/tpch/default5.asp。默认并不支持MySQL,需要自己手动做些调整,参见 https://imysql.com/2012/12/21/tpch-for-mysql-manual.html。

在本案中,我指定的 Scale Factor 参数是10,即:

[root@yejr.run dbgen]# ./dbgen -s 10 && ls -l *tbl-rw-r--r-- 1 root root  244847642 Apr 14 09:52 customer.tbl-rw-r--r-- 1 root root 7775727688 Apr 14 09:52 lineitem.tbl-rw-r--r-- 1 root root       2224 Apr 14 09:52 nation.tbl-rw-r--r-- 1 root root 1749195031 Apr 14 09:52 orders.tbl-rw-r--r-- 1 root root  243336157 Apr 14 09:52 part.tbl-rw-r--r-- 1 root root 1204850769 Apr 14 09:52 partsupp.tbl-rw-r--r-- 1 root root        389 Apr 14 09:52 region.tbl-rw-r--r-- 1 root root   14176368 Apr 14 09:52 supplier.tbl
复制

2. 创建测试表,导入测试数据。

查看几个表的数据量分别是:

+----------+------------+----------+----------------+-------------+--------------+| Name     | Row_format | Rows     | Avg_row_length | Data_length | Index_length |+----------+------------+----------+----------------+-------------+--------------+| customer | Dynamic    |  1476605 |            197 |   291258368 |            0 || lineitem | Dynamic    | 59431418 |            152 |  9035579392 |            0 || nation   | Dynamic    |       25 |            655 |       16384 |            0 || orders   | Dynamic    | 14442405 |            137 |  1992294400 |            0 || part     | Dynamic    |  1980917 |            165 |   327991296 |            0 || partsupp | Dynamic    |  9464104 |            199 |  1885339648 |            0 || region   | Dynamic    |        5 |           3276 |       16384 |            0 || supplier | Dynamic    |    99517 |            184 |    18366464 |            0 |+----------+------------+----------+----------------+-------------+--------------+
复制

提醒:几个测试表都不要加任何索引,包括主键,上表中 Index_length 的值均为0。

3. 运行测试SQL

本案选用的MySQL版本是8.0.19:

[root@yejr.run]> \s...Server version:         8.0.19-commercial MySQL Enterprise Server - Commercial...
复制

徐老师是在用TPC-H中的Q5时遇到的问题,本案也同样选择这个SQL。

不过,本案主要测试Hash Join,因此去掉了其中的GROUP BY和ORDER BY子句

先看下执行计划吧,都是全表扫描,好可怕...

[root@yejr.run]> desc select count(*)-> from->     customer,->     orders,->     lineitem,->     supplier,->     nation,->     region-> where->     c_custkey = o_custkey->     and l_orderkey = o_orderkey->     and l_suppkey = s_suppkey->     and c_nationkey = s_nationkey->     and s_nationkey = n_nationkey->     and n_regionkey = r_regionkey->     and r_name = 'AMERICA'->     and o_orderdate >= date '1993-01-01'->     and o_orderdate < date '1993-01-01' + interval '1' year;+----------+------+----------+----------+----------------------------------------------------+| table    | type | rows     | filtered | Extra                                              |+----------+------+----------+----------+----------------------------------------------------+| region   | ALL  |        5 |    20.00 | Using where                                        || nation   | ALL  |       25 |    10.00 | Using where; Using join buffer (Block Nested Loop) || supplier | ALL  |    98705 |    10.00 | Using where; Using join buffer (Block Nested Loop) || customer | ALL  |  1485216 |    10.00 | Using where; Using join buffer (Block Nested Loop) || orders   | ALL  | 14932433 |     1.11 | Using where; Using join buffer (Block Nested Loop) || lineitem | ALL  | 59386314 |     1.00 | Using where; Using join buffer (Block Nested Loop) |+----------+------+----------+----------+----------------------------------------------------+
复制

加上 format=tree 再看下(真壮观啊。。。)

*************************** 1. row ***************************EXPLAIN: -> Aggregate: count(0)-> Inner hash join (lineitem.L_SUPPKEY = supplier.S_SUPPKEY), (lineitem.L_ORDERKEY = orders.O_ORDERKEY)  (cost=40107736685515472896.00 rows=4010763818487343104)    -> Table scan on lineitem  (cost=0.07 rows=59386314)    -> Hash        -> Inner hash join (orders.O_CUSTKEY = customer.C_CUSTKEY)  (cost=60799566599072.12 rows=6753683238538)            -> Filter: ((orders.O_ORDERDATE >= DATE'1993-01-01') and (orders.O_ORDERDATE < <cache>((DATE'1993-01-01' + interval '1' year))))  (cost=0.16 rows=165883)                -> Table scan on orders  (cost=0.16 rows=14932433)            -> Hash                -> Inner hash join (customer.C_NATIONKEY = nation.N_NATIONKEY)  (cost=3664985889.79 rows=3664956624)                    -> Table scan on customer  (cost=0.79 rows=1485216)                    -> Hash                        -> Inner hash join (supplier.S_NATIONKEY = nation.N_NATIONKEY)  (cost=24976.50 rows=24676)                            -> Table scan on supplier  (cost=513.52 rows=98705)                            -> Hash                                -> Inner hash join (nation.N_REGIONKEY = region.R_REGIONKEY)  (cost=3.50 rows=3)                                    -> Table scan on nation  (cost=0.50 rows=25)                                    -> Hash                                        -> Filter: (region.R_NAME = 'AMERICA')  (cost=0.75 rows=1)                                            -> Table scan on region  (cost=0.75 rows=5)
复制

看起来的确是把最小的表放在最前面,把最大的放在最后面。

在开始跑之前,我们先看一眼手册中关于Hash Join的描述,其中有一段是这样的:

Memory usage by hash joins can be controlled using the join_buffer_sizesystem variable; a hash join cannot use more memory than this amount. When the memory required for a hash join exceeds the amount available, MySQL handles this by using files on disk. If thishappens, you should be aware that the join may not succeed if a hash join cannot fit into memory and it creates more files than set for open_files_limit. To avoid such problems, make either of the following changes: - Increase join_buffer_size so that the hash join does not spill over to disk.- Increase open_files_limit.
复制

简言之,当 join_buffer_size 不够时,会在hash join的过程中转储大量的磁盘表(把一个hash表切分成多个小文件放在磁盘上,再逐个读入内存进行hash join),因此建议加大 join_buffer_size,或者加大 open_files_limit 上限

所以,正式开跑前,我先把join_buffer_size调大到1GB,并顺便看下其他几个参数值:

[root@yejr.run]> select @@join_buffer_size,  @@tmp_table_size,  @@innodb_buffer_pool_size;+--------------------+------------------+---------------------------+| @@join_buffer_size | @@tmp_table_size | @@innodb_buffer_pool_size |+--------------------+------------------+---------------------------+|         1073741824 |         16777216 |               10737418240 |+--------------------+------------------+---------------------------+
复制

并且为了保险起见,在执行SQL时也用 SET_VAR(8.0新特性) 设置了 join_bufer_size,走起。

好在最后这个SQL有惊无险的执行成功,总耗时2911秒。

# Query_time: 2911.426483  Lock_time: 0.000251 Rows_sent: 1  Rows_examined: 76586082
复制

当然了,这个SQL执行过程中的代价也确实非常大,产生了大量的磁盘(不可见)临时文件。

我每隔几秒钟就统计一次所有临时文件的总大小,并且观察磁盘空间剩余量。

/data 分区最开始可用空间是 373GB,这条SQL在峰值吃掉了约170GB,着实可怕。

# 刚开始/dev/vdb       524032000 132967368 391064632  26% /data # 峰值时/dev/vdb       524032000 319732288 204299712  62% /data
复制

CPU的负载从监控上看倒是还算能接受,最高约38.4%


4. 补充测试

上面的测试中,优化器"擅自"修改了驱动顺序,那加上straight_join看看会怎样

[root@yejr.run]> EXPLAIN STRAIGHT_JOIN select count(*)from    customer straight_join     orders  straight_join     lineitem  straight_join     supplier  straight_join     nation  straight_join     regionwhere    c_custkey = o_custkey    and l_orderkey = o_orderkey    and l_suppkey = s_suppkey    and c_nationkey = s_nationkey    and s_nationkey = n_nationkey    and n_regionkey = r_regionkey    and r_name = 'AMERICA'    and o_orderdate >= date '1993-01-01'    and o_orderdate < date '1993-01-01' + interval '1' year;+----------+----------+----------+----------------------------------------------------+| table    | rows     | filtered | Extra                                              |+----------+----------+----------+----------------------------------------------------+| customer |  1485216 |   100.00 | NULL                                               || orders   | 14932433 |     1.11 | Using where; Using join buffer (Block Nested Loop) || lineitem | 59386314 |    10.00 | Using where; Using join buffer (Block Nested Loop) || supplier |    98705 |     1.00 | Using where; Using join buffer (Block Nested Loop) || nation   |       25 |    10.00 | Using where; Using join buffer (Block Nested Loop) || region   |        5 |    20.00 | Using where; Using join buffer (Block Nested Loop) |+----------+----------+----------+----------------------------------------------------+ #format=tree模式下| -> Aggregate: count(0)    -> Inner hash join (region.R_REGIONKEY = nation.N_REGIONKEY)  (cost=204565289351994015744.00 rows=8021527039324357632)        -> Filter: (region.R_NAME = 'AMERICA')  (cost=0.00 rows=1)            -> Table scan on region  (cost=0.00 rows=5)        -> Hash            -> Inner hash join (nation.N_NATIONKEY = customer.C_NATIONKEY)  (cost=200554431911464173568.00 rows=-9223372036854775808)                -> Table scan on nation  (cost=0.00 rows=25)                -> Hash                    -> Inner hash join (supplier.S_NATIONKEY = customer.C_NATIONKEY), (supplier.S_SUPPKEY = lineitem.L_SUPPKEY)  (cost=160446786739199049728.00 rows=-9223372036854775808)                        -> Table scan on supplier  (cost=0.00 rows=98705)                        -> Hash                            -> Inner hash join (lineitem.L_ORDERKEY = orders.O_ORDERKEY)  (cost=16253562153466286.00 rows=16253535510797654)                                -> Table scan on lineitem  (cost=0.01 rows=59386314)                                -> Hash                                    -> Inner hash join (orders.O_CUSTKEY = customer.C_CUSTKEY)  (cost=24638698342.46 rows=2736915995)                                        -> Filter: ((orders.O_ORDERDATE >= DATE'1993-01-01') and (orders.O_ORDERDATE < <cache>((DATE'1993-01-01' + interval '1' year))))  (cost=0.94 rows=165883)                                            -> Table scan on orders  (cost=0.94 rows=14932433)                                        -> Hash                                            -> Table scan on customer  (cost=153126.35 rows=1485216)
复制

最后实际执行耗时

[root@yejr.run]> mysql> select /*+ set_var(join_buffer_size=1073741824) */ STRAIGHT_JOIN count(*)...+----------+| count(*) |+----------+|    72033 |+----------+1 row in set (4 min 12.31 sec)
复制

这个SQL执行过程中,只产生了很少几个临时文件,影响几乎可以忽略不计的那种。

这次之所以会比较快,是因为 orders 表在第二顺序执行,对它还附加了WHERE条件,过滤后数据量变小了(全表1500万,过滤后227万),因此整体执行时间缩短了。

靠着 straight_join 拯救了危机。

此外,在测试的过程中,我还做过一次只有3个表的全表join,下面是执行计划

[root@yejr.run]> desc select count(*) from orders o , lineitem l, partsupp ps whereo.O_CUSTKEY = l.L_SUPPKEY and l.L_PARTKEY = ps.PS_AVAILQTY;+-------+----------+----------+----------------------------------------------------+| table | rows     | filtered | Extra                                              |+-------+----------+----------+----------------------------------------------------+| ps    |  7697248 |   100.00 | NULL                                               || l     | 59386314 |    10.00 | Using where; Using join buffer (Block Nested Loop) || o     | 14932433 |    10.00 | Using where; Using join buffer (Block Nested Loop) |+-------+----------+----------+----------------------------------------------------+
复制

在这个执行计划中,就不会出现徐老师说的问题,不再简单的把最小的表作为驱动表,最大的表放在最后面。

这条SQL耗时304秒,还好吧。

# Query_time: 304.889654  Lock_time: 0.000178 Rows_sent: 1  Rows_examined: 82986052
复制

写在最后

在前几天我的文章《MySQL没前途了吗?》中,其实已经说了MySQL目前不适合做OLAP业务,即便有Hash Join也不行,毕竟其适用的场景很有限。

本案中几个表完全没任何索引,这属于很极端的场景,不应该允许此类现象发生

另外,在已经明确需要走Hash Join的情况下,就应该人为干预,提前加大join_buffer_size,减少执行过程中生成的临时文件

当然了,如果遇到多表JOIN不符合预期时,还可以用STRAIGHT_JOIN强制设定驱动顺序,也可以规避这个问题。

不过,MySQL在偏OLAP场景上的性能的确还有很大提升空间,对此我持谨慎乐观态度,比如把ClickHouse给直接收编了呢 :)

对于本文,我心里不是很有底气,毕竟不是啥源码大神,如果理解上的错误,还请留言指正,不吝感激。

SQL优化大神郑松华对本文亦有贡献,谢谢二位老师。

全文完。


由叶老师主讲的知数堂「MySQL优化课」第17期已发车,课程从第15期就升级成MySQL 8.0版本了,现在上车刚刚好,扫码开启MySQL 8.0的修行之旅吧。


另外,叶老师在腾讯课堂《MySQL性能优化》精编版第一期已完结,本课程讲解读几个MySQL性能优化的核心要素:合理利用索引,降低锁影响,提高事务并发度

下面是自动拼团的二维码,组团价仅需78元

文章知识点与官方知识档案匹配,可进一步学习相关知识
MySQL入门技能树连接查询INNER JOIN58014 人正在系统学习中

与【转帖】MySQL 8.0 hash join有重大缺陷?相似的内容:

【转帖】MySQL 8.0 hash join有重大缺陷?

我并不这么看。 友情提醒:本文建议在PC端阅读。 徐春阳老师发文爆MySQL 8.0 hash join有重大缺陷。 文章核心观点如下:多表(比如3个个表)join时,只会简单的把表数据量小的放在前面作为驱动表,大表放在最后面,从而导致可能产生极大结果集的笛卡尔积,甚至耗尽CPU和磁盘空间。 就此现

[转帖]MySQL 8.0: When to use utf8mb3 over utf8mb4?

https://dev.mysql.com/blog-archive/mysql-8-0-when-to-use-utf8mb3-over-utf8mb4/ MySQL 8.0: When to use utf8mb3 over utf8mb4? Posted on May 19, 2017 by 

[转帖]MySQL 8.0.19 instant add column,亿级数据秒速增加字段

一、MySQL DDL 的方法 MySQL 在大型表上的 DDL 会带来耗时较久、负载较高、额外空间占用、MDL、主从同步延时等情况。需要特别引起重视,而MySQL 的 DDL 有很多种方法。 MySQL 本身自带三种方法,分别是:copy、inplace、instant。 copy 算法为最古老的

[转帖]MySQL 8.0 Instant Add Column功能解析

https://zhuanlan.zhihu.com/p/408702204 概述 DDL(Data Definition Language)是数据库内部的对象进行创建、删除、修改的操作语言,主要包括:加减列、更改列类型、加减索引等类型。数据库的模式(schema)会随着业务的发展不断变化,如果没有

[转帖]MySQL 8.0新特性和性能数据

https://plantegg.github.io/2022/07/03/MySQL8.0%E7%9A%84%E4%B8%80%E4%BA%9B%E6%95%B0%E6%8D%AE/ MySQL 8.0带来了很多新特性 针对性能方面介绍全在这个PPT( http://dimitrik.free.f

【转帖】MySQL 8.0.32如期而至

MySQL 8.0版本计划 MySQL 8.0开始采用快速迭代开发模式,基本上是每隔3个月就发布一个新的小版本。去年1月18日(2022.1.18)发布MySQL 8.0.28,今年1月17日发布MySQL 8.0.32,再看看其他几个版本的时间,还真是贼守时啊。 版本发布时间上一年版本上一年发布时

[转帖]【MySQL】MySQL 8.0 redo log写入性能问题分析

http://kernelmaker.github.io/MySQL_8_core 最近对比了MySQL 5.6和8.0在8核环境下oltp_write_only的性能,发现8.0写入性能(QPS 6-7万)反而低于5.6版本的(QPS 14万),所以进一步测试分析了下redo log这里可能导致性

[转帖]MySQL Performance : 8.0 and UTF8 impact

http://dimitrik.free.fr/blog/posts/mysql-performance-80-and-utf8-impact.html 2018-04-26 00:58 | MySQL, Performance, UTF8 by Dimitri The world is movin

[转帖]第一章 MySQL 8.0 介绍及安装配置

第一章 MySQL 8.0 介绍及安装配置 https://www.jianshu.com/p/d190c6b3520d 本课程,适合具备一定Linux运维或者开发基础的朋友,课程定级中、高级DBA。只要掌握80%,轻松助力薪资15k-25K。课程内容均来自与MySQL官网+MySQL源码。配套精品

[转帖]【MySQL 8】MySQL 5.7都即将停只维护了,是时候学习一波MySQL 8了

https://www.cnblogs.com/paul8339/p/17026571.html 阅读目录 账户与安全 索引增强 原子DDL操作 通用表达式(CTE) 其他 MySQL 8新特性选择MySQL 8的背景:MySQL 5.6已经停止版本更新了,对于 MySQL 5.7 版本,其将于 2