我并不这么看。

友情提醒:本文建议在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,即:


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

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

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


  1. +----------+------------+----------+----------------+-------------+--------------+
  2. | Name     | Row_format | Rows     | Avg_row_length | Data_length | Index_length |
  3. +----------+------------+----------+----------------+-------------+--------------+
  4. | customer | Dynamic    |  1476605 |            197 |   291258368 |            0 |
  5. | lineitem | Dynamic    | 59431418 |            152 |  9035579392 |            0 |
  6. | nation   | Dynamic    |       25 |            655 |       16384 |            0 |
  7. | orders   | Dynamic    | 14442405 |            137 |  1992294400 |            0 |
  8. | part     | Dynamic    |  1980917 |            165 |   327991296 |            0 |
  9. | partsupp | Dynamic    |  9464104 |            199 |  1885339648 |            0 |
  10. | region   | Dynamic    |        5 |           3276 |       16384 |            0 |
  11. | supplier | Dynamic    |    99517 |            184 |    18366464 |            0 |
  12. +----------+------------+----------+----------------+-------------+--------------+

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

3. 运行测试SQL

本案选用的MySQL版本是8.0.19:


  1. [root@yejr.run]> \s
  2. ...
  3. Server version:         8.0.19-commercial MySQL Enterprise Server - Commercial
  4. ...

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

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

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


  1. [root@yejr.run]> desc select count(*)
  2. -> from
  3. ->     customer,
  4. ->     orders,
  5. ->     lineitem,
  6. ->     supplier,
  7. ->     nation,
  8. ->     region
  9. -> where
  10. ->     c_custkey = o_custkey
  11. ->     and l_orderkey = o_orderkey
  12. ->     and l_suppkey = s_suppkey
  13. ->     and c_nationkey = s_nationkey
  14. ->     and s_nationkey = n_nationkey
  15. ->     and n_regionkey = r_regionkey
  16. ->     and r_name = 'AMERICA'
  17. ->     and o_orderdate >= date '1993-01-01'
  18. ->     and o_orderdate < date '1993-01-01' + interval '1' year;
  19. +----------+------+----------+----------+----------------------------------------------------+
  20. | table    | type | rows     | filtered | Extra                                              |
  21. +----------+------+----------+----------+----------------------------------------------------+
  22. | region   | ALL  |        5 |    20.00 | Using where                                        |
  23. | nation   | ALL  |       25 |    10.00 | Using where; Using join buffer (Block Nested Loop) |
  24. | supplier | ALL  |    98705 |    10.00 | Using where; Using join buffer (Block Nested Loop) |
  25. | customer | ALL  |  1485216 |    10.00 | Using where; Using join buffer (Block Nested Loop) |
  26. | orders   | ALL  | 14932433 |     1.11 | Using where; Using join buffer (Block Nested Loop) |
  27. | lineitem | ALL  | 59386314 |     1.00 | Using where; Using join buffer (Block Nested Loop) |
  28. +----------+------+----------+----------+----------------------------------------------------+

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


  1. *************************** 1. row ***************************
  2. EXPLAIN: -> Aggregate: count(0)
  3. -> Inner hash join (lineitem.L_SUPPKEY = supplier.S_SUPPKEY), (lineitem.L_ORDERKEY = orders.O_ORDERKEY)  (cost=40107736685515472896.00 rows=4010763818487343104)
  4.     -> Table scan on lineitem  (cost=0.07 rows=59386314)
  5.     -> Hash
  6.         -> Inner hash join (orders.O_CUSTKEY = customer.C_CUSTKEY)  (cost=60799566599072.12 rows=6753683238538)
  7.             -> 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)
  8.                 -> Table scan on orders  (cost=0.16 rows=14932433)
  9.             -> Hash
  10.                 -> Inner hash join (customer.C_NATIONKEY = nation.N_NATIONKEY)  (cost=3664985889.79 rows=3664956624)
  11.                     -> Table scan on customer  (cost=0.79 rows=1485216)
  12.                     -> Hash
  13.                         -> Inner hash join (supplier.S_NATIONKEY = nation.N_NATIONKEY)  (cost=24976.50 rows=24676)
  14.                             -> Table scan on supplier  (cost=513.52 rows=98705)
  15.                             -> Hash
  16.                                 -> Inner hash join (nation.N_REGIONKEY = region.R_REGIONKEY)  (cost=3.50 rows=3)
  17.                                     -> Table scan on nation  (cost=0.50 rows=25)
  18.                                     -> Hash
  19.                                         -> Filter: (region.R_NAME = 'AMERICA')  (cost=0.75 rows=1)
  20.                                             -> Table scan on region  (cost=0.75 rows=5)

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

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


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

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

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


  1. [root@yejr.run]> select @@join_buffer_size,  @@tmp_table_size,  @@innodb_buffer_pool_size;
  2. +--------------------+------------------+---------------------------+
  3. | @@join_buffer_size | @@tmp_table_size | @@innodb_buffer_pool_size |
  4. +--------------------+------------------+---------------------------+
  5. |         1073741824 |         16777216 |               10737418240 |
  6. +--------------------+------------------+---------------------------+

