oralce之 10046对Hash Join分析

通过将OR条件替换为UNION,解决了因哈希表桶中行数过多导致的SQL执行效率低下问题,执行时间从几小时缩短到16分钟。

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

前两天解决了一个优化SQL的case,SQL语句如下,big_table为150G大小,small_table很小,9000多条记录,不到1M大小,hash_area_size, sort_area_size均设置足够大,可以进行optimal hash join和memory sort。

1
2
3
4
5
6
select /*+ leading(b) use_hash(a b) */ distinct a.ID
from BIG_TABLE a, SMALL_TABLE b
where (a.category  = b.from_cat or
       a.category2 = b.from_cat) and
       a.site_id  = b.site_id and
       a.sale_end >= sysdate;

执行计划如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
--------------------------------------------------------------------------
| Id  | Operation            |  Name        | Rows  | Bytes | Cost (%CPU)|
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |              |     2 |   174 |    18  (17)|
|   1 |  SORT UNIQUE         |              |     2 |   174 |    18  (17)|
|*  2 |   HASH JOIN          |              |     2 |   174 |    17  (12)|
|   3 |    TABLE ACCESS FULL | SMALL_TABLE  |  1879 | 48854 |    14   (8)|
|*  4 |    TABLE ACCESS FULL | BIG_TABLE    |     4 |   244 |     3  (34)|
--------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("A"."SITE_ID"="B"."SITE_ID")
       filter("A"."CATEGORY"="B"."FROM_CAT" OR
              "A"."CATEGORY2"="B"."FROM_CAT")
   4 - filter("A"."SALE_END">=SYSDATE@!)

粗略来看,PLAN非常的完美,SQL HINT写的也很到位,小表在内build hash table,大表在外进行probe操作,根据经验来看,整个SQL执行的时间应该和FTS(Full Table Scan) BIG_TABLE的时间差不多。

但是FTS BIG_TABLE的时间大约是8分钟,而真个SQL执行的时间长达3~4小时。

那么问题究竟出在哪里?

FTS时间应该不会有太大变化,那么问题应该在hash join,设置event来trace一下hash join的过程:

1
alter session set events '10104 trace name context forever, level 2';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
### Hash table ###
# NOTE: The calculated number of rows in non-empty buckets may be smaller
#       than the true number.
Number of buckets with   0 rows:      16373
Number of buckets with   1 rows:          0
Number of buckets with   2 rows:          0
Number of buckets with   3 rows:          1
Number of buckets with   4 rows:          0
Number of buckets with   5 rows:          0
Number of buckets with   6 rows:          0
Number of buckets with   7 rows:          1
Number of buckets with   8 rows:          0
Number of buckets with   9 rows:          0
Number of buckets with between  10 and  19 rows:          1
Number of buckets with between  20 and  29 rows:          1
Number of buckets with between  30 and  39 rows:          3
Number of buckets with between  40 and  49 rows:          0
Number of buckets with between  50 and  59 rows:          0
Number of buckets with between  60 and  69 rows:          0
Number of buckets with between  70 and  79 rows:          0
Number of buckets with between  80 and  89 rows:          0
Number of buckets with between  90 and  99 rows:          0
Number of buckets with 100 or more rows:          4
### Hash table overall statistics ###
Total buckets: 16384 Empty buckets: 16373 Non-empty buckets: 11
Total number of rows: 9232
Maximum number of rows in a bucket: 2531
Average number of rows in non-empty buckets: 839.272705

仔细看,在一个bucket中最多的行数竟然有2531行,因为bucket中是一个链表的结构,所以这几千行都是串在一个链表上。 
由这一点想到这个Hash Table所依赖的hash key的distinct value可能太少,重复值太多。否则不应该会有这么多行在同一个bucket里面。

因为Join条件里面有两个列from_cat和site_id,穷举法有三种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SQL> select site_id,from_cat,count(*) from SMALL_TABLE group by site_id,from_cat having count(*)>100;
 
no rows selected
 
2. Build hash table based on (from_cat):
 
SQL> select from_cat,count(*) from SMALL_TABLE group by from_cat having count(*)>100;
 
no rows selected
 
3. Build hash table based on (site_id):
 
SQL> select site_id,count(*) from SMALL_TABLE group by site_id having count(*)>100;
 
   SITE_ID   COUNT(*)
---------- ----------
         0       2531
         2       2527
       146       1490
       210       2526

到这里可以发现,基于site_id这种情况和trace file中这两行很相符:

1
2
Number of buckets with 100 or more rows: 4
Maximum number of rows in a bucket: 2531

注:这判断过程可以从执行计划的“Predicate Information”部分看出:

1
access("A"."SITE_ID"="B"."SITE_ID")

所以推断这个hash table是基于site_id而建的,而Big_Table中大量的行site_id=0,都落在这个linked list最长的bucket中,而大部分行都会扫描完整个链表而最后被丢弃掉,所以这个Hash Join的操作效率非常差,几乎变为了Nest Loop操作。

找到了根本原因,问题也就迎刃而解了。

理想状况下,hash table应当建立于(site_id,from_cat)上,那么问题肯定出在这个OR上,把OR用UNION改写:

1
2
3
4
5
6
7
8
9
10
11
select /*+ leading(b) use_hash(a b) */ distinct a.ID
from BIG_TABLE a, SMALL_TABLE b
where  a.category  = b.from_cat and
       a.site_id  = b.site_id and
       a.sale_end >= sysdate
UNION
select /*+ leading(b) use_hash(a b) */ distinct a.ID
from BIG_TABLE a, SMALL_TABLE b
where  a.category2 = b.from_cat and
       a.site_id  = b.site_id and
       a.sale_end >= sysdate;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
