尽管 STOPKEY 优化,top-N 查询做太多工作

Posted

技术标签:

【中文标题】尽管 STOPKEY 优化,top-N 查询做太多工作【英文标题】:top-N query doing too much work in spite of STOPKEY optimization 【发布时间】:2013-05-21 21:30:12 【问题描述】:

这会很长,所以这里有一个简短的摘要来吸引您:我的 在其计划中使用 COUNT STOPKEYORDER BY STOPKEY 的前 N ​​个查询是 仍然无缘无故地变慢。

现在,详细信息。它从一个缓慢的功能开始。在现实生活中它 涉及使用正则表达式的字符串操作。出于演示目的, 这是一个故意愚蠢的递归斐波那契算法。我找到它了 输入到大约 25 时非常快,慢到 30 左右,并且 35岁可笑。

-- I repeat: Please no advice on how to do Fibonacci correctly.
-- This is slow on purpose!
CREATE OR REPLACE FUNCTION tmp_fib (
  n INTEGER
)
  RETURN INTEGER
AS
BEGIN
  IF n = 0 OR n = 1 THEN
    RETURN 1;
  END IF;
  RETURN tmp_fib(n-2) + tmp_fib(n-1);
END;
/

现在一些输入:姓名和数字列表。

CREATE TABLE tmp_table (
  name VARCHAR2(20) UNIQUE NOT NULL,
  num NUMBER(2,0)
);
INSERT INTO tmp_table (name,num)
  SELECT 'Alpha',    10 FROM dual UNION ALL
  SELECT 'Bravo',    11 FROM dual UNION ALL
  SELECT 'Charlie',  33 FROM dual;

这是一个慢查询的例子:使用慢斐波那契函数 选择 num 生成具有双位数的斐波那契数的行。

SELECT p.name, p.num
FROM tmp_table p
WHERE REGEXP_LIKE(tmp_fib(p.num), '(.)\1')
ORDER BY p.name;

这对于 11 和 33 来说是正确的,所以 BravoCharlie 在输出中。 运行大约需要5秒,几乎都是慢的 tmp_fib(33) 的计算。所以我想做一个更快的版本 通过将其转换为前 N 个查询来进行慢查询。 N = 1,看起来像 这个:

SELECT * FROM (
  SELECT p.name, p.num
  FROM tmp_table p
  WHERE REGEXP_LIKE(tmp_fib(p.num), '(.)\1')
  ORDER BY p.name
)
WHERE ROWNUM <= 1;

现在它返回顶部结果,Bravo。但仍然需要 5 秒 跑步!唯一的解释是它还在计算 tmp_fib(33),即使该计算的结果无关紧要 到结果。它应该能够确定Bravo 正在运行 要输出,因此无需测试 WHERE 条件 桌子的其余部分。

我认为也许只需要告诉优化器 tmp_fib 很贵。所以我试着告诉它,像这样:

ASSOCIATE STATISTICS WITH FUNCTIONS tmp_fib DEFAULT COST (999999999,0,0);

这会改变计划中的一些成本数字,但不会使 查询运行得更快。

SELECT * FROM v$version 的输出,以防版本相关:

Oracle Database 11g Enterprise Edition Release 11.2.0.2.0 - 64bit Production
PL/SQL Release 11.2.0.2.0 - Production
CORE    11.2.0.2.0      Production
TNS for 64-bit Windows: Version 11.2.0.2.0 - Production
NLSRTL Version 11.2.0.2.0 - Production

这是 top-1 查询的自动跟踪。它似乎在声称 查询花费了 1 秒,但事实并非如此。它运行了大约 5 秒。

NAME                        NUM
-------------------- ----------
Bravo                        11


Execution Plan
----------------------------------------------------------
Plan hash value: 548796432

-------------------------------------------------------------------------------------
| Id  | Operation               | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT        |           |     1 |    55 |     4  (25)| 00:00:01 |
|*  1 |  COUNT STOPKEY          |           |       |       |            |          |
|   2 |   VIEW                  |           |     1 |    55 |     4  (25)| 00:00:01 |
|*  3 |    SORT ORDER BY STOPKEY|           |     1 |    55 |     4  (25)| 00:00:01 |
|*  4 |     TABLE ACCESS FULL   | TMP_TABLE |     1 |    55 |     3   (0)| 00:00:01 |
-------------------------------------------------------------------------------------

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

   1 - filter(ROWNUM<=1)
   3 - filter(ROWNUM<=1)
   4 - filter( REGEXP_LIKE (TO_CHAR("TMP_FIB"("P"."NUM")),'(.)\1'))

Note
-----
   - dynamic sampling used for this statement (level=2)


Statistics
----------------------------------------------------------
         27  recursive calls
          0  db block gets
         25  consistent gets
          0  physical reads
          0  redo size
        593  bytes sent via SQL*Net to client
        524  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
          1  rows processed

