首页 > 其他分享 >执行计划中的NestLoop对比HashJoin对比

执行计划中的NestLoop对比HashJoin对比

时间:2024-04-03 17:13:23浏览次数:20  
标签:rows family .. HashJoin app NestLoop cost loops 对比

执行计划中的nestloop join 对比hash join

两种join 方式的定义

NESTE LOOP:

在嵌套循环中,内表被外表驱动,外表返回的每一行都要在内表中检索找到与它匹配的行。两个概念:驱动表(外部表)和内部表,这里用表这个次其实不是很准确,外部表和内部表可以是某张表的结果集。

在执行计划中如何区分外部表和内部表。我的通常习惯是安装前后顺序,nestloop join 里面位于上面的是外部表,位于下面的是内部表。一般情况下外部表的loop=1,内部表的loop=外部表的行数。

如下:

根据定义我们可以得到 nest loop 适合于一下两个场景

1、外部表结果集较小,循环次数就少。内部表如果是大表一定要保证大表对应的join字段有索引。

2、外部表结果集较大,内部表结果集较少的场景,同样可以保证nest loop 性能较优,但是如果外部表结果集非常大,循环次数就会很多,就会导致查询缓慢,这时候用hash join 可能效果会更好。

所以总结下来 如果要优化nest loop join 有两个方向

1、减少驱动表的结果集,降低循环次数

2、加快内部表的遍历时间,通过加索引等方式提高内部表的查询效率。

但是具体外部表的记录集多大之后就不建议使用nest loop join 了,有说1万的有说10万的,本人觉得还是要根据实际sql,这里应该没有具体限定值。

nest loop 相关内核参数

enable_nestloop 是否使用nestloop join 默认是on

HASH JOIN :

hash join 做大数据集连接时的常用方式,优化器使用两个表中较小的表(或数据源)利用连接键在内存中建立散列表,然后扫描较大的表并探测散列表,找出与散列表匹配的行。
这种方式适用于较小的表完全可以放于内存中的情况,

这样总成本就是访问两个表的成本之和。但是在表很大的情况下并不能完全放入内存,这时优化器会将它分割成若干不同的分区,不能放入内存的部分就把该分区写入磁盘的临时段,此时要有较大的临时段从而尽量提高I/O 的性能。

从这里就可以看到他的特点和hash索引有点相似,只能用于等值连接。另外他还受限于内存设置,而KingBase 对应的内存就是work_mem。需要保证hash数据全部存放在内存中才能保证hash join 性能最优,否则如果用到过多的 disk 就会严重影响性能。

下图是一个hash join的执行计划截图

注意一点就是hash join 不走索引。

不同场景中hash join 和nest loop join 对比。

场景1: 小表做join

1、构建测试表:
drop table app_family;
CREATE TABLE app_family (
"family_id" character varying(32 char) NOT NULL,
"application_id" character varying(32 char) NULL,
"family_number" character varying(50 char) ,
"household_register_number" character varying(50 char),
"poverty_reason" character varying(32 char),
CONSTRAINT "pk_app_family_idpk" PRIMARY KEY (family_id));
insert into app_family select generate_series(1,10000),generate_series(1,10000),'aaaa','aaa','bbb' from dual ;
INSERT INTO app_family SELECT *  FROM app_family
CREATE TABLE app_family2 AS SELECT *  FROM app_family
UPDATE app_family2 SET application_id ='a' WHERE  family_id:
:int > 5000;
2、查看nestloop join
SET enable_hashjoin TO off ;
SET enable_mergejoin TO OFF;
EXPLAIN ANALYZE select  t.*
from app_family t LEFT JOIN app_family2 p
on  t.family_id=p.application_id;

QUERY PLAN                                                                                                                 |
---------------------------------------------------------------------------------------------------------------------------+
Nested Loop Left Join  (cost=0.00..1500387.00 rows=10000 width=21) (actual time=0.018..11663.381 rows=10000 loops=1)       |
  Join Filter: ((t.family_id)::text = (p.application_id)::text)                                                            |
  Rows Removed by Join Filter: 99995000                                                                                    |
  ->  Seq Scan on app_family t  (cost=0.00..165.00 rows=10000 width=21) (actual time=0.007..10.015 rows=10000 loops=1)     |
  ->  Materialize  (cost=0.00..247.00 rows=10000 width=3) (actual time=0.000..0.405 rows=10000 loops=10000)                |
        ->  Seq Scan on app_family2 p  (cost=0.00..197.00 rows=10000 width=3) (actual time=0.004..1.035 rows=10000 loops=1)|
