【问题标题】:Join elimination not working in Oracle with sub queries使用子查询在 Oracle 中加入消除不起作用
【发布时间】:2017-03-22 06:35:30
【问题描述】:

我能够让联接消除适用于简单的情况,例如一对一关系,但不适用于稍微复杂的情况。 最终我想尝试锚建模,但首先我需要找到解决这个问题的方法。我正在使用 Oracle 12c Enterprise Edition Release 12.1.0.2.0。

我的测试用例的 DDL:

drop view product_5nf;
drop table product_color cascade constraints;
drop table product_price cascade constraints;
drop table product       cascade constraints;

create table product(
   product_id number not null
  ,constraint product_pk primary key(product_id)
);

create table product_color(
   product_id  number         not null references product
  ,color       varchar2(10)   not null
  ,constraint product_color_pk primary key(product_id)
);

create table product_price(
   product_id  number   not null references product
  ,from_date   date     not null
  ,price       number   not null
  ,constraint product_price_pk primary key(product_id, from_date)
);

一些示例数据:

insert into product values(1);
insert into product values(2);
insert into product values(3);
insert into product values(4);

insert into product_color values(1, 'Red');
insert into product_color values(2, 'Green');

insert into product_price values(1, date '2016-01-01', 10);
insert into product_price values(1, date '2016-02-01', 8);
insert into product_price values(1, date '2016-05-01', 5);

insert into product_price values(2, date '2016-02-01', 5);

insert into product_price values(4, date '2016-01-01', 10);

commit;

5NF 观点

第一个视图无法编译 - 它失败并出现 ORA-01799:列可能没有外部连接到子查询。不幸的是,当我查看锚建模的在线示例时,大多数历史视图都是这样定义的……

create view product_5nf as
   select p.product_id
         ,pc.color
         ,pp.price 
     from product p
     left join product_color pc on(
          pc.product_id = p.product_id
     )
     left join product_price pp on(
          pp.product_id = p.product_id
      and pp.from_date  = (select max(pp2.from_date) 
                             from product_price pp2 
                            where pp2.product_id = pp.product_id)
     );

以下是我修复它的尝试。当使用此视图并简单选择 product_id 时,Oracle 设法消除 product_color 但 product_price。

create view product_5nf as
   select product_id
         ,pc.color
         ,pp.price 
     from product p
     left join product_color pc using(product_id)
     left join (select pp1.product_id, pp1.price 
                  from product_price pp1
                 where pp1.from_date  = (select max(pp2.from_date) 
                                           from product_price pp2 
                                          where pp2.product_id = pp1.product_id)
              )pp using(product_id);

select product_id
  from product_5nf;

----------------------------------------------------------
| Id  | Operation             | Name             | Rows  |
----------------------------------------------------------
|   0 | SELECT STATEMENT      |                  |     4 |
|*  1 |  HASH JOIN OUTER      |                  |     4 |
|   2 |   INDEX FAST FULL SCAN| PRODUCT_PK       |     4 |
|   3 |   VIEW                |                  |     3 |
|   4 |    NESTED LOOPS       |                  |     3 |
|   5 |     VIEW              | VW_SQ_1          |     5 |
|   6 |      HASH GROUP BY    |                  |     5 |
|   7 |       INDEX FULL SCAN | PRODUCT_PRICE_PK |     5 |
|*  8 |     INDEX UNIQUE SCAN | PRODUCT_PRICE_PK |     1 |
----------------------------------------------------------

我找到的唯一解决方案是使用标量子查询,如下所示:

create or replace view product_5nf as
   select p.product_id
         ,pc.color
         ,(select pp.price
             from product_price pp
            where pp.product_id = p.product_id
              and pp.from_date = (select max(from_date)
                                    from product_price pp2
                                   where pp2.product_id = pp.product_id)) as price
     from product p
     left join product_color pc on(
          pc.product_id = p.product_id
     )

select product_id
  from product_5nf;

---------------------------------------------------
| Id  | Operation            | Name       | Rows  |
---------------------------------------------------
|   0 | SELECT STATEMENT     |            |     4 |
|   1 |  INDEX FAST FULL SCAN| PRODUCT_PK |     4 |
---------------------------------------------------

现在,Oracle 成功地删除了 product_price 表。但是,标量子查询的实现方式与连接不同,它们的执行方式根本无法让我在现实世界的场景中获得任何可接受的性能。

TL;DR 如何重写视图product_5nf 以便Oracle 成功消除两个依赖表?