UPdATE:正如我在 cmets 中提到的,INDEX 提示对这个查询有很大帮助。即使它不能很好地转化为我的现实世界场景,也足以被接受为正确答案。具有讽刺意味的是,Oracle 似乎从经验中吸取了教训,现在默认选择INDEX 计划;我必须告诉它NO_INDEX 才能重现原来的缓慢行为。

在实际场景中,我应用了一个更复杂的解决方案,将查询重写为 PL/SQL 函数。以下是我的技术应用于fib 问题的样子:

CREATE OR REPLACE PACKAGE tmp_package IS
  TYPE t_namenum IS TABLE OF tmp_table%ROWTYPE;
  FUNCTION get_interesting_names (howmany INTEGER) RETURN t_namenum PIPELINED;
END;
/

CREATE OR REPLACE PACKAGE BODY tmp_package IS
  FUNCTION get_interesting_names (howmany INTEGER) RETURN t_namenum PIPELINED IS
    CURSOR c IS SELECT name, num FROM tmp_table ORDER BY name;
    rec c%ROWTYPE;
    outcount INTEGER;
  BEGIN
    OPEN c;
    outcount := 0;
    WHILE outcount < howmany LOOP
      FETCH c INTO rec;
      EXIT WHEN c%NOTFOUND;
      IF REGEXP_LIKE(tmp_fib(rec.num), '(.)\1') THEN
        PIPE ROW(rec);
        outcount := outcount + 1;
      END IF;
    END LOOP;
  END;
END;
/

SELECT * FROM TABLE(tmp_package.get_interesting_names(1));

感谢阅读问题并运行测试并帮助我了解执行计划的响应者,我将按照他们的建议处理这个问题。

【问题讨论】:

注意filter 出现的位置 - 排序之前。您不能假设 11 是要检查有效性的第一行。 @Mat 所以这是我的问题:我如何说服 oracle 在排序后进行过滤? 如果不评估表中每一行的where 子句,它怎么知道Bravo 不会被过滤掉? 现在无法尝试,但您需要从按 num asc 排序的表中选择所有作为内部查询,然后使用您的 where 过滤,然后使用 rownum 限制以获得有机会先测试较小的值。 @AlexPoole 如果先进行排序,则首先在 Alpha 上调用过滤器函数,然后是 Bravo,然后按下停止键,不再需要过滤器测试。 【参考方案1】:

后续评论,因为这太大了。在 11.2.0.3 (OEL) 下运行,您的查询:

SELECT * FROM (
  SELECT p.name, p.num
  FROM tmp_table p
  WHERE REGEXP_LIKE(tmp_fib(p.num), '(.)\1')
  ORDER BY p.name
)
WHERE ROWNUM <= 1;

NAME                        NUM
-------------------- ----------
Bravo                        11 

Elapsed: 00:00:00.094
Plan hash value: 1058933870

----------------------------------------------------------------------------------
| Id  | Operation            | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |           |     1 |    25 |     4  (25)| 00:00:01 |
|*  1 |  COUNT STOPKEY       |           |       |       |            |          |
|*  2 |   VIEW               |           |     3 |    75 |     4  (25)| 00:00:01 |
|   3 |    SORT ORDER BY     |           |     3 |    75 |     4  (25)| 00:00:01 |
|   4 |     TABLE ACCESS FULL| TMP_TABLE |     3 |    75 |     3   (0)| 00:00:01 |
----------------------------------------------------------------------------------

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

   1 - filter(ROWNUM<=1)
   2 - filter( REGEXP_LIKE (TO_CHAR("TMP_FIB"("NUM")),'(.)\1'))

Note
-----
   - dynamic sampling used for this statement (level=2)

注意SORT ORDER BY 与您所看到的变化以及相应的rows 值。将 order-by 移动到子选择中看起来更像你的:

SELECT * FROM (
  SELECT * FROM (
    SELECT p.name, p.num
    FROM tmp_table p
    ORDER BY p.name
  )
  WHERE REGEXP_LIKE(tmp_fib(num), '(.)\1')
)
WHERE ROWNUM <= 1;

NAME                        NUM
-------------------- ----------
Bravo                        11 

Elapsed: 00:00:07.894
Plan hash value: 548796432

-------------------------------------------------------------------------------------
| Id  | Operation               | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT        |           |     1 |    25 |   171  (99)| 00:00:03 |
|*  1 |  COUNT STOPKEY          |           |       |       |            |          |
|   2 |   VIEW                  |           |     1 |    25 |   171  (99)| 00:00:03 |
|*  3 |    SORT ORDER BY STOPKEY|           |     1 |    25 |   171  (99)| 00:00:03 |
|*  4 |     TABLE ACCESS FULL   | TMP_TABLE |     1 |    25 |   170  (99)| 00:00:03 |
-------------------------------------------------------------------------------------

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

   1 - filter(ROWNUM<=1)
   3 - filter(ROWNUM<=1)
   4 - filter( REGEXP_LIKE (TO_CHAR("TMP_FIB"("P"."NUM")),'(.)\1'))

