优化 MERGE 语句的 SORT MERGE 连接

Posted

技术标签:

【中文标题】优化 MERGE 语句的 SORT MERGE 连接【英文标题】:Optimizing the SORT MERGE join of a MERGE statement 【发布时间】:2014-02-28 23:44:57 【问题描述】:

考虑将更改应用于聚合表的问题。必须更新存在的行,同时必须插入新行。我的做法如下:

在临时表中插入所有更改(一次 100K) 将临时表合并到主表中(最终达到数百万行)

SQL(带有 SORT MERGE 提示)如下所示(没什么花哨的):

merge /*+ USE_MERGE(t s) */ 
into F_SCREEN_INSTANCE t
using F_SCREEN_INSTANCE_BUF s 
    on (s.DAY_ID = t.DAY_ID and s.PARTIAL_ID = t.PARTIAL_ID)
when matched then update set 
    t.ACTIVE_TIME_SUM = t.ACTIVE_TIME_SUM + s.ACTIVE_TIME_SUM,
    t.IDLE_TIME_SUM = t.IDLE_TIME_SUM + s.IDLE_TIME_SUM
when not matched then insert values (
    s.DAY_ID, s.PARTIAL_ID, s.ID, s.AGENT_USER_ID, s.COMPUTER_ID, s.RAW_APPLICATION_ID, s.APP_USER_ID, s.APPLICATION_ID, s.USER_ID, s.RAW_MODULE_ID, s.MODULE_ID, s.START_TIME, s.RAW_SCREEN_NAME, s.SCREEN_ID, s.SCREEN_TYPE, s.ACTIVE_TIME_SUM, s.IDLE_TIME_SUM)

F_SCREEN_INSTANCE 表以(DAY_ID, PARTIAL_ID) 作为主键,也是 IOT(索引组织表)。这使其成为合并连接的理想候选者:行按查找键物理排序。

到目前为止一切顺利。我已经开始了一个基准测试,最初的时间看起来不错,一次合并需要 10 秒。但是大约一个小时后,由于 tempdb 使用量很大(每次合并 4GB),合并需要大约 4 分钟。下面的查询计划显示 F_SCREEN_INSTANCE 在合并之前重新排序,即使表已经理想地排序。当然,随着表的增长,需要更多的 tempdb,整个方法就会崩溃。

好的,那为什么要重新排序表格呢?原来是合并连接实现的一个限制:the second table is always sorted。

如果存在索引,那么数据库可以避免对第一个数据进行排序 放。但是,数据库总是对第二个数据集进行排序, 与索引无关。

O...K,那么我可以将主表设置为第一,将缓冲区设置为第二吗?不,这也不可能。无论我如何在USE_MERGE 提示中列出表,源表始终是第一位的。

最后,这是我的问题:我错过了什么吗?是否有可能使这种 SORT MERGE 方法起作用?

以下是解决您可能提出的问题的更多详细信息:

什么Oracle版本? 12c。 您尝试过 HASH JOIN 吗?是的,这很糟糕,正如预期的那样。需要扫描主表才能构建哈希表。它无法随着 F_SCREEN_INSTANCE 的增长而扩展。 您尝试过 LOOP JOIN 吗?是的,这也很糟糕。考虑到缓冲区表的大小,对 F_SCREEN_INSTANCE 的 100K 查找会花费不合理的时间。合并很快就花了大约 3 分钟。 总而言之,MERGE JOIN 在概念上是最好的访问策略,但 Oracle 实现似乎因重新排序目标表而严重瘫痪。

【问题讨论】:

您是否尝试过单独的updateinsert?我的经验是,这有时比merge 语句要快得多。 是的。但在那种情况下,我将无法使用批处理。 100K 插入/更新对的性能比这里的 MERGE 差。最终,我为 MERGE 使用了 NESTED LOOPS 提示。 【参考方案1】:

无论提示如何,排序合并外连接总是将外连接表放在第二位。添加额外的内部连接可以控制连接顺序,然后可以使用 ROWID 再次连接到大表。希望两个好的连接比一个坏的连接效果更好。

假设

这个答案假设排序合并连接是最快的连接,并且手册是正确的,第二个数据集总是排序的。如果没有更多关于数据的信息,就很难测试这些假设。

示例架构

这里有一些类似的表,用虚假的统计数据让优化器认为它们有 500M 行和 100K 行。

create table F_SCREEN_INSTANCE(DAY_ID number, PARTIAL_ID number, ID number, AGENT_USER_ID number,COMPUTER_ID number, RAW_APPLICATION_ID number, APP_USER_ID number, APPLICATION_ID number, USER_ID number, RAW_MODULE_ID number,MODULE_ID number, START_TIME date, RAW_SCREEN_NAME varchar2(100), SCREEN_ID number, SCREEN_TYPE number, ACTIVE_TIME_SUM number, IDLE_TIME_SUM number,
    constraint f_screen_instance_pk primary key (day_id, partial_id)
) organization index;