【问题讨论】:

  • 你确定标量子查询方法的性能真的很差吗?在 Oracle 12.1 中,优化器可以并且经常会取消嵌套标量子查询。因此,您可能会得到您正在寻找的东西:CBO 可以在不需要它们的查询中省略它们,并在需要时将它们取消嵌套以获得合理的性能。您可能会尝试标量子查询的变体,看看您是否无法诱使 Oracle 为您解除嵌套。
  • @MatthewMcPeak,我没有想到这一点,所以我只尝试了一种编写子查询的方法。绝对值得研究!我确实比较了标量 qry 与连接之间的执行计划和时间,差异是我构建的涉及数百 k 记录的稍微大一点的测试用例中执行时间的 1/3。
  • 我刚刚确认这是可能的。我会发布另一个答案。

标签: sql database oracle cost-based-optimizer anchor-modeling


【解决方案1】:

我认为你这里有两个问题。

首先,加入消除仅适用于某些特定情况(PK-PK、PK-FK 等)。这不是一般的事情,您可以LEFT JOIN 到任何将为每个连接键值返回单行并让 Oracle 消除连接的行集。

其次,即使 Oracle 已经足够先进,可以在任何 LEFT JOIN 上执行连接消除,它知道每个连接键值只能获得一行,但 Oracle 还不支持 LEFT JOINS 上基于复合键(Oracle 支持文档 887553.1 说这将在 R12.2 中提供)。

您可以考虑的一种解决方法是用每个product_id 的最后一行具体化一个视图。然后LEFT JOIN 到物化视图。像这样:

create table product(
   product_id number not null
  ,constraint product_pk primary key(product_id)
);

create table product_color(
   product_id  number         not null references product
  ,color       varchar2(10)   not null
  ,constraint product_color_pk primary key(product_id)
);

create table product_price(
   product_id  number   not null references product
  ,from_date   date     not null
  ,price       number   not null
  ,constraint product_price_pk  primary key (product_id, from_date )
);

-- Add a VIRTUAL column to PRODUCT_PRICE so that we can get all the data for 
-- the latest row by taking the MAX() of this column.
alter table product_price add ( sortable_row varchar2(80) generated always as ( lpad(product_id,10,'0') || to_char(from_date,'YYYYMMDDHH24MISS') || lpad(price,10,'0'))  virtual not null );

-- Create a MV snapshot so we can materialize a view having only the latest
-- row for each product_id and can refresh that MV fast on commit.
create materialized view log on product_price with sequence, primary key, rowid ( price  ) including new values;

-- Create the MV
create materialized view product_price_latest refresh fast on commit enable query rewrite as
SELECT product_id, max( lpad(product_id,10,'0') || to_char(from_date,'YYYYMMDDHH24MISS') || lpad(price,10,'0')) sortable_row
FROM   product_price
GROUP BY product_id;

-- Create a primary key on the MV, so we can do join elimination
alter table product_price_latest add constraint ppl_pk primary key ( product_id );

-- Insert the OP's test data
insert into product values(1);
insert into product values(2);
insert into product values(3);
insert into product values(4);

insert into product_color values(1, 'Red');
insert into product_color values(2, 'Green');

insert into product_price ( product_id, from_date, price ) values(1, date '2016-01-01', 10 );
insert into product_price ( product_id, from_date, price) values(1, date '2016-02-01', 8);
insert into product_price ( product_id, from_date, price) values(1, date '2016-05-01', 5);

insert into product_price ( product_id, from_date, price) values(2, date '2016-02-01', 5);

insert into product_price ( product_id, from_date, price) values(4, date '2016-01-01', 10);

commit;

-- Create the 5NF view using the materialized view
create or replace view product_5nf as
   select p.product_id
         ,pc.color
         ,to_date(substr(ppl.sortable_row,11,14),'YYYYMMDDHH24MISS') from_date
         ,to_number(substr(ppl.sortable_row,25)) price 
     from product p
     left join product_color pc on pc.product_id = p.product_id
     left join product_price_latest ppl on ppl.product_id = p.product_id 
;

-- The plan for this should not include any of the unnecessary tables.
select product_id from product_5nf;

-- Check the plan
SELECT *
FROM   TABLE (DBMS_XPLAN.display_cursor (null, null,
                                         'ALLSTATS LAST'));

------------------------------------------------
| Id  | Operation        | Name       | E-Rows |
------------------------------------------------
|   0 | SELECT STATEMENT |            |        |
|   1 |  INDEX FULL SCAN | PRODUCT_PK |      1 |
------------------------------------------------

【讨论】:

  • 好的,完美!这解释了“为什么”它对我不起作用,也让我知道我必须做些什么来解决它(直到 12.2)。
  • 感谢您提供有关组合键连接消除的信息,可能会在 12.2 中出现。我们肯定一直在等待这个功能!
