代码改变世界

Partition-wise join

2019-10-10 13:22  abce  阅读(1346)  评论(0编辑  收藏  举报

什么是“partition-wise join”呢?我们将用一个比喻来解释它的好处。

假设两个人,Logan和Shannon,决定住在一起。如果他们每个人都已经有了自己的住所,他们就会拥有很多你在任何家庭都能找到的普通物品。所以他们要做一个决定——是每样东西都保留两件,还是对它们的共同点进行“筛选”。在这个假想的场景中,我们将关注浴室和厨房中的家庭用品。Logan拿起一套厨刀和一个刀板,打电话问Shannon:“嘿,Shannon,你已经有一个刀架了吗?”

你觉得Shannon会怎么做?在整栋房子里找一个现有的刀板?当然不是。如果有刀板,那么它唯一的位置就是厨房。事实上,当Shannon和Logan在整所房子里匹配物品时,他们会把调查范围限制在对有问题的物品有意义的房间里。这是常识——为什么会有人在浴室里找(比如说)叉子和勺子呢?那只是白费力气。(编者注:任何有小孩的人当然会对这个比喻提出异议,他们会非常正确地指出,你可能在每个房间里都能找到所有可能的家居用品,可能在外面也能找到,但为了讨论的方便,我们将省略这种可能性)


这正是Partition-Wise Join使我们能够在数据库中做到的。如果两个表分区具有相同的定义,我们在分区键上进行join,那么定义保证对于表中位于分区P、分区键为k的行,我们只需要在要连接的表中的相同的分区中找对应的行(相同是基于分区定义的)。这种划分相当于“只在厨房里搜索,而不是在浴室”。在执行这样的连接时,我们可以通过执行计划看到这一点。让我们创建两个分区定义相等的表,然后在分区键上连接。

SQL> --
SQL> -- Example 1
SQL> --
SQL>
SQL> drop table t1 purge;

Table dropped.

SQL> drop table t2 purge;

Table dropped.

SQL>
SQL> create table t1 ( x int, y int )
  2  partition by range ( x )
  3  (
  4  partition p_kitchen values less than (10000),
  5  partition p_bathroom values less than (20000),
  6  partition p_dining values less than (30000)
  7  );

Table created.

SQL>
SQL>
SQL> create table t2 ( x int, y int )
  2  partition by range ( x )
  3  (
  4  partition p_kitchen values less than (10000),
  5  partition p_bathroom values less than (20000),
  6  partition p_dining values less than (30000)
  7  );

Table created.

SQL>
SQL>
SQL> insert into t1 select rownum, rownum from dual connect by level < 30000;

29999 rows created.

SQL> insert into t2 select * from t1;

29999 rows created.

SQL> commit;

Commit complete.

SQL> exec dbms_stats.gather_table_stats('','t1')

PL/SQL procedure successfully completed.

SQL> exec dbms_stats.gather_table_stats('','t2')

PL/SQL procedure successfully completed.

SQL>
SQL> --
SQL> set autotrace traceonly explain
SQL> select count(t1.y), count(t2.y)
  2  from t1,t2
  3  where t1.x = t2.x;

Execution Plan
----------------------------------------------------------
Plan hash value: 3155849676

---------------------------------------------------------------------------------------------
| Id  | Operation            | Name | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
---------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |      |     1 |    20 |  1641   (1)| 00:00:01 |       |       |
|   1 |  SORT AGGREGATE      |      |     1 |    20 |            |          |       |       |
|   2 |   PARTITION RANGE ALL|      | 29999 |   585K|  1641   (1)| 00:00:01 |     1 |     3 |
|*  3 |    HASH JOIN         |      | 29999 |   585K|  1641   (1)| 00:00:01 |       |       |
|   4 |     TABLE ACCESS FULL| T1   | 29999 |   292K|   820   (1)| 00:00:01 |     1 |     3 |
|   5 |     TABLE ACCESS FULL| T2   | 29999 |   292K|   820   (1)| 00:00:01 |     1 |     3 |
---------------------------------------------------------------------------------------------

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

   3 - access("T1"."X"="T2"."X")

SQL> set autotrace off
SQL>

 这里执行计划的关键部分是,hash join发生在分区范围内(或“under”分区范围内)的所有迭代中。这可以解释为:“从每个表中的第一个分区开始,对该分区执行散列连接。然后移动到下一个分区;在那个分区上执行散列连接”,等等。这在资源上是有效的,因为在任何情况下我们都不会尝试(显然是失败的)将表T1分区P_KITCHEN连接到表T2分区P_BATHROOM或P_DINING。每个散列连接是一个较小的操作,因此也更有可能在该会话的可用PGA分配中完成。而且,当涉及到并行运行这样的查询时,每个并行的奴隶进程都可以处理将一个分区对其他奴隶进程隔离的工作。

 

如果分区没有对齐(请参阅前面的编辑器注释),那么我们的连接就没有那么有效。

SQL> --
SQL> -- Example 2
SQL> --
SQL>
SQL>
SQL> drop table t2 purge;

Table dropped.

SQL> create table t2 ( x int, y int )
  2  partition by range ( x )
  3  (
  4  partition p1 values less than (15000),
  5  partition p3 values less than (30000)
  6  );

Table created.

SQL>
SQL> --
SQL> -- all partitions do NOT align, so we do NOT see partition-wise join
SQL> --
SQL>
SQL> insert into t2 select * from t1;

29999 rows created.

SQL> commit;

Commit complete.

SQL> exec dbms_stats.gather_table_stats('','t2')

PL/SQL procedure successfully completed.

SQL> set autotrace traceonly explain
SQL> select count(t1.y), count(t2.y)
  2  from t1,t2
  3  where t1.x = t2.x;

Execution Plan
----------------------------------------------------------
Plan hash value: 666786458

---------------------------------------------------------------------------------------------------------
| Id  | Operation                     | Name    | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
---------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |         |     1 |    20 |  1369   (1)| 00:00:01 |       |       |
|   1 |  SORT AGGREGATE               |         |     1 |    20 |            |          |       |       |
|*  2 |   HASH JOIN                   |         | 29999 |   585K|  1369   (1)| 00:00:01 |       |       |
|   3 |    PART JOIN FILTER CREATE    | :BF0000 | 29999 |   585K|  1369   (1)| 00:00:01 |       |       |
|   4 |     PARTITION RANGE ALL       |         | 29999 |   292K|   820   (1)| 00:00:01 |     1 |     3 |
|   5 |      TABLE ACCESS FULL        | T1      | 29999 |   292K|   820   (1)| 00:00:01 |     1 |     3 |
|   6 |    PARTITION RANGE JOIN-FILTER|         | 29999 |   292K|   548   (1)| 00:00:01 |:BF0000|:BF0000|
|   7 |     TABLE ACCESS FULL         | T2      | 29999 |   292K|   548   (1)| 00:00:01 |:BF0000|:BF0000|
---------------------------------------------------------------------------------------------------------

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

   2 - access("T1"."X"="T2"."X")

Note
-----
   - this is an adaptive plan

SQL> set autotrace off
SQL>
SQL>

 这里的关键元素是散列连接现在位于所有分区的循环之上。在Oracle的早期版本中,你不会看到包含:BF0000的行,因此这是一个跨所有行的简单联接,就好像这些表根本没有分区一样。但是当分区不一致时,在现代版本中情况会稍微好一些。我们使用“Bloom filter”(因此:BF前缀)来减少连接两个表的开销。既然我在这篇文章中使用了比喻,那就想想“提前打电话”到电影院,看看有没有你最喜欢的电影的座位。如果电影院老板说电影已经卖完了,你就省了一趟车。但只是因为影院老板说还有空座,你还是有可能开车去那里,发现电影在那段时间已经卖完了。“Bloom filter”就像提前打电话一样——你很有可能可以避免一些工作,但这并不是一个保证。你可以在Christian Antognini的一篇伟大的白皮书中读到有关“Bloom filter”的内容。

注意,所有的分区必须对齐。下面是一个示例,其中前三个分区是对齐的,边界是10000、20000和30000,但是第二个表T2定义了一个额外的分区。再一次,我们回到了Bloom filter选项。

SQL> --
SQL> -- Example 3
SQL> --
SQL> drop table t2 purge;

Table dropped.

SQL> create table t2 ( x int, y int )
  2  partition by range ( x )
  3  (
  4  partition p1 values less than (10000),
  5  partition p2 values less than (20000),
  6  partition p3 values less than (30000),
  7  partition p4 values less than (40000)
  8  );

Table created.

SQL>
SQL> --
SQL> -- all partitions do NOT align, so we do NOT see partition-wise join
SQL> --
SQL>
SQL> insert into t2 select rownum, rownum from dual connect by level < 40000;

39999 rows created.

SQL> commit;

Commit complete.

SQL> exec dbms_stats.gather_table_stats('','t2')

PL/SQL procedure successfully completed.

SQL> set autotrace traceonly explain
SQL> select count(t1.y), count(t2.y)
  2  from t1,t2
  3  where t1.x = t2.x;

Execution Plan
----------------------------------------------------------
Plan hash value: 666786458

---------------------------------------------------------------------------------------------------------
| Id  | Operation                     | Name    | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
---------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |         |     1 |    20 |  1913   (1)| 00:00:01 |       |       |
|   1 |  SORT AGGREGATE               |         |     1 |    20 |            |          |       |       |
|*  2 |   HASH JOIN                   |         | 29999 |   585K|  1913   (1)| 00:00:01 |       |       |
|   3 |    PART JOIN FILTER CREATE    | :BF0000 | 29999 |   585K|  1913   (1)| 00:00:01 |       |       |
|   4 |     PARTITION RANGE ALL       |         | 29999 |   292K|   820   (1)| 00:00:01 |     1 |     3 |
|   5 |      TABLE ACCESS FULL        | T1      | 29999 |   292K|   820   (1)| 00:00:01 |     1 |     3 |
|   6 |    PARTITION RANGE JOIN-FILTER|         | 39999 |   390K|  1093   (1)| 00:00:01 |:BF0000|:BF0000|
|   7 |     TABLE ACCESS FULL         | T2      | 39999 |   390K|  1093   (1)| 00:00:01 |:BF0000|:BF0000|
---------------------------------------------------------------------------------------------------------

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

   2 - access("T1"."X"="T2"."X")

Note
-----
   - this is an adaptive plan

SQL> set autotrace off
SQL>
SQL>
SQL>

 因此,对分区表进行更快的查询不仅仅是关于分区修剪。Partition-wise joins还可以对查询响应时间产生有益的影响。

 

https://connor-mcdonald.com/2017/09/21/partition-wise-join/