连接两个以上的表时,Oracle 连接消除无法按预期工作

Posted

技术标签:

【中文标题】连接两个以上的表时,Oracle 连接消除无法按预期工作【英文标题】:Oracle join elimination not working as expected when joining more than two tables 【发布时间】:2017-11-06 16:11:16 【问题描述】:

连接两个表时,连接消除工作正常:

SQL> set lines 200;
SQL> 
SQL> select * from v$version;

BANNER                                                                               CON_ID                                                                                                             
-------------------------------------------------------------------------------- ----------                                                                                                             
Oracle Database 12c Release 12.1.0.1.0 - 64bit Production                                 0                                                                                                             
PL/SQL Release 12.1.0.1.0 - Production                                                    0                                                                                                             
CORE    12.1.0.1.0  Production                                                                0                                                                                                             
TNS for Linux: Version 12.1.0.1.0 - Production                                            0                                                                                                             
NLSRTL Version 12.1.0.1.0 - Production                                                    0                                                                                                             

SQL> 
SQL> create table t01 (
  2    id integer,
  3    apk varchar2(255 char),
  4    constraint pk_01 primary key (id)
  5  );

Tabelle wurde erstellt.

SQL> create table t02 (
  2    id integer,
  3    apk varchar2(255 char),
  4    id_t01 integer,
  5    constraint pk_02 primary key (id),
  6    constraint fk_02 foreign key (id_t01) references t01(id)
  7  );

Tabelle wurde erstellt.

SQL> create table t03 (
  2    id integer,
  3    apk varchar2(255 char),
  4    id_t02 integer,
  5    constraint pk_03 primary key (id),
  6    constraint fk_03 foreign key (id_t02) references t02(id)
  7  );

Tabelle wurde erstellt.

SQL> create index ix_t03 on t03(id_t02);

Index wurde erstellt.

SQL> create index ix_t02 on t02(id_t01);

Index wurde erstellt.

SQL> insert into t01 (id, apk)
  2  select level, to_char(level)
  3  from dual
  4  connect by level <= 1000;

1000 Zeilen erstellt.

SQL> insert into t02(id, apk, id_t01)
  2  select id, apk, id from t01;

1000 Zeilen erstellt.

SQL> insert into t03(id, apk, id_t02)
  2  select id, apk, id from t01;

1000 Zeilen erstellt.

SQL> commit;

Transaktion mit COMMIT abgeschlossen.

SQL> 
SQL> exec dbms_stats.gather_table_stats(null, 'T01', method_opt=>'for all columns size skewonly', cascade=>true);

PL/SQL-Prozedur erfolgreich abgeschlossen.

SQL> exec dbms_stats.gather_table_stats(null, 'T02', method_opt=>'for all columns size skewonly', cascade=>true);

PL/SQL-Prozedur erfolgreich abgeschlossen.

SQL> exec dbms_stats.gather_table_stats(null, 'T03', method_opt=>'for all columns size skewonly', cascade=>true);

PL/SQL-Prozedur erfolgreich abgeschlossen.

SQL> commit;

Transaktion mit COMMIT abgeschlossen.

SQL> 
SQL> set autotrace traceonly explain;
SQL> 
SQL> select t02.id
  2  from t02
  3  left join t01 on t01.id = t02.id_t01;

Ausführungsplan
----------------------------------------------------------                                                                                                                                              

--------------------------------------------------------------                                                                                                                                          
| Id  | Operation            | Name  | Rows  | Bytes | Cost  |                                                                                                                                          
--------------------------------------------------------------                                                                                                                                          
|   0 | SELECT STATEMENT     |       |  1000 |  8000 |     2 |                                                                                                                                          
|   1 |  INDEX FAST FULL SCAN| PK_02 |  1000 |  8000 |     2 |                                                                                                                                          
--------------------------------------------------------------                                                                                                                                          

--> 对 t02 的主键索引进行快速全扫描,不读取 t01。这是我所期待的。

加入 t02 和 t03 也可以正常工作:

SQL> 
SQL> select t03.id
  2  from t03
  3  left join t02 on t02.id = t03.id_t02;

Ausführungsplan
----------------------------------------------------------                                                                                                                                              

--------------------------------------------------------------                                                                                                                                          
| Id  | Operation            | Name  | Rows  | Bytes | Cost  |                                                                                                                                          
--------------------------------------------------------------                                                                                                                                          
|   0 | SELECT STATEMENT     |       |  1000 |  8000 |     2 |                                                                                                                                          
|   1 |  INDEX FAST FULL SCAN| PK_03 |  1000 |  8000 |     2 |                                                                                                                                          
--------------------------------------------------------------                                                                                                                                          

--> 对 t03 的主键索引进行快速全扫描,不读取 t02。这是我所期待的。

