使用左外连接时 Oracle 分区修剪不起作用

Posted

技术标签:

【中文标题】使用左外连接时 Oracle 分区修剪不起作用【英文标题】:Oracle Partition Pruning not working when left outer join is used 【发布时间】:2014-11-21 22:01:46 【问题描述】:

我有一个表,它在数字列 (row_id) 上按列表分区,

TABLEA (ROW_ID NUMERIC(38), TB_KEY NUMERIC(38), ROW_DATA VARCHAR(20));

当我从没有连接的表中查询时,分区修剪有效

SELECT A.* FROM TABLEA A
WHERE ROW_ID IN (SELECT ID FROM TABLEB WHERE DT_COL = SYSDATE);

分区修剪失败当我离开外部连接到 TableB 时

SELECT A.* FROM TABLEA A
LEFT OUTER JOIN TABLEB B ON A.TB_KET = B.TB_KEY
WHERE ROW_ID IN (SELECT ID FROM TABLEB WHERE DT_COL = SYSDATE);

当我将左外连接更改为内连接时,分区修剪有效

SELECT A.* FROM TABLEA A
INNER JOIN TABLEB B ON A.TB_KET = B.TB_KEY
WHERE ROW_ID IN (SELECT ID FROM TABLEB WHERE DT_COL = SYSDATE);

当我对 TableB 进行左外连接并且不使用 IN 子句时,分区修剪有效

SELECT A.* FROM TABLEA A
LEFT OUTER JOIN TABLEB B ON A.TB_KET = B.TB_KEY
WHERE ROW_ID = 123;

分区修剪工作,当我离开 TableB 并为 IN 子句使用静态值时

SELECT A.* FROM TABLEA A
LEFT OUTER JOIN TABLEB B ON A.TB_KET = B.TB_KEY
WHERE ROW_ID IN (123, 345);

当我使用 IN 子句和子查询的结果查询该表分区的列时,有人可以解释一下为什么左外连接会导致分区修剪失败吗?

【问题讨论】:

【参考方案1】:

Oracle 11g 的答案是肯定的,分区修剪工作正常。

您的设置中有三种主要的访问模式,list partitionedTABLEA,让我们全部了解一下。请注意,我正在使用 最简单的可能语句来说明行为。

使用相等谓词或 IN 列表中的键访问

最简单的情况是在分区键的相等谓词中使用文字:

SELECT A.* FROM TABLEA A
LEFT OUTER JOIN TABLEB B ON A.TB_KET = B.TB_KEY
WHERE A.ROW_ID = 123;

这导致以下执行计划

-------------------------------------------------------------------------------------------------
| Id  | Operation              | Name   | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
-------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |        |     1 |    51 |     4   (0)| 00:00:01 |       |       |
|*  1 |  HASH JOIN OUTER       |        |     1 |    51 |     4   (0)| 00:00:01 |       |       |
|   2 |   PARTITION LIST SINGLE|        |     1 |    38 |     2   (0)| 00:00:01 |   KEY |   KEY |
|*  3 |    TABLE ACCESS FULL   | TABLEA |     1 |    38 |     2   (0)| 00:00:01 |     2 |     2 |
|   4 |   TABLE ACCESS FULL    | TABLEB |     1 |    13 |     2   (0)| 00:00:01 |       |       |
-------------------------------------------------------------------------------------------------

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

   1 - access("A"."TB_KET"="B"."TB_KEY"(+))
   3 - filter("A"."ROW_ID"=123)