create table F_SCREEN_INSTANCE_BUF(DAY_ID number, PARTIAL_ID number, ID number, AGENT_USER_ID number,COMPUTER_ID number, RAW_APPLICATION_ID number, APP_USER_ID number,APPLICATION_ID number, USER_ID number, RAW_MODULE_ID number, MODULE_ID number, START_TIME date, RAW_SCREEN_NAME varchar2(100), SCREEN_ID number, SCREEN_TYPE number, ACTIVE_TIME_SUM number, IDLE_TIME_SUM number,
    constraint f_screen_instance_buf_pk primary key (day_id, partial_id)
);

begin
    dbms_stats.set_table_stats(user, 'F_SCREEN_INSTANCE', numrows => 500000000);
    dbms_stats.set_table_stats(user, 'F_SCREEN_INSTANCE_BUF', numrows => 100000);
end;
/

问题

使用内连接时,可以通过 LEADING 提示实现所需的连接和连接顺序。较小的表 F_SCREEN_INSTANCE_BUF 是第二个表。

explain plan for
select /*+ use_merge(t s) leading(t s) */ *
from f_screen_instance_buf s
join f_screen_instance t
    on (s.DAY_ID = t.DAY_ID and s.PARTIAL_ID = t.PARTIAL_ID);

select * from table(dbms_xplan.display(format => '-predicate'));

Plan hash value: 563239985

-----------------------------------------------------------------------------------------------------
| Id  | Operation           | Name                  | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |                       |   100K|    19M|       |  6898  (66)| 00:00:01 |
|   1 |  MERGE JOIN         |                       |   100K|    19M|       |  6898  (66)| 00:00:01 |
|   2 |   INDEX FULL SCAN   | F_SCREEN_INSTANCE_PK  |   500M|    46G|       |  4504 (100)| 00:00:01 |
|   3 |   SORT JOIN         |                       |   100K|  9765K|    26M|  2393   (1)| 00:00:01 |
|   4 |    TABLE ACCESS FULL| F_SCREEN_INSTANCE_BUF |   100K|  9765K|       |    34   (6)| 00:00:01 |
-----------------------------------------------------------------------------------------------------

更改为左连接时,LEADING 提示不起作用。

explain plan for
select /*+ use_merge(t s) leading(t s) */ *
from f_screen_instance_buf s
left join f_screen_instance t
    on (s.DAY_ID = t.DAY_ID and s.PARTIAL_ID = t.PARTIAL_ID);

select * from table(dbms_xplan.display(format => '-predicate'));

Plan hash value: 1472690071

-----------------------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name                     | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |                          |   100K|    19M|       |    16M  (1)| 00:10:34 |
|   1 |  MERGE JOIN OUTER            |                          |   100K|    19M|       |    16M  (1)| 00:10:34 |
|   2 |   TABLE ACCESS BY INDEX ROWID| F_SCREEN_INSTANCE_BUF    |   100K|  9765K|       |   826   (0)| 00:00:01 |
|   3 |    INDEX FULL SCAN           | F_SCREEN_INSTANCE_BUF_PK |   100K|       |       |    26   (0)| 00:00:01 |
|   4 |   SORT JOIN                  |                          |   500M|    46G|   131G|    16M  (1)| 00:10:34 |
|   5 |    INDEX FAST FULL SCAN      | F_SCREEN_INSTANCE_PK     |   500M|    46G|       |  2703 (100)| 00:00:01 |
-----------------------------------------------------------------------------------------------------------------

据我所知,此限制没有记录。我尝试使用DBMS_XPLAN+outline 设置来查看完整的提示集,然后更改它们。但我所做的一切都无法改变LEFT JOIN 版本的连接顺序。也许其他人可以让这个工作。

select * from table(dbms_xplan.display(format => '-predicate +outline'));

... 
Outline Data
-------------

  /*+
      BEGIN_OUTLINE_DATA
      USE_MERGE(@"SEL$0E991E55" "T"@"SEL$1")
      LEADING(@"SEL$0E991E55" "S"@"SEL$1" "T"@"SEL$1")
      INDEX_FFS(@"SEL$0E991E55" "T"@"SEL$1" ("F_SCREEN_INSTANCE"."DAY_ID" "F_SCREEN_INSTANCE"."PARTIAL_ID"))
      INDEX(@"SEL$0E991E55" "S"@"SEL$1" ("F_SCREEN_INSTANCE_BUF"."DAY_ID" 
              "F_SCREEN_INSTANCE_BUF"."PARTIAL_ID"))
      OUTLINE(@"SEL$9EC647DD")
      OUTLINE(@"SEL$2")
      MERGE(@"SEL$9EC647DD")
      OUTLINE_LEAF(@"SEL$0E991E55")
      ALL_ROWS
      DB_VERSION('12.1.0.1')
      OPTIMIZER_FEATURES_ENABLE('12.1.0.1')
      IGNORE_OPTIM_EMBEDDED_HINTS
      END_OUTLINE_DATA
  */

