将LIKE运算符与DETERMINISTIC函数一起使用时,Oracle执行计划

时间:2011-03-17 14:52:20

标签: oracle sql-execution-plan sql-like deterministic

现在,当我在DETERMINISTIC运算符的右侧使用LIKE函数时,Oracle执行计划会出现严重问题。这是我的情况:

情况

我认为执行像这样的简化(简化)是明智的:

SELECT [...]
FROM customers cust
JOIN addresses addr ON addr.cust_id = cust.id
WHERE special_char_filter(cust.surname) like special_char_filter(?)

我会将?绑定到'Eder%'之类的内容。现在customersaddresses是非常大的表格。这就是使用索引很重要的原因。当然,addresses.cust_id上有一个常规索引。但我也在special_char_filter(customers.surname)上创建了一个基于函数的索引,它的工作效果非常好。

麻烦

问题是,涉及like子句的上述查询使用addresses上的FULL TABLE SCANS创建执行计划。看起来这个查询中的某些内容使Oracle无法使用addresses.cust_id上的索引。

解决方法

我发现,问题的解决方案是:

SELECT [...]
FROM customers cust
JOIN addresses addr ON addr.cust_id = cust.id
WHERE special_char_filter(cust.surname) like ?

我从like运算符的右侧删除了(DETERMINISTIC!)函数,并在Java中预先计算了绑定变量。现在这个查询超快,没有任何FULL TABLE扫描。这也非常快(虽然不相同):

SELECT [...]
FROM customers cust
JOIN addresses addr ON addr.cust_id = cust.id
WHERE special_char_filter(cust.surname) = special_char_filter(?)

混乱

我不明白这一点。在like运算符的右侧有确定性函数有什么问题?我在Oracle 11.2.0.1.0中已经观察到了这一点

3 个答案:

答案 0 :(得分:2)

查询中可能完全没有任何内容。基于成本的优化器可能只是混淆并认为FULL TABLE SCAN更快。您是否尝试在查询中使用HINT,强制Oracle使用您的索引?

答案 1 :(得分:2)

问题是Oracle不知道“special_char_filter(?)”将返回什么。如果它返回'%',那么使用索引将非常慢,因为一切都匹配。如果它返回'A%',它可能也会很慢(假设所有字母的分布相等),大约4%的行会匹配。如果它返回'%FRED%',它将不会返回很多行,但是使用索引范围扫描会表现不佳,因为行可能位于索引的开头,中间或末尾,所以它必须做整个索引。

如果您知道special_char_filter将始终返回一个开头至少包含三个“实心”字符的字符串,那么您可能会有更好的运气

选择[...] 来自客户 JOIN地址addr ON addr.cust_id = cust.id 在哪里,special_char_filter(cust.surname)就像special_char_filter(?) AND substr(special_char_filter(cust.surname),1,3)= substr(special_char_filter(?),1,3)

在子网上使用FBI(special_char_filter(cust.surname),1,3)

虽然如果预先计算java中的结果,那么坚持下去。

除此之外,我可能会查看Oracle Text的匹配项。

答案 2 :(得分:1)

下面的脚本显示了我用于在ADDRESSES索引上获取索引范围扫描的步骤。在您查看详细信息之前,您可能想要运行整个事情。如果没有得到两个索引范围扫描 对于最后两个查询,那么它可能与我们的版本,设置等有所不同。我正在使用10.2.0.1.0。

如果您确实看到了所需的计划,那么您可能希望逐步修改我的脚本以使其更准确地反映真实数据,并尝试找到使其中断的确切更改。希望我的设置至少接近真实的东西,并且不会遗漏任何细节 它与你的确切问题无关。

这是一个奇怪的问题,我不明白这里发生的一切。例如,我不知道为什么use_nl有效,但索引提示没有。

(请注意,我的执行时间基于重复执行。第一次运行时,某些查询可能会更慢,因为数据未缓存。)

--create tables
create table customers (id number, surname varchar2(100), other varchar2(100));
create table addresses (cust_id number, other varchar2(100));

--create data and indexes
insert into customers select level, 'ASDF'||level, level from dual connect by level <= 1000000;
insert into addresses select level, level from dual connect by level <= 1000000;
create index customers_id on customers(id);
create index addresses_cust_id on addresses(cust_id);
create index customers_special_char_filter on customers(special_char_filter(surname));

--create function
create or replace function special_char_filter(surname in varchar) return varchar2 deterministic is
begin
    return replace(surname, 'bad value!', null);
end;
/

--gather stats
begin
    dbms_stats.gather_table_stats(ownname => user, tabname => 'CUSTOMERS', cascade => true);
    dbms_stats.gather_table_stats(ownname => user, tabname => 'ADDRESSES', cascade => true);
end;
/

set autotrace on;

--Index range scan on CUSTOMERS_SPECIAL_CHAR_FILTER, but full table scan on ADDRESSES
--(0.2 seconds)
SELECT *
FROM customers cust
JOIN addresses addr ON addr.cust_id = cust.id
WHERE special_char_filter(cust.surname) like special_char_filter('ASDF100000bad value!%');

--This uses the addresses index but it does an index full scan.  Not really what we want.
--I'm not sure why I can't get an index range scan here.
--Various other index hints also failed here.  For example, no_index_ffs won't stop an index full scan.
--(1 second)
SELECT /*+ index(addr addresses_cust_id) */ *
FROM customers cust
JOIN addresses addr ON addr.cust_id = cust.id
WHERE special_char_filter(cust.surname) like special_char_filter('ASDF100000bad value!%');


--Success!  With this hint both indexes are used and it's super-fast.
--(0.02 seconds)
SELECT /*+ use_nl(cust addr) */ *
FROM customers cust
JOIN addresses addr ON addr.cust_id = cust.id
WHERE special_char_filter(cust.surname) like special_char_filter('ASDF100000bad value!%');


--But forcing the index won't always be a good idea, for example when the value starts with '%'.
--(1.2 seconds)
SELECT /*+ use_nl(cust addr) */ *
FROM customers cust
JOIN addresses addr ON addr.cust_id = cust.id
WHERE special_char_filter(cust.surname) like special_char_filter('%ASDF100000bad value!%');