本文所述技术,适用于 Hash Distinct 下压处理,Hash GroupBy 下压处理
基础技术
create table tbl1 (c1 int, c2 int) partition by hash(c2) partitions 3;
select /*+ parallel(3) use_hash_distinct */ distinct c1 from tbl1
下面讲解分布式场景下如何实现高效的 distinct 计算。
最简单的计划是各个机器上做初步的去重,然后将数据收集到中心节点做最终的去重
OceanBase(admin@test)>explain select /*+ parallel(3) use_hash_distinct */ distinct c1 from tbl1\G
Query Plan:
==========================================================
|ID|OPERATOR |NAME |EST. ROWS|COST|
----------------------------------------------------------
|0 |HASH DISTINCT | |1 |46 |
|1 | PX COORDINATOR | |1 |46 |
|2 | EXCHANGE OUT DISTR |:EX10001|1 |46 |
|3 | HASH DISTINCT | |1 |46 |
|4 | PX BLOCK ITERATOR | |1 |46 |
|5 | TABLE SCAN |tbl1 |1 |46 |
==========================================================
考虑到中心节点可能会收到大量初步去重后的数据,导致中心节点压力过大,引入两阶段 Hash Distinct 算法。
首先,各个机器上做独立的去重,然后对去重后的结果做 HASH 重分区,分发到多台机器上做第二阶段的去重。各个机器得到的去重结果就是最终结果,汇聚到中心节点直接输出即可。
OceanBase(admin@test)>explain select /*+ parallel(3) use_hash_distinct */ distinct c1 from tbl1\G
Query Plan:
==========================================================
|ID|OPERATOR |NAME |EST. ROWS|COST|
----------------------------------------------------------
|0 |PX COORDINATOR | |1 |46 |
|1 | EXCHANGE OUT DISTR |:EX10001|1 |46 |
|2 | HASH DISTINCT | |1 |46 |
|3 | EXCHANGE IN DISTR | |1 |46 |
|4 | EXCHANGE OUT DISTR (HASH)|:EX10000|1 |46 |
|5 | HASH DISTINCT | |1 |46 |
|6 | PX BLOCK ITERATOR | |1 |46 |
|7 | TABLE SCAN |tbl1 |1 |46 |
==========================================================
自适应技术
上面的两种方式足以应对大部分场景。为了进一步优化一种场景场景,引入自适应技术。
考虑场景:优化器已经生成了两阶段 Hash Distinct 算法,但各个节点上的 c1 分布非常离散,使得第一阶段 Hash Distinct 基本都是在做无用功。如果 c1 的值非常离散,理想情况下 5 号 Hash Distinct 算子应该什么都不做,直接 bypass 数据,让它去 2 号 Hash Distinct 算子里计算最终结果。
更复杂的场景:优化器已经生成了两阶段 Hash Distinct 算法,但各个节点上的 c1 的分区再数据流上呈现出时而离散,时而紧凑的特征。这时 5 号算子应该根据数据流的特征,有时做 distinct 计算,有时直接 bypass 数据。
对于上面的场景,都可以用自适应的技术来解决。
- 针对数据流中的数据建立 hash 表,并统计建立 hash 表过程中的命中率
- 当 hash 表的大小达到 L1 个字节时判定:
- 如果历史命中率大于 95%,那么继续扩大 hash 表
- 如果历史命中率小于 95%,那么废弃 hash 表
- 当 hash 表的大小达到 L2 个字节时判定:
- 如果历史命中率大于 95%,那么继续扩大 hash 表
- 如果历史命中率小于 95%,那么废弃 hash 表
- 当 hash 表的大小达到 L3 个字节时判定:
- 如果历史命中率大于 95%,那么新来的行要么命中 hash 表,要么直接 bypass 到 2 号Hash Distinct 算子去
- 如果历史命中率小于 95%,那么废弃 hash 表
讨论
- L1、L2、L3 的选取,可以基于 CPU 的 L1 缓存大小、L2缓存大小、L3缓存大小来决定。
95%
这个值只是举例子,在实际的系统里可以通过测试得到。 - 可以不是 3 层,可以是 2 层,比如 L2、L3 (因为 L1 Cache 实在太小了)
- 很多时候,决策的最优方向是尽量快速启动 Bypass,而不是提高命中率(反常识吧?)
其它改进
对于高频值,在每次淘汰 Hash Table Cache 时可予以保留。详见参考文献一节。
参考文献
《Adaptive and Big Data Scale Parallel Execution in Oracle》 第2节 GroupBy