Planning Time: 0.331 ms                                                                                                    |
Execution Time: 11667.704 ms        
 可以看到由于没有索引, 内部表loop 一次 耗时1ms左右 10000次循环就是1万ms。 
我们创建索引然后再看看执行计划

CREATE INDEX idx_family2 ON app_family2(application_id);
QUERY PLAN                                                                                                                              |
----------------------------------------------------------------------------------------------------------------------------------------+
Nested Loop Left Join  (cost=0.29..3685.00 rows=10000 width=21) (actual time=0.038..35.024 rows=10000 loops=1)                          |
  ->  Seq Scan on app_family t  (cost=0.00..165.00 rows=10000 width=21) (actual time=0.009..1.083 rows=10000 loops=1)                   |
  ->  Index Only Scan using idx_family2 on app_family2 p  (cost=0.29..0.33 rows=2 width=3) (actual time=0.003..0.003 rows=0 loops=10000)|
        Index Cond: (application_id = (t.family_id)::text)                                                                              |
        Heap Fetches: 0                                                                                                                 |
Planning Time: 0.228 ms                                                                                                                 |
Execution Time: 35.682 ms   

创建索引之后用时35ms。有明显提升。

2、收集hash join 执行计划
QUERY PLAN                                                                                                                 |
---------------------------------------------------------------------------------------------------------------------------+
Hash Right Join  (cost=290.00..513.26 rows=10000 width=21) (actual time=1.779..5.117 rows=10000 loops=1)                   |
  Hash Cond: ((p.application_id)::text = (t.family_id)::text)                                                              |
  ->  Seq Scan on app_family2 p  (cost=0.00..197.00 rows=10000 width=3) (actual time=0.013..0.638 rows=10000 loops=1)      |
  ->  Hash  (cost=165.00..165.00 rows=10000 width=21) (actual time=1.743..1.744 rows=10000 loops=1)                        |
        Buckets: 16384  Batches: 1  Memory Usage: 663kB                                                                    |
        ->  Seq Scan on app_family t  (cost=0.00..165.00 rows=10000 width=21) (actual time=0.012..0.602 rows=10000 loops=1)|
Planning Time: 0.105 ms                                                                                                    |
Execution Time: 5.495 ms                                                                                                   |

再这种情况下可以看到hash join 用时5ms。所以hash join 优势还是很明显的。                                                                                                   |                                                                              |

场景2: 大表+小表

1、我们重建app_family,将数据量增加到1000万,然后先收集 nest loop的执行计划。
insert into app_family select generate_series(1,10000000),generate_series(1,10000000),'aaaa','aaa','bbb' from dual ;

QUERY PLAN                                                                                                                                 |
-------------------------------------------------------------------------------------------------------------------------------------------+
Nested Loop Left Join  (cost=0.29..3574032.75 rows=9999565 width=27) (actual time=0.063..33128.886 rows=10000000 loops=1)                  |
  ->  Seq Scan on app_family t  (cost=0.00..174060.65 rows=9999565 width=27) (actual time=0.042..1447.904 rows=10000000 loops=1)           |
  ->  Index Only Scan using idx_family2 on app_family2 p  (cost=0.29..0.32 rows=2 width=3) (actual time=0.003..0.003 rows=0 loops=10000000)|
        Index Cond: (application_id = (t.family_id)::text)                                                                                 |
        Heap Fetches: 0                                                                                                                    |
Planning Time: 0.224 ms                                                                                                                    |
Execution Time: 33724.901 ms      

但是如果我们更换两个表的join顺序
EXPLAIN ANALYZE SELECT   t.*
from app_family2 p LEFT JOIN  app_family t 
on  t.family_id=p.application_id
可以看到小表做驱动表,大表走索引情况下执行效率明显提升用时84ms。    