--------------------------------------------------------------------------
| Id  | Operation            |  Name        | Rows  | Bytes | Cost (%CPU)|
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |              |     2 |   148 |    36  (59)|
|   1 |  SORT UNIQUE         |              |     2 |   148 |    36  (59)|
|   2 |   UNION-ALL          |              |       |       |            |
|*  3 |    HASH JOIN         |              |     1 |    74 |    17  (12)|
|   4 |     TABLE ACCESS FULL| SMALL_TABLE  |  1879 | 48854 |    14   (8)|
|*  5 |     TABLE ACCESS FULL| BIG_TABLE    |     4 |   192 |     3  (34)|
|*  6 |    HASH JOIN         |              |     1 |    74 |    17  (12)|
|   7 |     TABLE ACCESS FULL| SMALL_TABLE  |  1879 | 48854 |    14   (8)|
|*  8 |     TABLE ACCESS FULL| BIG_TABLE    |     4 |   192 |     3  (34)|
--------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
   3 - access("A"."CATEGORY"="B"."FROM_CAT" AND
              "A"."SITE_ID"="B"."SITE_ID")
   5 - filter("A"."SALE_END">=SYSDATE@!)
   6 - access("A"."CATEGORY2"="B"."FROM_CAT" AND
              "A"."SITE_ID"="B"."SITE_ID")
   8 - filter("A"."SALE_END">=SYSDATE@!)

初看这个PLAN好像不如第一个PLAN,因为执行了两次BIG_TABLE的FTS,但是让我们在来看看HASH TABLE的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
### Hash table ###
# NOTE: The calculated number of rows in non-empty buckets may be smaller
#       than the true number.
Number of buckets with   0 rows:       9306
Number of buckets with   1 rows:       5310
Number of buckets with   2 rows:       1436
Number of buckets with   3 rows:        285
Number of buckets with   4 rows:         43
Number of buckets with   5 rows:          4
Number of buckets with   6 rows:          0
Number of buckets with   7 rows:          0
Number of buckets with   8 rows:          0
Number of buckets with   9 rows:          0
Number of buckets with between  10 and  19 rows:          0
Number of buckets with between  20 and  29 rows:          0
Number of buckets with between  30 and  39 rows:          0
Number of buckets with between  40 and  49 rows:          0
Number of buckets with between  50 and  59 rows:          0
Number of buckets with between  60 and  69 rows:          0
Number of buckets with between  70 and  79 rows:          0
Number of buckets with between  80 and  89 rows:          0
Number of buckets with between  90 and  99 rows:          0
Number of buckets with 100 or more rows:          0
### Hash table overall statistics ###
Total buckets: 16384 Empty buckets: 9306 Non-empty buckets: 7078
Total number of rows: 9232
Maximum number of rows in a bucket: 5
Average number of rows in non-empty buckets: 1.304323

这就是我们所需要的Hash Table,最长的链表只有五行数据。

整个SQL的执行时间从三四个小时缩短为16分钟,大大超出了developer的预期。

这个SQL单纯从PLAN上很难看出问题所在,需要了解Hash Join的机制,进行更深一步的分析。

source:http://www.itpub.net/thread-955209-1-1.html

转载于:https://www.cnblogs.com/andy6/p/7502059.html

### 锁相环原理 锁相环PLL, Phase-Locked Loop)是一种反馈控制电路,其核心功能在于使输出信号的频率和相位与输入参考信号同步。该电路广泛应用于通信、电子设备以及计算机领域,在这些场景下起到至关重要的作用[^2]。 #### 构成部分及其工作方式 锁相环主要由三个组件构成: - **鉴相器**:负责检测两路信号之间的相位差异,并据此产生误差电压。 - **低通滤波器**:平滑来自鉴相器的脉冲序列,转换为连续变化的直流电平,从而去除高频噪声成分。 - **压控振荡器 (VCO)**:接收经过滤波后的电压作为调制信号,调整自身的震荡频率以匹配目标值。 当系统启动时,如果VCO初始频率\(W_o\)不同于期望的目标频率\(W_i\), 则会经历一个被称为“捕捉”的动态调节阶段;在此期间,通过不断修正直到两者趋于一致并最终达到稳定的锁定状态。此时,即使外界条件有所波动,只要变动幅度不大,PLL仍能保持良好的跟随特性,即所谓的跟踪性能[^5]。 ### 实现倍频的方法 为了实现倍频操作,可以在PLL架构内加入额外的功能单元——分频计数器。具体做法是在反馈路径中插入这样一个元件,使得送回至鉴相器端口处的实际比较对象成为原有时钟周期的一个整数倍版本。如此一来,尽管表面上看似乎只是改变了反馈比例关系而已,但实际上却巧妙地实现了对原始输入频率的有效放大效果[^1]。 ```cpp // C++伪代码展示简单的PLL倍频逻辑框架 class PLL { public: void setMultiplier(int multiplier); private: int _multiplier; // 设置倍频系数 double adjustFrequency(double inputFreq){ return inputFreq * _multiplier; } }; ``` ### 应用实例 除了上述提到的基础用途外,PLL还拥有众多其他应用场景,比如但不限于: - 提升本地振荡源稳定性; - 执行复杂的频率合成功能; - 支持高速数据传输链路上的数据恢复机制等。 值得注意的是,虽然大多数情况下PLL表现良好,但在某些极端条件下可能会出现失锁现象,这通常是因为输入信号质量太差或是设计参数不合理所致[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值