并且为了保险起见,在执行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,着实可怕。


  1. # 刚开始
  2. /dev/vdb       524032000 132967368 391064632  26% /data
  3. # 峰值时
  4. /dev/vdb       524032000 319732288 204299712  62% /data

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

4. 补充测试

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


  1. [root@yejr.run]> EXPLAIN STRAIGHT_JOIN select count(*)
  2. from
  3.     customer straight_join 
  4.     orders  straight_join 
  5.     lineitem  straight_join 
  6.     supplier  straight_join 
  7.     nation  straight_join 
  8.     region
  9. where
  10.     c_custkey = o_custkey
  11.     and l_orderkey = o_orderkey
  12.     and l_suppkey = s_suppkey
  13.     and c_nationkey = s_nationkey
  14.     and s_nationkey = n_nationkey
  15.     and n_regionkey = r_regionkey
  16.     and r_name = 'AMERICA'
  17.     and o_orderdate >= date '1993-01-01'
  18.     and o_orderdate < date '1993-01-01' + interval '1' year;
  19. +----------+----------+----------+----------------------------------------------------+
  20. | table    | rows     | filtered | Extra                                              |
  21. +----------+----------+----------+----------------------------------------------------+
  22. | customer |  1485216 |   100.00 | NULL                                               |
  23. | orders   | 14932433 |     1.11 | Using where; Using join buffer (Block Nested Loop) |
  24. | lineitem | 59386314 |    10.00 | Using where; Using join buffer (Block Nested Loop) |
  25. | supplier |    98705 |     1.00 | Using where; Using join buffer (Block Nested Loop) |
  26. | nation   |       25 |    10.00 | Using where; Using join buffer (Block Nested Loop) |
  27. | region   |        5 |    20.00 | Using where; Using join buffer (Block Nested Loop) |
  28. +----------+----------+----------+----------------------------------------------------+
  29. #format=tree模式下
  30. | -> Aggregate: count(0)
  31.     -> Inner hash join (region.R_REGIONKEY = nation.N_REGIONKEY)  (cost=204565289351994015744.00 rows=8021527039324357632)
  32.         -> Filter: (region.R_NAME = 'AMERICA')  (cost=0.00 rows=1)
  33.             -> Table scan on region  (cost=0.00 rows=5)
  34.         -> Hash
  35.             -> Inner hash join (nation.N_NATIONKEY = customer.C_NATIONKEY)  (cost=200554431911464173568.00 rows=-9223372036854775808)
  36.                 -> Table scan on nation  (cost=0.00 rows=25)
  37.                 -> Hash
  38.                     -> Inner hash join (supplier.S_NATIONKEY = customer.C_NATIONKEY), (supplier.S_SUPPKEY = lineitem.L_SUPPKEY)  (cost=160446786739199049728.00 rows=-9223372036854775808)
  39.                         -> Table scan on supplier  (cost=0.00 rows=98705)
  40.                         -> Hash
  41.                             -> Inner hash join (lineitem.L_ORDERKEY = orders.O_ORDERKEY)  (cost=16253562153466286.00 rows=16253535510797654)
  42.                                 -> Table scan on lineitem  (cost=0.01 rows=59386314)
  43.                                 -> Hash
  44.                                     -> Inner hash join (orders.O_CUSTKEY = customer.C_CUSTKEY)  (cost=24638698342.46 rows=2736915995)
  45.                                         -> 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)
  46.                                             -> Table scan on orders  (cost=0.94 rows=14932433)
  47.                                         -> Hash
  48.                                             -> Table scan on customer  (cost=153126.35 rows=1485216)

最后实际执行耗时


  1. [root@yejr.run]> mysql> select /*+ set_var(join_buffer_size=1073741824) */
  2.  STRAIGHT_JOIN count(*)
  3. ...
  4. +----------+
  5. | count(*) |
  6. +----------+
  7. |    72033 |
  8. +----------+
  9. 1 row in set (4 min 12.31 sec)

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

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

靠着 straight_join 拯救了危机。

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


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

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