当我尝试加入 t01、t02 和 t03 时出现问题:

SQL> select t03.id
  2  from t03
  3  left join t02 on t02.id = t03.id_t02
  4  left join t01 on t01.id = t02.id_t01;

Ausführungsplan
----------------------------------------------------------                                                                                                                                              

------------------------------------------------------------                                                                                                                                            
| Id  | Operation          | Name  | Rows  | Bytes | Cost  |                                                                                                                                            
------------------------------------------------------------                                                                                                                                            
|   0 | SELECT STATEMENT   |       |  1000 | 16000 |    18 |                                                                                                                                            
|   1 |  NESTED LOOPS OUTER|       |  1000 | 16000 |    18 |                                                                                                                                            
|   2 |   TABLE ACCESS FULL| T03   |  1000 |  8000 |    18 |                                                                                                                                            
|   3 |   INDEX UNIQUE SCAN| PK_02 |     1 |     8 |     0 |                                                                                                                                            
------------------------------------------------------------                                                                                                                                            

我希望(仅)在此处对 pk_03 进行全索引扫描,但执行计划在 T03 和 PK02 之间执行嵌套循环。

我做错了什么?我有错误的期望吗?我在 Oracle 文档/***/google 中找不到任何解释此行为的内容。

我正在使用的实际数据库确实有更多的列/表等,这只是一个最小的例子。当加入 20 个表并且没有发生预期的连接消除时,问题会变得更糟。这对我们的查询执行时间有相当负面的影响。

非常感谢。

【问题讨论】:

优化器必须认为查询计划将比替代方案更有效地执行。你确定这是一个问题吗?如果是这样,您是否尝试过使用查询提示,例如没有嵌套循环等? 对 pk_03 进行快速全扫描的代价为 2(参见 t02 和 t03 之间的连接),t01、t02 和 t03 之间的连接的执行计划的代价为 18。这正是我不明白,为什么不选择成本较低的计划= 2?我的生产数据库中的实际查询表示为一个视图,该视图被许多其他查询使用;虽然我可以尝试调整这个特定示例,但我无法对所有可能的查询组合使用查询提示。 您的代码在版本 12.1.0.2 中按预期工作。我建议检查 support.oracle.com。 @Matthew McPeak 看起来这个问题实际上与 oracle 版本有关:它在 Oracle 12.2.0.1.0 和 11.2.0.4.0 中按预期工作 - 尚未检查 12.1.0.2。谢谢! 【参考方案1】:
select t03.id
    from t03
    left join t02 on t02.id = t03.id_t02
    left join t01 on t01.id = t02.id_t01;

表 t01 可以从此查询中删除,因为不需要读取该表中的任何内容来确定 t02 中的哪些行具有指向 t01 的有效 FK - 您可以只检查 PK 索引而不是表。

要检查 t03 中的哪些行具有指向 t02 的有效 FK,您也不需要读取 t02。

但是要将这两组行连接在一起,您需要读取所有 t02, 以查看一组中的哪些行具有与另一组中的行匹配的 id。仅索引没有足够的信息来进行此连接。

From this answer,“索引记录仅包含索引字段和指向原始记录的指针” - 这意味着 fk_03(在 t03 上)看起来有点像这样:

id_t02 t02_rowid
873    AAEiyFAAVAAA49bAAA
874    AAEiyFAAVAAA49bAAB
...

要将它们连接在一起,您需要问:哪个 t02 rowids(来自第一个索引)指向第二个索引中具有 id_t01 值的记录?单独的索引没有这些信息 - 你必须阅读表格。

实际上,在审查时,我认为问题更多是将 t03.id 与 pk_02 结果匹配。抱歉,我不是这方面的专家。

无论如何,您可以将该信息添加到复合索引中:

create index ix_t0302 on t03(id_t02, id);

这改变了解释计划:

-------------------------------------------------------------------------------
| Id  | Operation          | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |          |    10 |   160 |     2   (0)| 00:00:01 |
|   1 |  NESTED LOOPS OUTER|          |    10 |   160 |     2   (0)| 00:00:01 |
|   2 |   INDEX FULL SCAN  | IX_T0302 |    10 |    80 |     1   (0)| 00:00:01 |
|*  3 |   INDEX UNIQUE SCAN| PK_02    |     1 |     8 |     1   (0)| 00:00:01 |
-------------------------------------------------------------------------------

*已编辑以更正自己

【讨论】:

以上是关于连接两个以上的表时,Oracle 连接消除无法按预期工作的主要内容,如果未能解决你的问题,请参考以下文章

在 Oracle 中使用连接语法连接 3 个以上的表

连接两种不同类型的表时 Tableau 中的自定义聚合

4.连接查询

连接两个表时插入缺失值

sql连接两个以上的表

SQL - 连接两个表时无法执行不同的计数