【问题标题】:Improve performance of NOT EXISTS in case of large tables在大表的情况下提高 NOT EXISTS 的性能
【发布时间】:2022-01-13 15:42:36
【问题描述】:

我想要完成的是从一个表中获取与基于特定过滤器的另一个表不匹配的行。这两个表比较大,所以我试图根据一定的时间范围过滤它们。

到目前为止我所经历的步骤。

  1. 从“T1”获取过去 3 天的 ID
SELECT
id 
FROM T1
WHERE STARTTIME BETWEEN '3 days ago' AND 'now';

执行时间4.5s

  1. 从“T2”获取过去 3 天的 ID
SELECT
id 
FROM T2
WHERE STARTTIME BETWEEN '3 days ago' AND 'now';

执行时间2.5s

  1. 现在我尝试使用NOT EXISTS 将两个语句的结果合并为一个
SELECT
CID
FROM T1
WHERE STARTTIME BETWEEN '3 days ago' AND 'now'
AND NOT EXISTS (
  SELECT NULL FROM T2
  WHERE T1.ID = T2.ID 
  AND STARTTIME BETWEEN '3 days ago' AND 'now'
);

执行时间23s

我还尝试了 this answer 中的 INNER JOIN 逻辑,认为它是有道理的,但我没有得到任何结果,所以我无法正确评估。

有没有更好的方法来构造这个可能导致更快执行时间的语句?

19.01.2022 - 基于 cmets 的更新

  1. 预期结果可以包含 1 到 10 000 之间的任意行数

  2. 使用的列具有以下索引:

CREATE INDEX IX_T1_CSTARTTIME
   ON T1 (CSTARTTIME ASC)
   TABLESPACE MYHOSTNAME_DATA1;
CREATE INDEX IX_T2_CSTARTTIME
   ON T2 (CSTARTTIME ASC)
   TABLESPACE MYHOSTNAME_DATA2;