Note
-----
   - dynamic sampling used for this statement (level=2)

不知道这在您的实际场景中会有多大帮助或实用性,但在这种情况下(无论如何,在我的环境中),在所有获取的列中添加一个索引 - 以获得完整的索引扫描而不是完整的表扫描 - 似乎改变了行为:

CREATE INDEX tmp_index ON tmp_table(name, num);

index TMP_INDEX created.

SELECT * FROM (
  SELECT p.name, p.num
  FROM tmp_table p
  WHERE REGEXP_LIKE(tmp_fib(p.num), '(.)\1')
  ORDER BY p.name
)
WHERE ROWNUM <= 1;

NAME                        NUM
-------------------- ----------
Bravo                        11 

Elapsed: 00:00:00.093
Plan hash value: 1841475998

-------------------------------------------------------------------------------
| Id  | Operation         | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |           |     1 |    25 |     1   (0)| 00:00:01 |
|*  1 |  COUNT STOPKEY    |           |       |       |            |          |
|*  2 |   VIEW            |           |     3 |    75 |     1   (0)| 00:00:01 |
|   3 |    INDEX FULL SCAN| TMP_INDEX |     3 |    75 |     1   (0)| 00:00:01 |
-------------------------------------------------------------------------------

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

   1 - filter(ROWNUM<=1)
   2 - filter( REGEXP_LIKE (TO_CHAR("TMP_FIB"("NUM")),'(.)\1'))

Note
-----
   - dynamic sampling used for this statement (level=2)

SELECT * FROM (
  SELECT * FROM (
    SELECT p.name, p.num
    FROM tmp_table p
    ORDER BY p.name
  )
  WHERE REGEXP_LIKE(tmp_fib(num), '(.)\1')
)
WHERE ROWNUM <= 1;

NAME                        NUM
-------------------- ----------
Bravo                        11 

Elapsed: 00:00:00.093
Plan hash value: 1841475998

-------------------------------------------------------------------------------
| Id  | Operation         | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |           |     1 |    25 |     1   (0)| 00:00:01 |
|*  1 |  COUNT STOPKEY    |           |       |       |            |          |
|   2 |   VIEW            |           |     1 |    25 |     1   (0)| 00:00:01 |
|*  3 |    INDEX FULL SCAN| TMP_INDEX |     1 |    25 |     1   (0)| 00:00:01 |
-------------------------------------------------------------------------------

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

   1 - filter(ROWNUM<=1)
   3 - filter( REGEXP_LIKE (TO_CHAR("TMP_FIB"("P"."NUM")),'(.)\1'))

Note
-----
   - dynamic sampling used for this statement (level=2)

顺便说一句,在我用任何rownum 变体运行此操作后,我最终开始收到ORA-01000: maximum open cursors exceeded 错误。我在每次运行结束时丢弃对象但保持连接。我认为这暗示了某个地方的另一个错误,尽管可能与您所看到的无关,因为即使在索引扫描时也会发生这种情况。

【讨论】:

看起来你得到了我想要的优化,过滤器被移到了排序之上。不知道是不是11.2.0.2和11.2.0.3的区别 很可能是补丁集中的优化器调整,是的。我希望我能想到一个提示,使我的查询在我的环境中表现得像你的,从而可能使你的表现像你想要的那样,但还没有想出任何东西。 我怀疑您希望使用 NO_PUSH_PRED 优化器提示来强制执行此行为。 @DavidAldridge - 我在原始查询上尝试过(没有额外的索引),但我无法让它产生任何效果,可能是因为使用不正确,或者因为它没有连接可以作用。我只是提出一些想法供 OP 考虑,但我对真实情况(甚至这个测试,TBH)的工作/稳定没有任何信心。 @AlexPoole 抱歉,我的意思是特别针对具有两个嵌套内嵌视图的版本——第二个代码块。【参考方案2】:

兴趣显然消失了,所以我将在自我回答中总结可能的解决方案。

    升级 - 较新的 Oracle 似乎可以更好地优化此类查询。 使用 INDEX 提示使内部查询按已排序的顺序检索行,从而使 STOPKEY 能够正常工作。 在 PL/SQL 中重写,内部查询作为游标。从光标中获取,直到获得足够的匹配项,然后关闭它。

【讨论】:

以上是关于尽管 STOPKEY 优化,top-N 查询做太多工作的主要内容,如果未能解决你的问题,请参考以下文章

总结代码而不在 R 中做太多参数

Choreographer: Skipped frames : 应用程序可能在其主线程上做太多工作

对服务使用 AsyncTask 类仍然得到“跳过 31 帧!应用程序可能在其主线程上做太多工作。”。为啥?

Flink SQL ---Top-N ,Window Top-N

Oracle子查询相关内容(包含TOP-N查询和分页查询)

抖音 Android 性能优化系列:启动优化实践