【解决方案2】:

我不能让price join被淘汰,但是如果你这样做,至少可以减少访问单个索引进行价格检查:

CREATE OR REPLACE view product_5nf as
select p.product_id
      ,pc.color
      ,pp.price 
 from product p
 left join product_color pc ON p.product_id = pc.product_id
 left join (select pp1.product_id, pp1.price 
              from (SELECT product_id,
                           price,
                           from_date,
                           max(from_date) OVER (PARTITION BY product_id) max_from_date
                    FROM   product_price) pp1
             where pp1.from_date = max_from_date) pp ON p.product_id = pp.product_id;

【讨论】:

  • 感谢您的努力!我已经走上了这条路,但关键部分是加入消除。您的回答没有回答我的问题,但我认为无论如何它都会增加问题的价值。谢谢!
  • 是的;这是一个有趣的问题。我想可能是因为 product_price 表的每个 product_id 可能有不止一行(根据主键),这导致 Oracle 不进行消除(即使我们知道子查询每个 product_id 只会返回一行)。但是,将主键切换为 product_key 并没有帮助。
  • 更多一点的建议是,如果我使用产品 id 的 pk 创建 product_price 表,然后直接加入该表,它就可以正常工作。因此,我认为 Oracle 不确定您的子查询只会返回导致问题的每个 product_id 一行的事实。我尝试将子查询创建为单独的视图并仅在 product_id 列上添加主键,但这也不起作用。
  • 我在 Twitter 上与 Jonathan Lewis 进行了简短讨论,他将我推荐给 this optimiser blog entry。这可能是该帖子末尾提到的第一个限制,导致优化器无法消除连接。从哪里调用视图?如果它来自一个过程,那么如果您不查询价格列,也许您应该使用 IF 语句来运行不同的查询?
  • 我想知道是否将子查询冗余重写为“select pp1.product_id, max(pp1.price) price”等会鼓励优化器知道结果在 product_id 上是唯一的。
【解决方案3】:

现在,Oracle 成功地删除了 product_price 表。但是,标量子查询的实现方式与连接不同,它们的执行方式根本无法让我在现实世界的场景中获得任何可接受的性能。

Oracle 12.1 中基于成本的优化器可以将查询转换为未嵌套的标量子查询。因此,性能可能与您在问题中所追求的 LEFT JOIN 一样好。

诀窍是你必须稍微调整一下。

首先,确保标量子查询返回max() 而没有group by,这样CBO 就知道不可能获得超过一行。 (否则它不会取消嵌套)。

其次,您需要将product_price 中的所有字段组合成一个标量子查询,否则CBO 将多次取消嵌套并加入product_price

这里有一个 Oracle 12.1 的测试用例说明了这个工作。

drop view product_5nf;
drop table product_color cascade constraints;
drop table product_price cascade constraints;
drop table product       cascade constraints;


create table product(
   product_id number not null
  ,constraint product_pk primary key(product_id)
);

create table product_color(
   product_id  number         not null references product
  ,color       varchar2(10)   not null
  ,constraint product_color_pk primary key(product_id)
);

create table product_price(
   product_id  number   not null references product
  ,from_date   date     not null
  ,price       number   not null
  ,constraint product_price_pk  primary key (product_id, from_date )
);

insert into product ( product_id ) SELECT rownum FROM dual connect by rownum <= 100000;

insert into product_color ( product_id, color ) SELECT rownum, dbms_random.string('a',8) color FROM DUAL connect by rownum <= 100000;

--delete from product_price;
insert into product_price ( product_id, from_date, price ) SELECT product_id, trunc(sysdate) + dbms_random.value(-3,3) from_date, floor(dbms_random.value(50,120)/10)*10 price from product cross join lateral ( SELECT rownum x FROM dual connect by rownum <= mod(product_id,5));

commit;

begin dbms_stats.gather_table_stats ( ownname => USER, tabname => 'PRODUCT' ); end; 
begin dbms_stats.gather_table_stats ( ownname => USER, tabname => 'PRODUCT_COLOR' ); end; 
begin dbms_stats.gather_table_stats ( ownname => USER, tabname => 'PRODUCT_PRICE' ); end; 

commit;

alter table product_price add ( composite_column varchar2(80) generated always as ( to_char(from_date,'YYYYMMDDHH24MISS') || lpad(price,10,0)) virtual );

create or replace view product_5nf as
   select d.product_id, d.color, to_date(substr(d.product_date_price,1,14),'YYYYMMDDHH24MISS') from_date, to_number(substr(d.product_date_price,-10)) price 