QUERY PLAN                                                                                                                                |
------------------------------------------------------------------------------------------------------------------------------------------+
Nested Loop Left Join  (cost=0.43..78386.00 rows=10000 width=27) (actual time=0.058..83.462 rows=10000 loops=1)                           |
  ->  Seq Scan on app_family2 p  (cost=0.00..197.00 rows=10000 width=3) (actual time=0.031..1.349 rows=10000 loops=1)                     |
  ->  Index Scan using pk_app_family_idpk on app_family t  (cost=0.43..7.82 rows=1 width=27) (actual time=0.008..0.008 rows=0 loops=10000)|
        Index Cond: ((family_id)::text = (p.application_id)::text)                                                                        |
Planning Time: 0.091 ms                                                                                                                   |
Execution Time: 84.238 ms   

同样的情况下我们看一下hash join 的执行情况

QUERY PLAN                                                                                                                           |
-------------------------------------------------------------------------------------------------------------------------------------+
Hash Right Join  (cost=367412.21..436072.46 rows=9999565 width=27) (actual time=3414.711..6855.453 rows=10000000 loops=1)            |
  Hash Cond: ((p.application_id)::text = (t.family_id)::text)                                                                        |
  ->  Seq Scan on app_family2 p  (cost=0.00..197.00 rows=10000 width=3) (actual time=0.010..0.885 rows=10000 loops=1)                |
  ->  Hash  (cost=174060.65..174060.65 rows=9999565 width=27) (actual time=3413.135..3413.136 rows=10000000 loops=1)                 |
        Buckets: 65536  Batches: 256  Memory Usage: 2826kB                                                                           |
        ->  Seq Scan on app_family t  (cost=0.00..174060.65 rows=9999565 width=27) (actual time=0.018..947.863 rows=10000000 loops=1)|
Planning Time: 0.145 ms                                                                                                              |
Execution Time: 7154.517 ms                                                                                                          |

QUERY PLAN                                                                                                                           |
-------------------------------------------------------------------------------------------------------------------------------------+
Hash Left Join  (cost=367412.21..436072.46 rows=10000 width=27) (actual time=3363.830..4873.281 rows=10000 loops=1)                  |
  Hash Cond: ((p.application_id)::text = (t.family_id)::text)                                                                        |
  ->  Seq Scan on app_family2 p  (cost=0.00..197.00 rows=10000 width=3) (actual time=0.017..0.970 rows=10000 loops=1)                |
  ->  Hash  (cost=174060.65..174060.65 rows=9999565 width=27) (actual time=3362.324..3362.325 rows=10000000 loops=1)                 |
        Buckets: 65536  Batches: 256  Memory Usage: 2826kB                                                                           |
        ->  Seq Scan on app_family t  (cost=0.00..174060.65 rows=9999565 width=27) (actual time=0.011..946.358 rows=10000000 loops=1)|
Planning Time: 0.158 ms                                                                                                              |
Execution Time: 4873.893 ms                                                                                                          |
可以看到hash join 在两种不同join顺序的情况下执行时间有差距,但是不大。 
但是相比较于最优情况下的nest loop join 是有明显差距。
                                                                                                         |                                                                                             |

场景3:两个大表

构造两个1000万的大表,然后对比nest loop 和 hash join
DROP  TABLE  app_family2;
CREATE TABLE app_family2 AS SELECT *  FROM app_family;
UPDATE app_family2 SET application_id ='a' WHERE  family_id::int > 5000;
CREATE INDEX idx_family2 ON app_family2(application_id);

首先看一下nest loop 
QUERY PLAN                                                                                                                                   |
---------------------------------------------------------------------------------------------------------------------------------------------+
Nested Loop Left Join  (cost=0.43..5249466.93 rows=10000000 width=27) (actual time=0.024..37066.301 rows=10000000 loops=1)                   |
  ->  Seq Scan on app_family2 p  (cost=0.00..238135.00 rows=10000000 width=7) (actual time=0.008..1733.272 rows=10000000 loops=1)            |
  ->  Index Scan using pk_app_family_idpk on app_family t  (cost=0.43..0.50 rows=1 width=27) (actual time=0.003..0.003 rows=0 loops=10000000)|
        Index Cond: ((family_id)::text = (p.application_id)::text)                                                                           |
Planning Time: 0.241 ms                                                                                                                      |
Execution Time: 37644.050 ms                
可以看到因为有索引,所以全部的执行时间就是集中在loop 上
和场景1 执行计划基本一样唯一差异的就是loop次数  总计用时37s

然后看一下 hash join

