Oracle 表的连接方式(2)-----HASH JOIN的基本机制1

  们对hash join的常见误解,一般包括两个:

  第一个误解:是我们经常以为hash join需要对两个做join的表都做全表扫描

  第二个误解:是经常以为hash join会选择比较小的表做build table

纠正第一个误解:

  我们经常以为hash join需要对两个做join的表都做全表扫描,但实际情况HASH JOIN是不会限制SQL的访问方法的。我们用下面的测试来验证:

--创建测试表probe_tab:
SQL> create table probe_tab
  2  initrans 3
  3  nologging
  4  as
  5  with generator as (
  6   select 
  7   rownum id
  8   from all_objects 
  9   where rownum <= 3000
 10  )
 11  select
 12   10000 + rownum id,
 13   trunc(dbms_random.value(0,5000)) n1,
 14   rpad(rownum,20) probe_vc,
 15   rpad(x,500) probe_padding
 16  from
 17   generator v1,
 18   generator v2
 19  where
 20   rownum <= 5000
 21  ;

Table created.

--创建主键
SQL> alter table probe_tab add constraint pb_pk primary key(id);

Table altered.

--创建测试表build_tab:
SQL> create table build_tab
  2  initrans 3
  3  nologging
  4  as
  5  with generator as (
  6   select 
  7   rownum id
  8   from all_objects 
  9   where rownum <= 3000
 10  )
 11  select
 12   rownum id,
 13   10001 + trunc(dbms_random.value(0,5000)) id_probe,
 14   rpad(rownum,20) build_vc,
 15   rpad(x,500) build_padding
 16  from
 17   generator v1,
 18   generator v2
 19  where
 20   rownum <= 5000
 21  ;

Table created.

--创建主键
SQL> alter table build_tab add constraint bu_pk primary key(id);


--创建外键
SQL> alter table build_tab add constraint bu_fk_pb foreign key (id_probe) references probe_tab;

--创建索引
SQL> create index bu_fk_pb on build_tab(id_probe);


--收集两个表的统计信息
SQL> begin
  2   dbms_stats.gather_table_stats(
  3   SYS,
  4   build_tab,
  5   cascade => true,
  6   estimate_percent => null,
  7   method_opt => for all columns size 1
  8   );
  9  end;
 10  /

SQL> begin
  2   dbms_stats.gather_table_stats(
  3   SYS,
  4   probe_tab,
  5   cascade => true,
  6   estimate_percent => null,
  7   method_opt => for all columns size 1
  8   );
  9  end;
 10  /


--我们使用workarea_size_policy = manual并且设置hash_area_size = 1048576,
--因为hash join的效率和我们分配给它的内存多少关系很大,
--所以为了能够精确的控制分配的内存数量,我们采用了手工的分配方式
SQL> begin
  2  
  3   begin execute immediate alter session set workarea_size_policy = manual;
  4   exception when others then null;
  5   end;
  6  
  7   begin execute immediate alter session set hash_area_size = 1048576;
  8   exception when others then null;
  9   end;
 10  
 11  end;
 12  /

PL/SQL procedure successfully completed.

SQL> set autotrace traceonly explain
SQL> set line 200
SQL> select
  2   bu.build_vc,
  3   pb.probe_vc,
  4   pb.probe_padding
  5  from
  6   build_tab bu,
  7   probe_tab pb
  8  where
  9   bu.id between 1 and 500
 10  and pb.id = bu.id_probe
 11  ;

Execution Plan
----------------------------------------------------------
Plan hash value: 2915561138

------------------------------------------------------------------------------------------
| Id  | Operation                    | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |           |   500 |   271K|   149   (1)| 00:00:02 |
|*  1 |  HASH JOIN                   |           |   500 |   271K|   149   (1)| 00:00:02 |
|   2 |   TABLE ACCESS BY INDEX ROWID| BUILD_TAB |   500 | 15000 |    42   (0)| 00:00:01 |
|*  3 |    INDEX RANGE SCAN          | BU_PK     |   500 |       |     3   (0)| 00:00:01 |
|   4 |   TABLE ACCESS FULL          | PROBE_TAB |  5000 |  2573K|   106   (0)| 00:00:02 |
------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - access("PB"."ID"="BU"."ID_PROBE")
   3 - access("BU"."ID">=1 AND "BU"."ID"<=500)

SQL> 

  结论:根据计划的第3步(index range scan),可以确定hash join并不会限制SQL的访问方法。

      注:在执行计划中靠近HASH JOIN的表为build table(内表)

