现在,当我在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%'
之类的内容。现在customers
和addresses
是非常大的表格。这就是使用索引很重要的原因。当然,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中已经观察到了这一点
答案 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!%');