これは長くなりそうなので、ここに簡単な要約を示します。トップ 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 が 2 桁のフィボナッチ数を生成する行を選択します。
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
そして、これがトップ 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 : コメントで述べたように、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));
質問を読んでテストを実行し、実行計画を理解するのを手伝ってくれた回答者に感謝します。彼らが提案する方法でこの質問を破棄します。