from 
(    select p.product_id
         ,pc.color
         ,( SELECT max(composite_column)  FROM product_price pp WHERE pp.product_id = p.product_id AND pp.from_date = ( SELECT max(pp2.from_date) FROM product_price pp2 WHERE pp2.product_id = pp.product_id ) ) product_date_price
     from product p
     left join product_color pc on pc.product_id = p.product_id )  d
;

select product_id from product_5nf;

----------------------------------------------
| Id  | Operation         | Name    | E-Rows |
----------------------------------------------
|   0 | SELECT STATEMENT  |         |        |
|   1 |  TABLE ACCESS FULL| PRODUCT |    100K|
----------------------------------------------

select * from product_5nf;

SELECT *
FROM   TABLE (DBMS_XPLAN.display_cursor (null, null,
                                         'ALLSTATS LAST'));

--------------------------------------------------------------------------------------
| Id  | Operation                | Name          | E-Rows |  OMem |  1Mem | Used-Mem |
--------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT         |               |        |       |       |          |
|*  1 |  HASH JOIN RIGHT OUTER   |               |    100K|  8387K|  3159K| 8835K (0)|
|   2 |   VIEW                   | VW_SSQ_2      |      2 |       |       |          |
|   3 |    HASH GROUP BY         |               |      2 |    13M|  2332K|   12M (0)|
|   4 |     VIEW                 | VM_NWVW_3     |      2 |       |       |          |
|*  5 |      FILTER              |               |        |       |       |          |
|   6 |       HASH GROUP BY      |               |      2 |    23M|  5055K|   20M (0)|
|*  7 |        HASH JOIN         |               |    480K|    12M|  4262K|   17M (0)|
|   8 |         TABLE ACCESS FULL| PRODUCT_PRICE |    220K|       |       |          |
|   9 |         TABLE ACCESS FULL| PRODUCT_PRICE |    220K|       |       |          |
|* 10 |   HASH JOIN OUTER        |               |    100K|  5918K|  3056K| 5847K (0)|
|  11 |    TABLE ACCESS FULL     | PRODUCT       |    100K|       |       |          |
|  12 |    TABLE ACCESS FULL     | PRODUCT_COLOR |    100K|       |       |          |
--------------------------------------------------------------------------------------

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

   1 - access("ITEM_2"="P"."PRODUCT_ID")
   5 - filter("PP"."FROM_DATE"=MAX("PP2"."FROM_DATE"))
   7 - access("PP2"."PRODUCT_ID"="PP"."PRODUCT_ID")
  10 - access("PC"."PRODUCT_ID"="P"."PRODUCT_ID")

【讨论】:

  • 仅供参考...我并不是说取消嵌套肯定更快。保持它嵌套(本质上是一个嵌套循环左连接)可能会更快。但关键是,有了技术,你就可以控制任何一种方式。
  • 我正在尝试!看起来很有希望!我还没有理解 substr() 的魔力,但我想如果我只需要价格就不需要它了? from_date 永远不会包含在最终视图中。
  • 是的,如果您只需要一列,则不需要substr() 的东西。 substr() 用于拆分我们填充在一起的多个列,以便我们可以将它们全部放在一个标量子查询中。
  • 你能更新结果吗?哪个更快,速度快多少?今天晚些时候我可以自己尝试一下,但也许不是每个人都这么感兴趣。
  • 标量技术与连接之间的性能差异是因素 2。我无法在 cmets 中发布代码,所以我编辑了您的答案并添加了它。如果这不是它应该如何工作的,请道歉:)
【解决方案4】:

好的,我正在回答我自己的问题。此答案中的信息适用于Oracle Database 12c Enterprise Edition Release 12.1.0.2.0 - 64bit Production,但可能不适用于更高版本。 不要为这个答案投票,因为它没有回答问题。

由于当前版本的特定限制(如 Mathew McPeak 所述),根本不可能让 Oracle 完全消除底层 5NF 视图中不必要的连接。限制是 不可能在基于复合键的左连接上消除连接

解决此限制的任何尝试似乎都会引入重复或更新异常。接受的答案演示了如何通过使用物化视图并因此复制数据来克服优化器中的这一限制。这个答案展示了如何以更少的重复但更新异常来解决问题。

此解决方法基于您可以在唯一索引中使用可为空的列这一事实。我们将null 用于所有历史版本,将实际product_id 用于使用外键引用产品表的最新版本。

alter table product_price add(
   latest_id number
  ,constraint product_price_uk  unique(latest_id)
  ,constraint product_price_fk2 foreign key(latest_id) references product(product_id)
  ,constraint product_price_chk check(latest_id = product_id)
);

