Планы выполнения Oracle при использовании оператора LIKE с функцией DETERMINISTIC

Теперь у меня есть действительно сложная вещь с планами выполнения Oracle, приводящими в хаос, когда я использую функцию DETERMINISTIC справа от оператора LIKE. Это моя ситуация:

Ситуация

Я подумал, что было бы разумно выполнить такой запрос (упрощенный):

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. Похоже, что-то в этом запросе не позволяет Oracle использовать индексы для addresses.cust_id.

Обходной путь

Я узнал, что решение моей проблемы таково:

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

Я удалил функцию (DETERMINISTIC !) из правой части аналогичного оператора и предварительно вычислил переменную связывания в Java. Теперь этот запрос сверхбыстрый, без ПОЛНОГО СКАНИРОВАНИЯ ТАБЛИЦ. Это тоже очень быстро (хотя и не эквивалентно):

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.


person Lukas Eder    schedule 17.03.2011    source источник
comment
версии оракула очень важны для этого типа вопросов. Что такое версия Oracle rdbms?   -  person ik_zelf    schedule 17.03.2011
comment
Я внимательно наблюдал эту проблему в версии 11.2.0.1.0. Вполне вероятно, что он появится и в 10-граммовой версии. Я не могу официально подтвердить это, хотя   -  person Lukas Eder    schedule 17.03.2011


Ответы (3)


В приведенном ниже сценарии показаны шаги, которые я использовал для сканирования диапазона индекса в индексе 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!%');
person Jon Heller    schedule 19.03.2011
comment
извините, я не получил уведомления о вашем ответе. Выглядит как хороший эталон. С другой стороны, вы встроили свои строковые литералы и не использовали переменные связывания, что может вызвать некоторые проблемы из-за просмотра переменных связывания. Я до сих пор путаюсь в этих вещах... - person Lukas Eder; 13.04.2011
comment
Я проверил ваш скрипт. Забавно, что последний оператор, как правило, самый быстрый в моей БД, даже с ведущим %. С другой стороны, оценка строк, как правило, сильно отличается от фактических строк в планах выполнения запросов при использовании подсказки /*+ use_nl */... - person Lukas Eder; 13.04.2011
comment
Я приму ваш ответ, так как он ближе всего к любому решению. - person Lukas Eder; 13.04.2011

В запросе может вообще ничего не быть. Оптимизатор, основанный на затратах, может просто запутаться и подумать, что ПОЛНОЕ СКАНИРОВАНИЕ ТАБЛИЦЫ быстрее. Вы пытались использовать HINT в запросе, заставляя Oracle использовать ваш индекс?

person dseibert    schedule 17.03.2011
comment
Да, я перепробовал почти все подсказки, которые нашел здесь: psoug.org/reference/hints.html безуспешно. Для JOIN никогда не использовался индекс. Но меня смущает, что Oracle не понимает, что расчет special_char_filter(?) только один раз перед расчетом плана выполнения поможет... Технически special_char_filter(?) является константой. - person Lukas Eder; 17.03.2011
comment
@ Лукас Эдер: Вы пробовали /*+ use_nl(cust addr) */? Ни одна из подсказок индекса не помогла, но у меня сработало use_nl. - person Jon Heller; 18.03.2011
comment
Хм, я не пробовал. Но почему вы думаете, что вложенный цикл (а не хеш-соединение) заставит использовать индекс? Обратите внимание, есть еще /*+ use_nl_using_index(...) */, который мне не подошел - person Lukas Eder; 18.03.2011
comment
Хм, /*+ use_nl */ тоже не сработало. Вложенный цикл не применяется, я все еще получаю хэш-соединение с полным сканированием таблицы... Это действительно искривлено. - person Lukas Eder; 18.03.2011

Проблема в том, что 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)

с ФБР на substr(special_char_filter(cust.surname),1,3)

Хотя, если предварительный расчет результата в java работает, то придерживайтесь его.

Кроме того, я бы, вероятно, посмотрел на Oracle Text для совпадений.

person Gary Myers    schedule 17.03.2011
comment
Спасибо за ваш вклад. К сожалению, я не знаю содержимого ?, так как это ввод пользователя. Я просто подумал, что должен быть какой-то способ, которым Oracle может вычислить результат этой функции до расчета плана выполнения. Oracle Text будет представлен очень скоро. Я надеюсь, что это решит все мои проблемы! :-) - person Lukas Eder; 18.03.2011
comment
вы используете prepared statement - они весьма полезны для подготовки плана выполнения однажды и многократного запуска. Вы пробовали с простым заявлением (не подготовленным)? - person Mat; 18.03.2011
comment
@Мэт, ты прав. Это логично, я об этом не подумал. Однако в нашей архитектуре простое выражение не подходит. Но я протестировал простой оператор непосредственно в базе данных, используя Toad (где я провел анализ своего плана выполнения). На самом деле это ничего не изменило. - person Lukas Eder; 18.03.2011