纠正第二误解:

  我们通常以为hash join会选择比较小的表做build table,但看看建表语句,这两个表其实是一样大的。所以并不是选择比较小的表,而是看哪个表上得到的结果集比较小,就把哪个表作为build table。进一步说,Oracle是如何比较哪个表上的结果集比较小呢?为了说明这一点,我们需要把原来的SQL拆分成2部分:

--第一部分sql
select 
    bu.id,
    bu.build_vc,
    bu.id_probe
from build_tab bu
where    bu.id between 1 and 500    ;

--第二部分sql
select 
    pb.probe_vc,
    pb.probe_padding,
    pb.id
from probe_tab pb;

    Oracle会根据这两个虚拟的查询的返回结果决定到底哪个表的返回结果集表较小

  这个结果集的大小则是 = 结果集的行数*user_tab_columns.sum(avg_col_len)计算得出的

  需要注意的是:一般我们收集统计数据的方式都是dbms_stats,有的时候由于历史原因我们可能还在使用analyze,当在计算 avg_col_len的时候,我们会发现dbms_stats计算出来的avg_col_len一般要比analyze计算的要大1,这是因为我们表里 的列除了数据占用空间,列本身也是需要空间的,当计算avg_col_len的时候dbms_stats注意到了这一点,而analyze忽略了这一点。 这可能导致同样的SQL在dbms_stats分析的系统上的执行计划,和analyze分析的系统上的执行计划不一样。

先查询两个表的user_tab_columns字节数:

SQL> select column_name, avg_col_len from user_tab_columns where table_name=PROBE_TAB;

COLUMN_NAME                    AVG_COL_LEN
------------------------------ -----------
ID                                       5
N1                                       4
PROBE_VC                                21
PROBE_PADDING                          501

SQL> select column_name, avg_col_len from user_tab_columns where table_name=BUILD_TAB;

COLUMN_NAME                    AVG_COL_LEN
------------------------------ -----------
ID                                       4
ID_PROBE                                 5
BUILD_VC                                21
BUILD_PADDING                          501

SQL>

  根据拆分后的sql 得到的结果集:

  表probe_tab的结果集是(5+21+521)*5000=2635000

  表build_tab的结果集是(4+5+21)*500=15000

  两个表的大小是相同的,但build_tab的结果集远远小于probe_tab的结果集,所以用表build_tab作为hash内表

作为验证,我们把原来查询的select list做一下修改,即改变查询的结果集:

--更改后的sql
select
 bu.build_vc,
 pb.probe_vc,
 bu.build_padding
from
 build_tab bu,
 probe_tab pb
where
 bu.id between 1 and 500
and pb.id = bu.id_probe;


--拆分后sql1:
select 
    bu.build_vc,
    bu.build_padding,
    bu.id,bu.
    id_probe
from build_tab bu
where bu.id between 1 and 500

--拆分后的sql2:
select 
    pb.probe_vc,
    pb.id
from probe_tab pb

根据拆分后的sql 得到的结果集:

probe_tab的结果集是(21+4)*5000=125000,

build_tab的结果集是(501+21+4+5)*500=265500,

根据结果集推测probe_tab为hash内表,验证结果probe_tab为HASH内表

SQL> select
  2   bu.build_vc,
  3   pb.probe_vc,
  4   bu.build_padding
  5  from
  6   build_tab bu,
  7   probe_tab pb
  8  where
  9   bu.id between 1 and 500
 10  and pb.id = bu.id_probe;

Execution Plan
----------------------------------------------------------
Plan hash value: 861942995

------------------------------------------------------------------------------------------
| Id  | Operation                    | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |           |   500 |   271K|   149   (1)| 00:00:02 |
|*  1 |  HASH JOIN                   |           |   500 |   271K|   149   (1)| 00:00:02 |
|   2 |   TABLE ACCESS FULL          | PROBE_TAB |  5000 |   126K|   106   (0)| 00:00:02 |
|   3 |   TABLE ACCESS BY INDEX ROWID| BUILD_TAB |   500 |   259K|    42   (0)| 00:00:01 |
|*  4 |    INDEX RANGE SCAN          | BU_PK     |   500 |       |     3   (0)| 00:00:01 |
------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - access("PB"."ID"="BU"."ID_PROBE")
   4 - access("BU"."ID">=1 AND "BU"."ID"<=500)

SQL>

 

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。