这条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有重大缺陷?的更多相关文章

  1. mysql 8.0.18 hash join测试(内外网首文)

    CREATE TABLE COLUMNS_hj as select * from information_schema.`COLUMNS`; INSERT INTO COLUMNS_hj SELECT ...

  2. MySQL8.0 新特性 Hash Join

    概述&背景 MySQL一直被人诟病没有实现HashJoin,最新发布的8.0.18已经带上了这个功能,令人欣喜.有时候在想,MySQL为什么一直不支持HashJoin呢?我想可能是因为MySQ ...

  3. 如何干涉MySQL优化器使用hash join

    GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源. GreatSQL是MySQL的国产分支版本,使用上与MySQL一致. 前言 实验 总结 前言 数据库的优化器相当于人类的大 ...

  4. MySQL与MariaDB核心特性比较详细版v1.0(覆盖mysql 8.0/mariadb 10.3,包括优化、功能及维护)

    注:本文严禁任何形式的转载,原文使用word编写,为了大家阅读方便,提供pdf版下载. MySQL与MariaDB主要特性比较详细版v1.0(不含HA).pdf 链接:https://pan.baid ...

  5. MySQL 8.0 新特性梳理汇总

    一 历史版本发布回顾 从上图可以看出,基本遵循 5+3+3 模式 5---GA发布后,5年 就停止通用常规的更新了(功能不再更新了): 3---企业版的,+3年功能不再更新了: 3 ---完全停止更新 ...

  6. mysql 8.0.28 查询语句执行顺序实测结果

    TL;NRs 根据实测结果,MySQL8.0.28 中 SQL 语句的执行顺序为: (8) SELECT (5) DISTINCT <select_list> (1) FROM <l ...

  7. SQL Tuning 基础概述06 - 表的关联方式:Nested Loops Join,Merge Sort Join & Hash Join

    nested loops join(嵌套循环)   驱动表返回几条结果集,被驱动表访问多少次,有驱动顺序,无须排序,无任何限制. 驱动表限制条件有索引,被驱动表连接条件有索引. hints:use_n ...

  8. Sort merge join、Nested loops、Hash join(三种连接类型)

    目前为止,典型的连接类型有3种: Sort merge join(SMJ排序-合并连接):首先生产driving table需要的数据,然后对这些数据按照连接操作关联列进行排序:然后生产probed ...

  9. 视图合并、hash join连接列数据分布不均匀引发的惨案

    表大小 SQL> select count(*) from agent.TB_AGENT_INFO; COUNT(*) ---------- 1751 SQL> select count( ...

  10. 最新电Call记录统计-full hash join用法

    declare @time datetime set @time='2016-07-01' --最新的电Call记录统计查询--SELECT t.zuoxi1,t.PhoneCount,t.Phone ...

随机推荐

  1. 云图说丨Astro Canvas一站式数据可视化开发,分钟级构建业务大屏

    摘要:Astro大屏应用是Astro轻应用提供的可视化页面构建服务,提供了丰富的可视化组件.灵活的数据接入和多种方式页面构建能力,支持多屏适配,帮助开发者快速构建和发布专业水准的实时可视化应用. 本文 ...

  2. 认识一下MRS里的“中间人”Alluxio

    摘要:Alluxio在mrs的数据处理生态中处于计算和存储之间,为上层spark.presto.mapredue.hive计算框架提供了数据抽象层,计算框架可以通过统一的客户端api和全局命名空间访问 ...

  3. 华为AppCube通过中国信通院“低代码开发平台通用能力要求”评估!

    摘要:华为AppCube应用魔方顺利通过信通院评估,被认证为具备 "低代码开发平台通用能力"的企业服务平台. 本文分享自华为云社区<华为AppCube通过中国信通院" ...

  4. 学会这5种JS函数继承方式,前端面试你至少成功50%

    摘要:函数继承是在JS里比较基础也是比较重要的一部分,而且也是面试中常常要问到的.下面带你快速了解JS中有哪几种是经常出现且必须掌握的继承方式.掌握下面的内容面试也差不多没问题啦~ 本文分享自华为云社 ...

  5. vue2升级vue3:vue3创建全局属性和方法

    vue2.x挂载全局是使用Vue.prototype.$xxxx=xxx的形式来挂载,然后通过this.$xxx来获取挂载到全局的变量或者方法 在vue3.x这种方法显然是不行了,vue3中在setu ...

  6. web自动化测试(3):web功能自动化测试selenium基础课

    继上篇<web自动化测试(1):为什么选择selenium做自动化测试>,本文介绍如selenium使用 做UI自动化测试,需要什么技能 前端相关技术:HTML.XML.JavaScrip ...

  7. 火山引擎DataTester:如何使用A/B测试优化全域营销效果

      当前,营销技术步入了全渠道.全周期的全域时代,随着广泛的数据积累,数据科学技术在营销领域发挥着越来越重要的作用,从消费者人群洞察到智能化信息广告投放,营销的提效让企业得以在转化的每个环节提升影响力 ...

  8. SQL Server 2016 安装

    数据库安装 选择全新安装模式继续安装 输入产品秘钥:这里使用演示秘钥进行 接受许可 规则检测 可以后期再开放防火墙对外端口 选择需要安装的功能,想省事可以选择[全选] 可以安装JDK,这边选择取消 P ...

  9. SQL SERVER数据分组后取第一条数据——PARTITION BY

    MySQL 数据分组后取第一条数据 SQL SERVER (mssql) 数据分组后取第一条数据 SQL 如下 找状态=1的数据,按 HospitalId,DeptId 组合并倒序排序,每组里面取第一 ...

  10. JDk 与 ADB 环境变量配置

    ### Java环境变量配置 首先,JDK是整个Java的核心,包括了Java运行环境,一推Java工具和Java基础的类库. 网址:https://www.oracle.com/technetwor ...