现有如下 SQL。

    1. select * from a,b where a.object_id=b.object_id;

    表 a 有 4GB,表 b 有 6GB,两表关联后返回大量数据,应该走 HASH 连接。因为 a 比 b 小,所以 a 表应该作为 HASH JOIN 的驱动表。驱动表 a 有 4GB,需要放入 PGA 中。因为 PGA 中 work area 不能超过 2G,所以 PGA 不能完全容纳下驱动表,这时有部分数据会溢出到磁盘(TEMP)进行 on-disk hash join。我们可以开启并行查询加快查询速度。

    超大表与超大表在进行并行 HASH 连接的时候,需要将两个表根据连接列进行 HASH 运算,然后将运算结果放到 PGA 中,再进行 HASH 连接,这种并行 HASH 连接就叫作并行 HASH HASH 连接。假设对上面 SQL 启用 6 个并行查询,a 表会根据连接列进行 HASH 运算然后拆分为 6 份,记为 a1,a2,a3,a4,a5,a6,b 表也会根据连接列进行 HASH 运算然后拆分为 6 份,记为 b1,b2,b3,b4,b5,b6。那么以上 SQL 开启并行就相当于被改写成如下 SQL。

    1. select * from a1b1 where a1.object_id=b1.object_id ---并行进行
    2. union all
    3. select * from a2b2 where a2.object_id=b2.object_id ---并行进行
    4. union all
    5. select * from a3b3 where a3.object_id=b3.object_id ---并行进行
    6. union all
    7. select * from a4b4 where a4.object_id=b4.object_id ---并行进行
    8. union all
    9. select * from a5b5 where a5.object_id=b5.object_id ---并行进行
    10. union all
    11. select * from a6b6 where a6.object_id=b6.object_id ---并行进行

    对于上面 SQL,开启并行查询就能避免 on-disk hash join,因为表不是特别大,而且被拆分到内存中了。怎么写 HINT 实现并行 HASH HASH 呢?我们需要添 hint:**pq_distribute(被驱动表 hash,hash)**

    现在我们来查看并行 HASH HASH 的执行计划(为了方便排版,执行计划中省略了部分数据)。

    1. explain plan for select
    2. /*+ parallel(6) use_hash(a,b) pq_distribute(b hash,hash) */
    3. *
    4. from a, b
    5. where a.object_id = b.object_id;
    1. Explained.
    1. select * from table(dbms_xplan.display);
    1. PLAN_TABLE_OUTPUT
    2. -------------------------------------------------------------------------------------
    3. Plan hash value: 728916813
    4. -------------------------------------------------------------------------------------
    5. | Id | Operation | Name | Rows | Bytes |TempSpc|IN-OUT|PQ Distrib|
    6. -------------------------------------------------------------------------------------
    7. | 0 | SELECT STATEMENT | | 3046M| 1174G| | | |
    8. | 1 | PX COORDINATOR | | | | | | |
    9. | 2 | PX SEND QC (RANDOM) | :TQ10002 | 3046M| 1174G| | P->S |QC (RAND) |
    10. |* 3 | HASH JOIN BUFFERED | | 3046M| 1174G| 324M| PCWP | |
    11. | 4 | PX RECEIVE | | 9323K| 1840M| | PCWP | |
    12. | 5 | PX SEND HASH | :TQ10000 | 9323K| 1840M| | P->P |HASH |
    13. | 6 | PX BLOCK ITERATOR | | 9323K| 1840M| | PCWC | |
    14. | 7 | TABLE ACCESS FULL| A | 9323K| 1840M| | PCWP | |
    15. | 8 | PX RECEIVE | | 20M| 4045M| | PCWP | |
    16. | 9 | PX SEND HASH | :TQ10001 | 20M| 4045M| | P->P |HASH |
    17. | 10 | PX BLOCK ITERATOR | | 20M| 4045M| | PCWC | |
    18. | 11 | TABLE ACCESS FULL| B | 20M| 4045M| | PCWP | |
    19. -------------------------------------------------------------------------------------
    20. Predicate Information (identified by operation id):
    21. ---------------------------------------------------
    22. 3 - access("A"."OBJECT_ID"="B"."OBJECT_ID")

    两表如果进行的是并行 HASH HASH 关联,执行计划 Operation 会出现 PX SEND HASH 关键字PQ Distrib 会出现 HASH 关键字。

    如果表 a 有 20G,表 b 有 30G,即使采用并行 HASH HASH 连接也很难跑出结果,因为要把两个表先映射到 PGA 中,这需要耗费一部分 PGA,之后在进行 HASH JOIN 的时候也需要部分 PGA,此时 PGA 根本就不够用,如果我们查看等待事件,会发现进程一直在做 DIRECT PATH READ/WRITE TEMP

    如何解决超级大表(几十 GB)与超级大表(几十 GB)关联的性能问题呢?我们可以根据并行 HASH HASH 关联的思路,人工实现并行 HASH HASH。下面就是人工实现并行 HASH HASH 的过程。

    现在我们创建新表 p1,在表 a 的结构上添加一个字段 HASH_VALUE,同时根据 HASH_VALUE 进行 LIST 分区。

    1. CREATE TABLE P1(
    2. HASH_VALUE NUMBER,
    3. OWNER VARCHAR2(30),
    4. OBJECT_NAME VARCHAR2(128),
    5. SUBOBJECT_NAME VARCHAR2(30),
    6. OBJECT_ID NUMBER,
    7. DATA_OBJECT_ID NUMBER,
    8. OBJECT_TYPE VARCHAR2(19),
    9. CREATED DATE,
    10. LAST_DDL_TIME DATE,
    11. TIMESTAMP VARCHAR2(19),
    12. STATUS VARCHAR2(7),
    13. TEMPORARY VARCHAR2(1),
    14. GENERATED VARCHAR2(1),
    15. SECONDARY VARCHAR2(1),
    16. NAMESPACE NUMBER,
    17. EDITION_NAME VARCHAR2(30)
    18. )
    19. PARTITION BY list(HASH_VALUE)
    20. (
    21. partition p0 values (0),
    22. partition p1 values (1),
    23. partition p2 values (2),
    24. partition p3 values (3),
    25. partition p4 values (4)
    26. );
    1. Table created.

    然后我们创建新表 p2,在表 b 的结构上添加一个字段 HASH_VALUE,同时根据 HASH_VALUE 进行 LIST 分区。

    1. CREATE TABLE P2(
    2. HASH_VALUE NUMBER,
    3. OWNER VARCHAR2(30),
    4. OBJECT_NAME VARCHAR2(128),
    5. SUBOBJECT_NAME VARCHAR2(30),
    6. OBJECT_ID NUMBER,
    7. DATA_OBJECT_ID NUMBER,
    8. OBJECT_TYPE VARCHAR2(19),
    9. CREATED DATE,
    10. LAST_DDL_TIME DATE,
    11. TIMESTAMP VARCHAR2(19),
    12. STATUS VARCHAR2(7),
    13. TEMPORARY VARCHAR2(1),
    14. GENERATED VARCHAR2(1),
    15. SECONDARY VARCHAR2(1),
    16. NAMESPACE NUMBER,
    17. EDITION_NAME VARCHAR2(30)
    18. )
    19. PARTITION BY list(HASH_VALUE)
    20. (
    21. partition p0 values (0),
    22. partition p1 values (1),
    23. partition p2 values (2),
    24. partition p3 values (3),
    25. partition p4 values (4)
    26. );
    1. Table created.

    请注意,两个表分区必须一模一样,如果分区不一样,就有数据无法关联上

    我们将 a 表的数据迁移到新表 p1 中。

    1. insert into p1
    2. select ora_hashobject_id, 4), a.* from a ---注意排除 object_id null 的数据
    3. commit

    然后我们将 b 表的数据迁移到新表 p2 中。

    1. insert into p2
    2. select ora_hashobject_id, 4), b.* from b ---注意排除 object_id null 的数据
    3. commit

    下面 SQL 就是并行 HASH HASH 关联的人工实现。

    1. select *
    2. from p1, p2
    3. where p1.object_id = p2.object_id
    4. and p1.hash_value = 0
    5. and p2.hash_value = 0;
    6. select *
    7. from p1, p2
    8. where p1.object_id = p2.object_id
    9. and p1.hash_value = 1
    10. and p2.hash_value = 1;
    11. select *
    12. from p1, p2
    13. where p1.object_id = p2.object_id
    14. and p1.hash_value = 2
    15. and p2.hash_value = 2;
    16. select *
    17. from p1, p2
    18. where p1.object_id = p2.object_id
    19. and p1.hash_value = 3
    20. and p2.hash_value = 3;
    21. select *
    22. from p1, p2
    23. where p1.object_id = p2.object_id
    24. and p1.hash_value = 4
    25. and p2.hash_value = 4;

    此方法运用了 ora_hash 函数。Oracle 中的 HASH 分区就是利用的 ora_hash 函数

    ora_hash 使用方法如下。

    ora_hash(列,HASH 桶),HASH 桶默认是 4 294 967 295,可以设置 0~4 294 967 295。

    ora_hash(object_id,4)会把 object_id 的值进行 HASH 运算,然后放到 0、1、2、3、4 这些桶里面,也就是说ora_hash(object_id,4)只会产生 0、1、2、3、4 这几个值。

    将大表(a,b)拆分为分区表(p1,p2)之后,我们只需要依次关联对应的分区,这样就不会出现 PGA 不足的问题,从而解决了超级大表关联查询的效率问题。在实际生产环境中,需要添加多少分区,请自己判断