文書化されていないヒントを使用するuse_concat(or_predicates(1))
か、を使用してクエリを書き直す必要がありますUNION ALL
。オプティマイザーは、関数に関係なく、これらのタイプの述部で問題を抱えています。
期待される計画
次のような計画が必要です。
------------------------------------------------------
| Id | Operation | Name |
------------------------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | CONCATENATION | |
|* 2 | FILTER | |
|* 3 | TABLE ACCESS FULL | SOME_TABLE |
|* 4 | FILTER | |
|* 5 | TABLE ACCESS FULL | SOME_TABLE |
|* 6 | FILTER | |
|* 7 | TABLE ACCESS BY INDEX ROWID| SOME_TABLE |
|* 8 | INDEX UNIQUE SCAN | SYS_C0010268 |
------------------------------------------------------
のFILTER
は、Explain PlanのセクションのOperation
典型的なものとは大きく異なります。これらは条件を評価し、実行時に使用する実行計画の部分を決定します。関数に渡される値に応じて、プランは完全なテーブル スキャン (名前または日付の非選択的な述語の場合) を使用するか、一意のインデックス スキャン (ID の非常に選択的な述語の場合) を使用します。filter
Predicate Information
FILTER
これは、まさにあなたのようなクエリで必要なものです。また、クエリに少数AND
の としかない場合はOR
、FILTER
.
実績計画
しかし実際には、複雑な述語を使用すると、計画は次のようになります。
----------------------------------------
| Id | Operation | Name |
----------------------------------------
| 0 | SELECT STATEMENT | |
|* 1 | TABLE ACCESS FULL| SOME_TABLE |
----------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("SOME_FUNCTION"('CONTEXT')='context of filtering by name'
AND "T"."NAME" LIKE '%'||"ANOTHER_FUNCTION"('NAME')||'%' OR
"SOME_FUNCTION"('CONTEXT')='context of taking actual rows' AND
"T"."START_DATE"<=TO_DATE("ANOTHER_FUNCTION"('ACTUAL_DATE'),'...') AND
"T"."END_DATE">=TO_DATE("ANOTHER_FUNCTION"('ACTUAL_DATE'),'...') OR
"SOME_FUNCTION"('CONTEXT')='context of selecting by id' AND
"T"."ID"=TO_NUMBER("ANOTHER_FUNCTION"('ID')))
テーブル全体のスキャンが必ずしも悪いわけではありません。しかし、単一の主キー値を選択するのはかなりひどいものです。
サンプル スキーマ
テーブルと 100 万のサンプル行を作成します。選択性の高い列もあれば、選択性の低い列もあります。それらはすべてヒストグラムを持っているため、オプティマイザーは処理するための多くの優れた情報を持っています.
drop table some_table purge;
create table some_table
(
id number primary key,
name varchar2(100),
start_date date,
end_date date
);
begin
for i in 1 .. 10 loop
insert into some_table
select
level+(i*100000),
'Name '||mod(level, 5),
date '2000-01-01' + mod(level, 10000),
date '2010-01-01' + mod(level, 10000)
from dual
connect by level <= 100000;
end loop;
end;
/
begin
dbms_stats.gather_table_stats(user, 'SOME_TABLE'
,method_opt => 'for all columns size 254');
end;
/
サンプル関数
これらの関数は非常に静的であり、オプティマイザはそれを認識している必要があります。この例ではsome_function
、何にも一致しない方法で使用しています。これは一種の最良のシナリオです。このクエリが何も返さないことは、Oracle にとって非常に簡単にわかるはずです。
--Static functions.
create or replace function some_function(p_context in varchar2) return varchar2 is
begin
return p_context;
end;
/
--Btw, returning stringly-typed data is almost always a horrible idea.
--(Althogh if you're dealing with sys_context you may not have a choice.)
create or replace function another_function(p_type in varchar2) return varchar2 is
begin
if p_type = 'ID' then
return '1';
elsif p_type = 'NAME' then
return 'Name 1';
elsif p_type = 'ACTUAL_DATE' then
return '2000-01-01';
end if;
end;
/
デフォルト - FILTER 操作のない不適切な計画
デフォルトのプランは非常に貧弱です。クエリはほぼ 0 秒で実行されるはずですが、代わりに完全なテーブル スキャンを実行する必要があります。
explain plan for
SELECT * FROM some_table t
WHERE
(
some_function('CONTEXT') = 'context of selecting by id'
AND t.id = TO_NUMBER(another_function('ID'))
)
OR (
some_function('CONTEXT') = 'context of filtering by name'
AND t.name LIKE '%' || another_function('NAME') || '%'
)
OR (
some_function('CONTEXT') = 'context of taking actual rows'
AND TO_DATE(another_function('ACTUAL_DATE'), '...')
BETWEEN t.start_date AND t.end_date
);
select * from table(dbms_xplan.display);
Plan hash value: 3038250352
--------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 525 | 14700 | 1504 (17)| 00:00:01 |
|* 1 | TABLE ACCESS FULL| SOME_TABLE | 525 | 14700 | 1504 (17)| 00:00:01 |
--------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("SOME_FUNCTION"('CONTEXT')='context of filtering by name'
AND "T"."NAME" LIKE '%'||"ANOTHER_FUNCTION"('NAME')||'%' OR
"SOME_FUNCTION"('CONTEXT')='context of taking actual rows' AND
"T"."START_DATE"<=TO_DATE("ANOTHER_FUNCTION"('ACTUAL_DATE'),'...') AND
"T"."END_DATE">=TO_DATE("ANOTHER_FUNCTION"('ACTUAL_DATE'),'...') OR
"SOME_FUNCTION"('CONTEXT')='context of selecting by id' AND
"T"."ID"=TO_NUMBER("ANOTHER_FUNCTION"('ID')))
use_concat(or_predicates(1)) - FILTER を使用した適切な計画
ヒントはUSE_CONCAT
、クエリを個別のステップに変換しUNION ALL
ます。そして、各述語は単純で、FILTER
操作があります。残念ながらUSE_CONCAT
、いくつかの奇妙な制限があります。インデックスが使用されている場合にのみ機能する場合があります (My Oracle Support のドキュメント 259741.1 を参照してください)。また、まったく機能しない場合や、回避策が機能しない場合があり、12c ではまだ修正されていません (ドキュメント 14545269.8)。
追加or_predicates(1)
すると機能しますが、完全に文書化されていません。
explain plan for
SELECT --+ use_concat(or_predicates(1))
*
FROM some_table t
WHERE
(
some_function('CONTEXT') = 'context of selecting by id'
AND t.id = TO_NUMBER(another_function('ID'))
)
OR (
some_function('CONTEXT') = 'context of filtering by name'
AND t.name LIKE '%' || another_function('NAME') || '%'
)
OR (
some_function('CONTEXT') = 'context of taking actual rows'
AND TO_DATE(another_function('ACTUAL_DATE'), '...')
BETWEEN t.start_date AND t.end_date
);
select * from table(dbms_xplan.display);
Plan hash value: 1618041905
----------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 52500 | 1435K| 2721 (8)| 00:00:01 |
| 1 | CONCATENATION | | | | | |
|* 2 | FILTER | | | | | |
|* 3 | TABLE ACCESS FULL | SOME_TABLE | 2500 | 70000 | 1362 (8)| 00:00:01 |
|* 4 | FILTER | | | | | |
|* 5 | TABLE ACCESS FULL | SOME_TABLE | 49999 | 1367K| 1356 (7)| 00:00:01 |
|* 6 | FILTER | | | | | |
|* 7 | TABLE ACCESS BY INDEX ROWID| SOME_TABLE | 1 | 28 | 3 (0)| 00:00:01 |
|* 8 | INDEX UNIQUE SCAN | SYS_C0010269 | 1 | | 2 (0)| 00:00:01 |
----------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter("SOME_FUNCTION"('CONTEXT')='context of taking actual rows')
3 - filter("T"."START_DATE"<=TO_DATE("ANOTHER_FUNCTION"('ACTUAL_DATE'),'...') AND
"T"."END_DATE">=TO_DATE("ANOTHER_FUNCTION"('ACTUAL_DATE'),'...'))
4 - filter("SOME_FUNCTION"('CONTEXT')='context of filtering by name')
5 - filter("T"."NAME" LIKE '%'||"ANOTHER_FUNCTION"('NAME')||'%' AND
(LNNVL("SOME_FUNCTION"('CONTEXT')='context of taking actual rows') OR
LNNVL("T"."START_DATE"<=TO_DATE("ANOTHER_FUNCTION"('ACTUAL_DATE'),'...')) OR
LNNVL("T"."END_DATE">=TO_DATE("ANOTHER_FUNCTION"('ACTUAL_DATE'),'...'))))
6 - filter("SOME_FUNCTION"('CONTEXT')='context of selecting by id')
7 - filter((LNNVL("SOME_FUNCTION"('CONTEXT')='context of filtering by name') OR
LNNVL("T"."NAME" LIKE '%'||"ANOTHER_FUNCTION"('NAME')||'%')) AND
(LNNVL("SOME_FUNCTION"('CONTEXT')='context of taking actual rows') OR
LNNVL("T"."START_DATE"<=TO_DATE("ANOTHER_FUNCTION"('ACTUAL_DATE'),'...')) OR
LNNVL("T"."END_DATE">=TO_DATE("ANOTHER_FUNCTION"('ACTUAL_DATE'),'...'))))
8 - access("T"."ID"=TO_NUMBER("ANOTHER_FUNCTION"('ID')))
UNION ALL - FILTER を使用した優れた計画
クエリを手動で展開する方がおそらく安全な方法です。ただし、クエリがどれほど複雑かによっては、非常に見苦しくなる場合があります。
explain plan for
SELECT * FROM some_table t
WHERE some_function('CONTEXT') = 'context of selecting by id' AND t.id = TO_NUMBER(another_function('ID'))
union all
SELECT * FROM some_table t
WHERE some_function('CONTEXT') = 'context of filtering by name' AND t.name LIKE '%' || another_function('NAME') || '%'
union all
SELECT * FROM some_table t
WHERE some_function('CONTEXT') = 'context of taking actual rows' AND TO_DATE(another_function('ACTUAL_DATE'), '...') BETWEEN t.start_date AND t.end_date
select * from table(dbms_xplan.display);
(Plan not shown - it's basically the same as the `USE_CONCAT` version.)
CASE - FILTER のない悪い計画
述語を単一CASE
に書き直すことは良い考えでしたが、ここではうまくいかないようです。それは私の特定の例の問題だけかもしれませんが。
explain plan for
SELECT *
FROM some_table t
WHERE
case
when some_function('CONTEXT') = 'context of selecting by id'
AND t.id = TO_NUMBER(another_function('ID')) then 1
when some_function('CONTEXT') = 'context of filtering by name'
AND t.name LIKE '%' || another_function('NAME') || '%' then 1
when some_function('CONTEXT') = 'context of taking actual rows'
AND TO_DATE(another_function('ACTUAL_DATE'), '...') BETWEEN t.start_date AND t.end_date then 1
else 0 end
= 1;
select * from table(dbms_xplan.display);
(Plan not shown - it's basically the same as the default version with the full table scan.)