-- One-time update of existing data
update product_price a
   set a.latest_id = a.product_id
 where from_date = (select max(from_date) 
                      from product_price b 
                     where a.product_id = b.product_id);   

PRODUCT_ID FROM_DATE       PRICE  LATEST_ID
---------- ---------- ---------- ----------
         1 2016-01-01         10       null
         1 2016-02-01          8       null
         1 2016-05-01          5          1
         2 2016-02-01          5          2
         4 2016-01-01         10          4

-- New view definition             
create or replace view product_5nf as
   select p.product_id
         ,pc.color
         ,pp.price
     from product p
     left join product_color pc on(pc.product_id = p.product_id)
     left join product_price pp on(pp.latest_id  = p.product_id);

当然,现在latest_id 必须手动维护...每当插入新记录时,必须首先将旧记录更新为空。

这种方法有两个好处。首先,Oracle 能够完全删除不必要的连接。其次,连接不作为标量子查询执行。

SQL> select count(*) from product_5nf;

---------------------------------------
| Id  | Operation        | Name       |
---------------------------------------
|   0 | SELECT STATEMENT |            |
|   1 |  SORT AGGREGATE  |            |
|   2 |   INDEX FULL SCAN| PRODUCT_PK |
---------------------------------------

Oracle 认识到即使不接触基表也可以解决计数问题。并且看不到不必要的连接...

SQL> select product_id, price from product_5nf;

---------------------------------------------------------
| Id  | Operation                    | Name             |
---------------------------------------------------------
|   0 | SELECT STATEMENT             |                  |
|*  1 |  HASH JOIN OUTER             |                  |
|   2 |   INDEX FULL SCAN            | PRODUCT_PK       |
|   3 |   TABLE ACCESS BY INDEX ROWID| PRODUCT_PRICE    |
|*  4 |    INDEX FULL SCAN           | PRODUCT_PRICE_UK |
---------------------------------------------------------

Oracle 认识到我们必须加入product_price 才能获得价格列。而product_color 也不见踪影……

SQL> select * from product_5nf;

----------------------------------------------------------
| Id  | Operation                     | Name             |
----------------------------------------------------------
|   0 | SELECT STATEMENT              |                  |
|*  1 |  HASH JOIN OUTER              |                  |
|   2 |   NESTED LOOPS OUTER          |                  |
|   3 |    INDEX FULL SCAN            | PRODUCT_PK       |
|   4 |    TABLE ACCESS BY INDEX ROWID| PRODUCT_COLOR    |
|*  5 |     INDEX UNIQUE SCAN         | PRODUCT_COLOR_PK |
|   6 |   TABLE ACCESS BY INDEX ROWID | PRODUCT_PRICE    |
|*  7 |    INDEX FULL SCAN            | PRODUCT_PRICE_UK |
----------------------------------------------------------

这里 Oracle 必须实现所有连接,因为所有列都被引用。

【讨论】:

  • 很好地说明了通过更好的数据建模可以解决多少查询问题。如果您希望数据库快速且频繁地为您提供最新信息,请标记最近的行。物化视图选项是一种最透明的方式。如果您可以修改维护数据的流程,那么您的方法会更好:将数据放入您想要从数据库中取出的数据库中。
【解决方案5】:

[我不知道 ANTI-JOIN 是否算作 Oracle 中的子查询],但 not exists 技巧通常是避免聚合子查询的一种方法:

CREATE VIEW product_5nfa as
   SELECT p.product_id
         ,pc.color
         ,pp.price
     FROM product p
     LEFT JOIN product_color pc
        ON pc.product_id = p.product_id
     LEFT join product_price pp
        ON pp.product_id = p.product_id
        AND NOT EXISTS ( SELECT * FROM product_price pp2
            WHERE pp2.product_id = pp.product_id
            AND pp2.from_date  > pp.from_date
            )   
     ;

来自 OP 的评论:视图已创建,但 Oracle 仍无法删除连接。这是执行计划。

select count(*) from product_5nfa;

-------------------------------------------------
| Id  | Operation            | Name             |
-------------------------------------------------
|   0 | SELECT STATEMENT     |                  |
|   1 |  SORT AGGREGATE      |                  |
|   2 |   NESTED LOOPS OUTER |                  |
|   3 |    INDEX FULL SCAN   | PRODUCT_PK       |
|   4 |    VIEW              |                  |
|   5 |     NESTED LOOPS ANTI|                  |
|*  6 |      INDEX RANGE SCAN| PRODUCT_PRICE_PK |
|*  7 |      INDEX RANGE SCAN| PRODUCT_PRICE_PK |
-------------------------------------------------

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-09-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多