这会很长,所以这里有一个快速的总结来吸引你:我的 top-N 查询在它COUNT STOPKEY
的ORDER BY STOPKEY
计划中仍然很慢,没有充分的理由。
现在,细节。它从一个缓慢的功能开始。在现实生活中,它涉及使用正则表达式的字符串操作。出于演示目的,这里有一个故意愚蠢的递归斐波那契算法。我发现它对于高达 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 来说是正确的,所以Bravo
和Charlie
在输出中。运行大约需要 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
更新:正如我在评论中提到的,INDEX
提示对这个查询有很大帮助。即使它不能很好地转化为我的现实世界场景,也足以被接受为正确答案。具有讽刺意味的是,甲骨文似乎已经吸取了经验,现在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));
感谢阅读问题并运行测试并帮助我理解执行计划的响应者,我将按照他们的建议处理这个问题。