QUERY PLAN                                                                                                                            |
--------------------------------------------------------------------------------------------------------------------------------------+
Hash Left Join  (cost=367412.21..778841.36 rows=10027496 width=27) (actual time=4408.948..9869.481 rows=10000000 loops=1)             |
  Hash Cond: ((p.application_id)::text = (t.family_id)::text)                                                                         |
  ->  Seq Scan on app_family2 p  (cost=0.00..238409.96 rows=10027496 width=2) (actual time=0.039..2061.101 rows=10000000 loops=1)     |
  ->  Hash  (cost=174060.65..174060.65 rows=9999565 width=27) (actual time=4408.762..4408.763 rows=10000000 loops=1)                  |
        Buckets: 65536  Batches: 256  Memory Usage: 2826kB                                                                            |
        ->  Seq Scan on app_family t  (cost=0.00..174060.65 rows=9999565 width=27) (actual time=0.013..1281.885 rows=10000000 loops=1)|
Planning Time: 0.156 ms                                                                                                               |
Execution Time: 10187.486 ms                   

可以看到这种情况下hash join 明显快很多, 他的耗时基本上就是 
hash表生成时间+另一张表扫描时间  。
然后我们在模拟一下work_mem  偏小的情况下 hash join 
SET work_mem=1024 --设置work_mem =1M
QUERY PLAN                                                                                                                            |
--------------------------------------------------------------------------------------------------------------------------------------+
Hash Left Join  (cost=367412.21..778841.36 rows=10027496 width=27) (actual time=4983.689..9894.142 rows=10000000 loops=1)             |
  Hash Cond: ((p.application_id)::text = (t.family_id)::text)                                                                         |
  ->  Seq Scan on app_family2 p  (cost=0.00..238409.96 rows=10027496 width=2) (actual time=0.044..1709.922 rows=10000000 loops=1)     |
  ->  Hash  (cost=174060.65..174060.65 rows=9999565 width=27) (actual time=4983.615..4983.615 rows=10000000 loops=1)                  |
        Buckets: 16384  Batches: 1024  Memory Usage: 703kB                                                                            |
        ->  Seq Scan on app_family t  (cost=0.00..174060.65 rows=9999565 width=27) (actual time=0.008..1278.113 rows=10000000 loops=1)|
Planning Time: 0.165 ms                                                                                                               |
Execution Time: 10204.286 ms                                                                                                          |

可以看到 work_mem 减小后, Buckets 少了, 但是Batches 增加了。
 Buckets 就是用于存放hash值的内存空间,因为work_mem 小了 所以 Buckets 减少。
batchs代表为了执行这个hash join Buckets 被服用了几次。

如果我们增大SET work_mem=1024000 到1G 看一下执行计划
QUERY PLAN                                                                                                                            |
--------------------------------------------------------------------------------------------------------------------------------------+
Hash Left Join  (cost=299055.21..563787.36 rows=10027496 width=27) (actual time=5407.391..8326.606 rows=10000000 loops=1)             |
  Hash Cond: ((p.application_id)::text = (t.family_id)::text)                                                                         |
  ->  Seq Scan on app_family2 p  (cost=0.00..238409.96 rows=10027496 width=2) (actual time=0.040..1301.358 rows=10000000 loops=1)     |
  ->  Hash  (cost=174060.65..174060.65 rows=9999565 width=27) (actual time=5315.162..5315.162 rows=10000000 loops=1)                  |
        Buckets: 16777216  Batches: 1  Memory Usage: 724606kB                                                                         |
        ->  Seq Scan on app_family t  (cost=0.00..174060.65 rows=9999565 width=27) (actual time=0.037..1143.247 rows=10000000 loops=1)|
Planning Time: 0.138 ms                                                                                                               |
Execution Time: 8730.006 ms                                                                                                           |

 可以看到 work_mem 到1G后,内存中   Buckets 增加了很多,但是  Batches 就1次
这说明所有的hash值都存在了内存中, 整体时间耗时8.7s 提升1.3s左右                                                                |                                                                                         |

总结

通过实验我们进一步验证了nest loop 和hash join 各自适应场景和注意事项总结下来如下

1、nest loop join 顺序很重要,驱动表要尽可能晓。 因为loop循环次数直接影响执行时间

2、两个大表关联场景hash join 最优,但是hash join 对work_mem 要求高,尤其是IO比较慢的情况下。

