Как вы препятствуете оптимизатору Oracle на основе затрат делать плохие оптимизации?

Рассмотрим следующий сценарий. У меня есть стол stupid_table) в схеме, которую я не могу контролировать. Это третье лицо, запрещено. Не обидчивый Я могу запросить его, но не добавлять индексы или новые таблицы или изменять дизайн.

Каждый столбец в stupid_table это VARCHAR2(50 BYTE)Есть много столбцов, но мне нужно только два из них: row_type а также magic_number, magic_number заполняется строковым представлением целого числа, но только там, где row_type установлен в 'DATA'Мне нужны только магические числа, которые больше нуля.

SELECT TO_NUMBER(magic_number)
FROM stupid_table
WHERE row_type = 'DATA'
AND TO_NUMBER(magic_number) > 0;

Это приводит к ошибке Oracle "неверное число", поскольку оптимизатор на основе затрат (CBO) выбирает оценку TO_NUMBER до проверки row_type и есть целая куча строк с другим row_type и другое использование для magic_number поле.

Хорошо, как насчет того, чтобы сначала отфильтровать строки, а затем выполнить сравнение?

SELECT TO_NUMBER(t.magic_number)
FROM (
    SELECT magic_number
    FROM stupid_table
    WHERE row_type = 'DATA'
) t
AND TO_NUMBER(t.magic_number) > 0;

Теперь CBO, кажется, решает, что запрос довольно прост и игнорирует хитрость, которую я использовал, что дает план запроса, идентичный оригиналу.

Наконец, в отчаянии, я прибегаю к грязным взломам: использование /*+RULE*/ подсказка запроса, чтобы заставить Oracle использовать старый оптимизатор на основе правил. Это работает как сон, но в этом нет необходимости, не говоря уже о том, что он использует функцию Oracle, которая больше не поддерживается.

Есть лучший способ сделать это?

8 ответов

Решение

Вы можете просто избежать использования TO_NUMBER вообще? Похоже, это улучшит производительность в любом случае. Что-то вроде:

WHERE t.magic_number != '0'

Если могут быть отрицательные числа или числа с плавающей запятой, вам могут потребоваться дополнительные проверки, но это, безусловно, представляется возможным.

Заставьте CASE сделать работу за вас

select to_number(magic_number) 
from stupid_table
where row_type = 'DATA'
and case when row_type = 'DATA' then to_number(magic_number) else 0 end > 0

В моем тестовом случае у меня были проблемы с воссозданием вашей ошибки, так что удивляйтесь, если есть DATA строки, в которых нет номеров. Но это также может быть способ, которым оптимизатор обрабатывает мои запросы.

Я думаю, что подсказка no_merge также может решить вашу проблему, но, поскольку у меня возникли проблемы с воспроизведением проблемы, я не могу быть уверен.

SELECT --+ no_merge(t)
  TO_NUMBER(t.magic_number)
FROM (
    SELECT magic_number
    FROM mike_temp_stupid_table
    WHERE row_type = 'DATA'
) t
where TO_NUMBER(t.magic_number) > 0;

Я бы решил это, написав собственную функцию преобразования, которая поглощает исключение, т.е.

CREATE OR REPLACE FUNCTION my_to_number( p_str IN VARCHAR2 )
  RETURN number
IS 
BEGIN
  RETURN to_number( p_str );
EXCEPTION
  WHEN OTHERS THEN
    RETURN null;
END;

а затем измените запрос

SELECT TO_NUMBER(magic_number)
FROM stupid_table
WHERE row_type = 'DATA'
AND MY_TO_NUMBER(magic_number) > 0;

За исключением этого, вы, конечно, можете взять план запроса, сгенерированный RBO, и создать профиль, который заставит CBO использовать этот план. Возможно, это немного проще в управлении, чем попытка предоставить полный набор подсказок, которые не позволят CBO когда-либо применять предикат MAGIC_NUMBER перед предикатом ROW_TYPE.

Точный способ заключается в использовании ordered_predicates подсказка, чтобы изменить порядок вашего WHERE условия оцениваются.

Документация: Oracle ORDERED_PREDICATES Подсказка

SELECT /*+ ORDERED_PREDICATES */ TO_NUMBER(magic_number)
FROM stupid_table
WHERE row_type = 'DATA'
AND TO_NUMBER(magic_number) > 0;

Теперь попробуйте поменять условия, и вы снова получите ошибку. Пожалуйста, рассмотрите другие ответы, потому что я также сомневаюсь, что вызов TO_NUMBER - это ваше лучшее решение.

Как насчет создания материализованного представления фрагмента stupid_table, который содержит только тип строки DATA?

Я обычно добавляю rownum, чтобы прекратить толкование предикатов. (Подсказки могут сделать это тоже, но их так легко ошибиться, и с этим типом проблемы, если вы ошибаетесь, вы можете не заметить сразу.) Также вам, вероятно, следует добавить комментарий, чтобы кто-то позже не пытался "оптимизировать" свой код и удалить то, что выглядит как ненужная логика.

SELECT TO_NUMBER(t.magic_number)
FROM (
    --Bad data, use rownum for type safety
    SELECT magic_number, rownum
    FROM stupid_table
    WHERE row_type = 'DATA'
) t
AND TO_NUMBER(t.magic_number) > 0;

Оператор with позволяет применять определенный порядок оценки.

WITH
has_numerics_only AS
(
    SELECT magic_number
    FROM stupid_table
    WHERE row_type = 'DATA'
)
SELECT TO_NUMBER(t.magic_number)
FROM has_numerics_only
WHERE TO_NUMBER(t.magic_number) > 0;

Также рассмотрите возможность того, что в одной или нескольких строках "ДАННЫЕ" действительно содержатся неверные данные.

Вы можете попробовать:

SELECT TO_NUMBER(magic_number)
FROM stupid_table
WHERE row_type = 'DATA'
AND REGEXP_LIKE(magic_number, '^\d{1,}$');

Если это по-прежнему не работает, перемещение условия в предложение HAVING может заставить оптимизатор сначала выполнить его оценку.

SELECT TO_NUMBER(magic_number)
FROM (
SELECT magic_number
FROM stupid_table
WHERE row_type = 'DATA'
GROUP BY magic_number
HAVING REGEXP_LIKE(magic_number, '^\d{1,}$')) ilv;

В противном случае материализованное представление или использование курсора PL/SQL могут быть единственным способом.

Другие вопросы по тегам