仅访问 TABLEA 的相关分区(此处为分区 #2) - 请参阅列 PstartPstop

IN LIST 中的情况有点复杂,但类似

SELECT A.* FROM TABLEA A
LEFT OUTER JOIN TABLEB B ON A.TB_KET = B.TB_KEY
WHERE ROW_ID IN (123, 345);

-------------------------------------------------------------------------------------------------
| Id  | Operation              | Name   | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
-------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |        |     1 |    51 |     4   (0)| 00:00:01 |       |       |
|*  1 |  HASH JOIN OUTER       |        |     1 |    51 |     4   (0)| 00:00:01 |       |       |
|   2 |   PARTITION LIST INLIST|        |     1 |    38 |     2   (0)| 00:00:01 |KEY(I) |KEY(I) |
|*  3 |    TABLE ACCESS FULL   | TABLEA |     1 |    38 |     2   (0)| 00:00:01 |KEY(I) |KEY(I) |
|   4 |   TABLE ACCESS FULL    | TABLEB |     1 |    13 |     2   (0)| 00:00:01 |       |       |
-------------------------------------------------------------------------------------------------

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

   1 - access("A"."TB_KET"="B"."TB_KEY"(+))
   3 - filter("A"."ROW_ID"=123 OR "A"."ROW_ID"=345)

在这种情况下,可以访问更多分区,但只考虑那些包含来自IN LIST 的键的分区。

同样适用于使用绑定变量的访问。

使用 NESTED LOOPS 访问表中的键

更复杂的是连接两个表的情况。在为来自TABLEB 的每个键使用嵌套循环连接时 TABLEA 被访问。这意味着对于每个键,只访问该键所在的一个分区。

SELECT  A.* FROM TABLEA A
WHERE ROW_ID IN (SELECT ID FROM TABLEB WHERE DT_COL = SYSDATE);

---------------------------------------------------------------------------------------------------
| Id  | Operation                | Name   | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
---------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT         |        |     1 |    60 |     4  (25)| 00:00:01 |       |       |
|   1 |  NESTED LOOPS            |        |     1 |    60 |     4  (25)| 00:00:01 |       |       |
|   2 |   SORT UNIQUE            |        |     1 |    22 |     2   (0)| 00:00:01 |       |       |
|*  3 |    TABLE ACCESS FULL     | TABLEB |     1 |    22 |     2   (0)| 00:00:01 |       |       |
|   4 |   PARTITION LIST ITERATOR|        |   100K|  3710K|     1   (0)| 00:00:01 |   KEY |   KEY |
|*  5 |    TABLE ACCESS FULL     | TABLEA |   100K|  3710K|     1   (0)| 00:00:01 |   KEY |   KEY |
---------------------------------------------------------------------------------------------------

再次有一个分区修剪KEY - KEY,所以只有来自TABLEB的键的分区被访问,但从嵌套循环的性质来看,一个分区可以被访问多次(对于不同的键)。

使用 HASH JOIN 访问表中的键

使用HASH JOIN 是最复杂的情​​况,分区修剪必须发生在连接开始之前。这是工作中的Bloom Filter。

它是如何工作的?在扫描TABLEB之后,Oracle 知道所有相关的键,这些键可以映射到相关的分区和一个 创建这些分区的Bloom Filter (BF)(操作 3 和 2)。 BF 被传递给TABLEA 并用于对其进行分区修剪(操作 4 和 5)。

-------------------------------------------------------------------------------------------------------
| Id  | Operation                   | Name    | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
-------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |         |   100K|  5859K|     5  (20)| 00:00:01 |       |       |
|*  1 |  HASH JOIN RIGHT SEMI       |         |   100K|  5859K|     5  (20)| 00:00:01 |       |       |
|   2 |   PART JOIN FILTER CREATE   | :BF0000 |     1 |    22 |     2   (0)| 00:00:01 |       |       |
|*  3 |    TABLE ACCESS FULL        | TABLEB  |     1 |    22 |     2   (0)| 00:00:01 |       |       |
|   4 |   PARTITION LIST JOIN-FILTER|         |   100K|  3710K|     2   (0)| 00:00:01 |:BF0000|:BF0000|
|   5 |    TABLE ACCESS FULL        | TABLEA  |   100K|  3710K|     2   (0)| 00:00:01 |:BF0000|:BF0000|
-------------------------------------------------------------------------------------------------------

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

   1 - access("ROW_ID"="ID")
   3 - filter("DT_COL"=SYSDATE@!)

请参阅 PstartPstop :BFnnnn 作为布隆过滤器的标志。

【讨论】:

【参考方案2】:

分区修剪可以与LEFT OUTER JOININ 子查询一起使用。您可能会看到一个非常具体的问题,需要更具体的测试用例。

示例架构

drop table tableb purge;
drop table tablea purge;

create table tablea (row_id numeric(38),tb_key numeric(38),row_data varchar(20))
 partition by list(row_id)
 (partition p1 values (1),partition p123 values(123),partition p345 values(345));
create table tableb (id numeric(38), dt_col date, tb_key numeric(38));

begin
    dbms_stats.gather_table_stats(user, 'TABLEA');
    dbms_stats.gather_table_stats(user, 'TABLEB');
end;
/

查询

--Partition pruning works when i query from table with no joins:
explain plan for 
SELECT A.* FROM TABLEA A
WHERE ROW_ID IN (SELECT ID FROM TABLEB WHERE DT_COL = SYSDATE);
select * from table(dbms_xplan.display);

--Partition Pruning fails when I do left outer join to TableB
explain plan for
SELECT A.* FROM TABLEA A
LEFT OUTER JOIN TABLEB B ON A.TB_KEY = B.TB_KEY
WHERE ROW_ID IN (SELECT ID FROM TABLEB WHERE DT_COL = SYSDATE);
select * from table(dbms_xplan.display);

--Partition Pruning works when I change left outer join to inner join
explain plan for
SELECT A.* FROM TABLEA A
INNER JOIN TABLEB B ON A.TB_KEY = B.TB_KEY
WHERE ROW_ID IN (SELECT ID FROM TABLEB WHERE DT_COL = SYSDATE);
select * from table(dbms_xplan.display);

--Partition Pruning works when I do left outer join to TableB and do not use IN clause
explain plan for
SELECT A.* FROM TABLEA A
LEFT OUTER JOIN TABLEB B ON A.TB_KEY = B.TB_KEY
WHERE ROW_ID = 123;
select * from table(dbms_xplan.display);

--Partition Pruning works when I do left outer join to TableB and use static values for IN clause
explain plan for
SELECT A.* FROM TABLEA A
LEFT OUTER JOIN TABLEB B ON A.TB_KEY = B.TB_KEY
WHERE ROW_ID IN (123, 345);
select * from table(dbms_xplan.display);

输出

此处不显示完整的执行计划,以节省空间。唯一重要的列是PstartPstop,这意味着使用了分区修剪。

执行计划如下所示:

... -----------------
... | Pstart| Pstop |
... -----------------
... 
... |   KEY |   KEY |
... |   KEY |   KEY |
... 
... -----------------

OR

... -----------------
... | Pstart| Pstop |
... -----------------
... 
... |     2 |     2 |
... |     2 |     2 |
... 
... -----------------

OR

... -----------------
... | Pstart| Pstop |
... -----------------
... 
... |KEY(I) |KEY(I) |
... |KEY(I) |KEY(I) |
... 
... -----------------

这有什么帮助?

不是很多。尽管您提供的信息比典型问题多得多,但解决此问题还需要更多信息。

至少现在您知道问题不是由分区修剪的一般限制引起的。这里有一个非常具体的问题,很可能与优化器统计信息有关。深入了解此类问题可能需要大量时间。我建议从上面的示例数据开始,并添加更多功能和数据,直到分区修剪消失。

在这里发布新的测试用例,通过修改问题,应该有人能够解决它。

【讨论】:

以上是关于使用左外连接时 Oracle 分区修剪不起作用的主要内容,如果未能解决你的问题,请参考以下文章

在 hive 中使用 Null 检查的左外连接查询不起作用

当我们在 select 中有 case 语句时,使用左外连接进行 Oracle 更新

添加 2 表条件时,Oracle 左外连接停止返回结果

在 Linq 中使用左外连接

oracle左外连接不显示右空值

Oracle左外连接问题