3、一般小表的join,hash join 快,但都是ms级别响应,差别不大。

标签:rows,family,..,HashJoin,app,NestLoop,cost,loops,对比
From: https://www.cnblogs.com/kingbase/p/17798306.html

相关文章

  • 精挑细选:哪款PLM软件最适合您的企业?全面对比10大热门产品
    20款产品生命周期管理(PLM)软件包括:1.PingCode;2.Worktile;3.Productboard;4.Propel;5.ArasPLM;6.CentricRetAIlPLM;7.SiemensTeamcenter;8.BambooRose;9.AndromedaPLMbyNGC;10.ClickUp。产品生命周期管理(PLM)软件是支持产品开发和管理过程的工具。它提供一个数字平台,供......
  • TaskPool 和 Worker 的对比
    作用:TaskPool(任务池)和Worker都为应用程序提供多线程运行环境,用于处理耗时的计算任务或其他密集型任务,避免阻塞主线程,提高系统性能。 实现特点对比:内存模型:TaskPool和Worker都提供线程间隔离,内存不共享。参数传递机制:两者都采用结构化克隆算法进行序列化、反序列化,......
  • Windows、WSL 与 Linux 的性能对比
    Windows、WSL与Linux的性能对比芋道源码我的微信公众号:芋道源码。一起知其然,知其所以然。 105人赞同了该文章 Phoronix网站发布了关于Windows、WSL和Linux的性能基准测试结果。测试的内容包括网络性能、I/O性能、编程语言运行性能和图形......
  • 【4月首发】京东 阿里 腾讯云服务器价格对比表 低至50元/年 幻兽帕鲁 雾锁王国 我的世
    本文纯原创,侵权必究【云服务器推荐】价格对比!阿里云京东云腾讯云选购指南视频截图《最新对比表》已更新在文章头部—腾讯云文档,文章具有时效性,请以腾讯文档为准!【腾讯文档实时更新】2024年-幻兽帕鲁服务器专区最新云服务器对比表https://docs.qq.com/document/DV0RCS0l......
  • m基于OFDM系统的PAPR性能matlab仿真,对比LFDMA,IFDMA,DFDMA
    1.算法仿真效果matlab2022a仿真结果如下:   2.算法涉及理论知识概要       在正交频分复用(OrthogonalFrequencyDivisionMultiplexing,OFDM)系统中,信号的峰值功率相对于其平均功率的比例称为峰均功率比(Peak-to-AveragePowerRatio,PAPR)。由于多个子载波的叠......
  • .net和java串口通讯压力测试对比
    最近由于工作要求,需要对一个串口通讯设备进行压力测试,要求连续持续对串口设备发送指令,无间隔,测试设备是否会死机。要求做到毫秒级,测试第三方的工具,基本上都无法达到毫秒级,最少的也是10毫秒。于是就自己写代码测试。通过.net写,发现最少能达到1毫秒,但是有遗漏,看日志如下2024-03-3......
  • 工厂方法模式与抽象工厂模式的深度对比
    ​......
  • .Net 对象与对象之间的映射转换的6中方式以及性能对比
    我们在.Net开发的过程中,经常会遇到一个实体对象与另一个实体对象之间的映射转换,接下来我们将依次去实现6个对象间映射转换的方式,并对他们进行性能测试,找出其中效率最高的方式。通过对象Copy,通过new一个新的实体对象通过手动赋值的方式实现publicclassObjectCopyMapper{......
  • js对比日期大小
    我们在日常开发过程中,经常会用到JavaScript语言在前端代码中,进行日期的选择,比如开始日期和结束日期的选择,同时我们希望用户在选择日期的时候不要选错日期,比如结束日期早于开始日期,那么从逻辑上数据肯定是错的,所以为了检测用户选择的日期是否正确,将会用到开始日期和结束日期的比......
  • Hybrid-PSC:基于对比学习的混合网络,解决长尾图片分类 | CVPR 2021
     论文提出新颖的混合网络用于解决长尾图片分类问题,该网络由用于图像特征学习的对比学习分支和用于分类器学习的交叉熵分支组成,在训练过程逐步将训练权重调整至分类器学习,达到更好的特征得出更好的分类器的思想。另外,为了节省内存消耗,论文提出原型有监督对比学习。从实验结果来看......