注意:刚刚注意到索引位于不同的表空间,这也可能是一个潜在问题吗?

  1. Marmite Bomber 的优秀 cmets 之后,以下是语句的执行计划:

    ---------------------------------------------------------------------------------------------
    | Id  | Operation            | Name  | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
    --------------------------------------------------------------------------------------
    |   0 | SELECT STATEMENT     |       | 21773 |  2019K|       |  1817K  (1)| 00:01:12 |
    |*  1 |  HASH JOIN RIGHT ANTI|       | 21773 |  2019K|   112M|  1817K  (1)| 00:01:12 |
    |*  2 |   TABLE ACCESS FULL  | T2     |  2100K|    88M|       |  1292K  (1)| 00:00:51 |
    |*  3 |   TABLE ACCESS FULL  | T1     |  2177K|   105M|       |   512K  (1)| 00:00:21 |
    ---------------------------------------------------------------------------------------------
    
    Predicate Information (identified by operation id):
    ---------------------------------------------------
    
    1 - access("T2"."ID"="T1"."ID")
    2 - filter("STARTTIME">=1642336690000 AND "T2"."ID" IS NOT NULL 
               AND "STARTTIME"<=1642595934000)
    3 - filter("STARTTIME">=1642336690000 AND 
               "STARTTIME"<=1642595934000)
    
    Column Projection Information (identified by operation id):
    -----------------------------------------------------------
    
    1 - (#keys=1; rowset=256) "T1"."ID"[CHARACTER,38]
    2 - (rowset=256) "T2"."ID"[CHARACTER,38]
    3 - (rowset=256) "ID"[CHARACTER,38]
    

【问题讨论】:

  • 问题本身可能导致反连接的时间更长(“不存在”条件)。例如,如果每个表有 100 万行,并且每个查询选择 10,000 行(每个表中的最后三天),则每个表上的三天过滤器可能需要几秒钟。但是,反连接步骤必须将第一个表中的 10,000 行与第二个表中的 10,000 行进行比较;也就是 1 亿次比较,这当然会比最初的过滤花费更长的时间。
  • 您没有告诉我们两个表上存在哪些索引(如果有的话)。 ID 列和日期列上的单独索引可能会有所帮助;日期列上的索引可能会使初始步骤更快,但真正重要的索引将是 ID 列上的索引,因为它们会影响反连接的执行。
  • 顺便说一句:将您的 NOT EXISTS 条件重写为 NOT IN 条件或联接(加上一些条件)不会使您的查询更快。 Oracle 使用最有效的方法(或者优化器“认为”是最有效的方法,无论如何)将您的条件(无论您使用哪种语法)重新写入其自己的连接版本。这是错误的看法;您的查询很好,就像现在一样。查找索引、统计信息 - 它们是最新的等等,而不是查询的结构。
  • @Yanis Petras,您可以尝试结合 LEFT JOIN 和 WHERE 子句:SELECT T1.CID FROM T1 LEFT JOIN T2 ON T1.ID = T2.ID AND T2.STARTTIME BETWEEN '3 天前' AND 'now' WHERE T1.STARTTIME BETWEEN '3 days ago' AND 'now' AND T2.ROWID 为 NULL
  • 就像我说的(并且您刚刚确认),使用不同的语法编写相同的查询将无济于事。 Oracle 在内部将它们全部重写为它发送执行的同一查询。关于索引:看看你是否通过在 ID 上添加索引得到有意义的改进。 (在大型组织中,您会先在开发环境中执行此操作,然后再要求您的 DBA 允许您在生产环境中创建相同的索引。)

标签: sql oracle join query-optimization


【解决方案1】:

有没有更好的方法来构造这个可能导致更快执行时间的语句?

您的基本职责是编写SQL语句,Oracle的基本职责是制定执行计划

如果您不满意(但您应该知道使用NOT EXISTS 的两个来源的组合将花费比提取数据的时间总和更长的时间来源)您的第一步应该是验证执行计划(而不是尝试重写声明)。

查看更多详细信息如何进行here

EXPLAIN PLAN  SET STATEMENT_ID = 'stmt1' into   plan_table  FOR
SELECT
PAD
FROM T1
WHERE STARTTIME BETWEEN date'2021-01-11' AND date'2021-01-13'
AND NOT EXISTS (
  SELECT NULL FROM T2
  WHERE T1.ID = T2.ID 
  AND STARTTIME BETWEEN date'2021-01-11' AND date'2021-01-13'
);

SELECT * FROM table(DBMS_XPLAN.DISPLAY('plan_table', 'stmt1','ALL'));

这是你应该看到的

-----------------------------------------------------------------------------
| Id  | Operation            | Name | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |      |  1999 |   150K| 10175   (1)| 00:00:01 |
|*  1 |  HASH JOIN RIGHT ANTI|      |  1999 |   150K| 10175   (1)| 00:00:01 |
|*  2 |   TABLE ACCESS FULL  | T2   |  2002 | 26026 |  4586   (1)| 00:00:01 |
|*  3 |   TABLE ACCESS FULL  | T1   |  4002 |   250K|  5589   (1)| 00:00:01 |
-----------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
 
   1 - access("T1"."ID"="T2"."ID")
   2 - filter("STARTTIME"<=TO_DATE(' 2021-01-13 00:00:00', 'syyyy-mm-dd 
              hh24:mi:ss') AND "STARTTIME">=TO_DATE(' 2021-01-11 00:00:00', 
              'syyyy-mm-dd hh24:mi:ss'))
   3 - filter("STARTTIME"<=TO_DATE(' 2021-01-13 00:00:00', 'syyyy-mm-dd 
              hh24:mi:ss') AND "STARTTIME">=TO_DATE(' 2021-01-11 00:00:00', 
              'syyyy-mm-dd hh24:mi:ss'))

请注意,hash join(此处为anti,由于not exists)是连接两个大行源的最佳方式。另请注意,该计划不使用索引。原因是一样的——访问大数据你不想遍历索引。

与低基数行源 (OTPL) 的情况相反,您希望看到 索引访问NESTED LOOPS ANTI

有时 Oracle 感到困惑(例如,在看到 陈旧的统计数据时)并决定采用 NESTED LOOP 方式,即使是对于大数据 - 这会导致经过很长时间。

这至少可以帮助您确定是否有问题。

【讨论】:

    【解决方案2】:

    也许一个简单的MINUS 操作将完成您正在寻找的:

        select id 
          from ( select id 
                   from t1
                  where starttime between '3 days ago' and 'now' 
                 MINUS
                 select id 
                   from t2
                  where starttime between '3 days ago' and 'now' 
               );
    

    因为你实际上定义了starttime between '3 days ago' and 'now'。这实际上使用了您当前的查询,因为 MINUS 操作从第一个中删除那些确实存在于第二个中的值并返回结果。请在此处查看MINUS demo

    【讨论】:

    • 感谢您的建议。这个实际上表现更差 - 33s。
    猜你喜欢
    • 2010-10-07
    • 2014-04-25
    • 1970-01-01
    • 2010-11-03
    • 2015-07-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-19
    • 1970-01-01
    相关资源
    最近更新 更多