可能的解决方案

--#3: Join the large table to the smaller result set.  This uses the largest table twice,
--but the plan can use the ROWID for a very quick join.
explain plan for
merge into F_SCREEN_INSTANCE t
using
(
    --#2: Now get the missing rows with an outer join.  Since the _BUF table is
    --small I assume it does not make a big difference exactly how it it joind
    --to the 100K result set.
    --The hints NO_MERGE and NO_PUSH_PRED are required to keep the INNER_JOIN
    --inline view intact.
    select /*+ no_merge(inner_join) no_push_pred(inner_join) */ inner_join.*
    from f_screen_instance_buf s
    left join
    (
        --#1: Get 100K rows efficiently with an inner join.
        --Note that the ROWID is retrieved here.
        select /*+ use_merge(t s) leading(t s) */ s.*, s.rowid s_rowid
        from f_screen_instance_buf s
        join f_screen_instance t
            on (s.DAY_ID = t.DAY_ID and s.PARTIAL_ID = t.PARTIAL_ID)
    ) inner_join
        on (s.DAY_ID = inner_join.DAY_ID and s.PARTIAL_ID = inner_join.PARTIAL_ID)
) s
    on (s.s_rowid = t.rowid)
when matched then update set 
    t.ACTIVE_TIME_SUM = t.ACTIVE_TIME_SUM + s.ACTIVE_TIME_SUM,
    t.IDLE_TIME_SUM = t.IDLE_TIME_SUM + s.IDLE_TIME_SUM
when not matched then insert values (
    s.DAY_ID, s.PARTIAL_ID, s.ID, s.AGENT_USER_ID, s.COMPUTER_ID, s.RAW_APPLICATION_ID, s.APP_USER_ID, s.APPLICATION_ID, s.USER_ID, s.RAW_MODULE_ID, s.MODULE_ID, s.START_TIME, s.RAW_SCREEN_NAME, s.SCREEN_ID, s.SCREEN_TYPE, s.ACTIVE_TIME_SUM, s.IDLE_TIME_SUM);

虽然不是很漂亮,但至少它在排序合并连接中首先生成了一个大表的计划。

select * from table(dbms_xplan.display);

Plan hash value: 1086560566

-------------------------------------------------------------------------------------------------------------
| Id  | Operation                | Name                     | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------------------------------
|   0 | MERGE STATEMENT          |                          |   500G|   173T|       |  5355K (43)| 00:03:30 |
|   1 |  MERGE                   | F_SCREEN_INSTANCE        |       |       |       |            |          |
|   2 |   VIEW                   |                          |       |       |       |            |          |
|*  3 |    HASH JOIN OUTER       |                          |   500G|   179T|    29M|  5355K (43)| 00:03:30 |
|*  4 |     HASH JOIN OUTER      |                          |   100K|    28M|  3712K|  8663  (53)| 00:00:01 |
|   5 |      INDEX FAST FULL SCAN| F_SCREEN_INSTANCE_BUF_PK |   100K|  2539K|       |     9   (0)| 00:00:01 |
|   6 |      VIEW                |                          |   100K|    25M|       |  6898  (66)| 00:00:01 |
|   7 |       MERGE JOIN         |                          |   100K|    12M|       |  6898  (66)| 00:00:01 |
|   8 |        INDEX FULL SCAN   | F_SCREEN_INSTANCE_PK     |   500M|    12G|       |  4504 (100)| 00:00:01 |
|*  9 |        SORT JOIN         |                          |   100K|  9765K|    26M|  2393   (1)| 00:00:01 |
|  10 |         TABLE ACCESS FULL| F_SCREEN_INSTANCE_BUF    |   100K|  9765K|       |    34   (6)| 00:00:01 |
|  11 |     INDEX FAST FULL SCAN | F_SCREEN_INSTANCE_PK     |   500M|    46G|       |  2703 (100)| 00:00:01 |
-------------------------------------------------------------------------------------------------------------

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

   3 - access("INNER_JOIN"."S_ROWID"=("T".ROWID(+)))
   4 - access("S"."PARTIAL_ID"="INNER_JOIN"."PARTIAL_ID"(+) AND 
              "S"."DAY_ID"="INNER_JOIN"."DAY_ID"(+))
   9 - access("S"."DAY_ID"="T"."DAY_ID" AND "S"."PARTIAL_ID"="T"."PARTIAL_ID")
       filter("S"."PARTIAL_ID"="T"."PARTIAL_ID" AND "S"."DAY_ID"="T"."DAY_ID")

【讨论】:

以上是关于优化 MERGE 语句的 SORT MERGE 连接的主要内容,如果未能解决你的问题,请参考以下文章

python merge sort + merge n具有可变长度的排序列表

Oracle merge into 的效率问题

Merge Sort

排序算法--Merge Sorting--归并排序--Merge sort--归并排序

Write a merge sort program

归并排序(Merge Sort)