Как встроить переменную в PL/SQL?

Ситуация

У меня возникли проблемы с планом выполнения запроса среднего размера для большого объема данных в Oracle 11.2.0.2.0. Чтобы ускорить процесс, я ввел фильтр диапазона, который работает примерно так:

PROCEDURE DO_STUFF(
    org_from VARCHAR2 := NULL,
    org_to   VARCHAR2 := NULL)

  -- [...]
  JOIN organisations org
    ON (cust.org_id = org.id
   AND ((org_from IS NULL) OR (org_from <= org.no))
   AND ((org_to   IS NULL) OR (org_to   >= org.no)))
  -- [...]

Как видите, я хочу ограничить JOIN из organisations, используя необязательный диапазон номеров организаций. Клиентский код может вызывать DO_STUFF с ограничением (предположительно быстрым) или без него (очень медленным).

Проблема

Проблема в том, что PL/SQL создаст переменные связывания для вышеуказанных параметров org_from и org_to, чего я и ожидал в большинстве случаев:

  -- [...]
  JOIN organisations org
    ON (cust.org_id = org.id
   AND ((:B1 IS NULL) OR (:B1 <= org.no))
   AND ((:B2 IS NULL) OR (:B2 >= org.no)))
  -- [...]

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

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

  -- [...]
  JOIN organisations org
    ON (cust.org_id = org.id
   AND ((10 IS NULL) OR (10 <= org.no))
   AND ((20 IS NULL) OR (20 >= org.no)))
  -- [...]

Под «намного» я ​​имею в виду в 5-10 раз быстрее. Обратите внимание, что запрос выполняется очень редко, т.е. раз в месяц. Поэтому мне не нужно кэшировать план выполнения.

Мои вопросы

  • Как я могу встроить значения в PL/SQL? Я знаю о EXECUTE IMMEDIATE, но предпочел бы PL/SQL компилирует мой запрос, а не выполняет конкатенацию строк.

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

  • Я что-то пропустил? Может быть, есть совершенно другой способ добиться улучшения плана выполнения запроса, кроме встраивания переменных (обратите внимание, я также пробовал немало подсказок, но я не эксперт в этой области)?


person Lukas Eder    schedule 18.03.2011    source источник
comment
Я почти уверен, что это совпадение. Все запросы скомпилированы, и я полностью уверен, что следую тому, что вы спрашиваете о встраивании переменных. Переменные связывания — это способ Oracle сообщить мне значение, которое находится в общем пуле.   -  person Sathyajith Bhat    schedule 18.03.2011
comment
@ Сатья, я обновлю свой вопрос. Под встраиванием я подразумеваю встраивание, т.е. переменные больше не являются переменными, а являются константами оператора SQL.   -  person Lukas Eder    schedule 18.03.2011
comment
Получаете ли вы на самом деле разные планы запросов последовательно с переменными связывания и жестко закодированными значениями? Или ваша разница в производительности потенциально связана с тем, что медленный запрос считывает кучу данных в кеш (базу данных, ОС или SAN), так что быстрый запрос выигрывает от более быстрого чтения? Какая версия Оракла? Можно ли предположить, что столбец NO в ORGANIZATIONS является уникальным ключом? Точна ли статистика в столбце NO?   -  person Justin Cave    schedule 18.03.2011
comment
@ Джастин, да, планы совпадают. Я измерил 1, 2, 1, 2, 1, 2 и т. д. Также я проверил различные значения привязки. С переменными связывания я получаю ПОЛНОЕ СКАНИРОВАНИЕ ТАБЛИЦЫ, тогда как с жестко закодированными значениями план выглядит намного лучше. Версия 11.2.0.2.0. NO уникален, статистика точная.   -  person Lukas Eder    schedule 18.03.2011
comment
@Justin, на всякий случай проблема не в таблице organisations. У этого всего около 550 записей. Каждая организация имеет более 100 тыс. записей в различных других таблицах, которые связаны с полным запросом.   -  person Lukas Eder    schedule 18.03.2011
comment
Может быть, опубликовать планы выполнения?   -  person FrustratedWithFormsDesigner    schedule 18.03.2011
comment
@Frustrated... Тогда не используйте FormsDesigner :-) Потребуется некоторое время, чтобы опубликовать полный запрос (29 строк) и планы выполнения (45-47 шагов выполнения), так как мне нужно каким-то образом анонимизировать их перед публикацией. их здесь. Я сделаю это через некоторое время, если это действительно необходимо. Однако важный вопрос здесь (для меня) заключается в том, есть ли способ обойти EXECUTE IMMEDIATE. Я изменил порядок вопросов...   -  person Lukas Eder    schedule 18.03.2011
comment
@Lukas Eder: Хорошо, я не понимал, что это настолько сложно! Я полагаю, вы уже создали правильно спроектированные индексы и обновили их?   -  person FrustratedWithFormsDesigner    schedule 18.03.2011
comment
@Frustrated: Да, индексы правильные. Вот почему встраивание/жесткое кодирование значений происходит так быстро, потому что тогда они фактически используются. Я также пытался использовать всевозможные подсказки, чтобы заставить Oracle использовать эти индексы (я должен указать это в своем вопросе).   -  person Lukas Eder    schedule 18.03.2011
comment
@Lukas Eder: Хммм, это сложный вопрос ... Иногда я обнаруживал, что самый правильный индекс не является очевидным. Я иногда исправлял подобные проблемы, создавая новые индексы, объединяющие столбцы, которые явно не связаны, но, поскольку все они участвуют в запросе, это значительно ускоряет рассматриваемый запрос. Но это сильно зависит от запросов и базовой структуры таблицы. Может быть, часть условий можно переписать так, чтобы они ссылались на другие проиндексированные столбцы? Как насчет перезаписи, чтобы использовать BETWEEN для фильтра диапазона? Может ли это помочь?   -  person FrustratedWithFormsDesigner    schedule 18.03.2011
comment
@Frustrated, ключевое слово BETWEEN приводит к почти такому же плану выполнения. Вероятно, какой-то очень неортодоксальный индекс мог решить проблему за пределами моего понимания. Но это не объясняет, почему встроенные значения работают намного лучше, чем связанные переменные. Я думаю, мне придется опубликовать запрос... :-)   -  person Lukas Eder    schedule 18.03.2011
comment
@Lukas: Просто снимок, но в вашей процедуре вы передаете varchar2, но в вашем примере встраивания используются числа. Возможно, Oracle выполняет преобразования to_number за кулисами при использовании varchars, переданных в proc? (т.е. вместо этого установите параметры как число).   -  person tbone    schedule 18.03.2011
comment
@Tbone, хм, хорошее наблюдение. org.no — это столбец VARCHAR2, но он содержит только 5-значные числа. Итак, я думаю, в моем встроенном примере Oracle выполняет автоматическое приведение типов или to_char или что-то в этом роде. Я проверю, имеет ли это какое-то значение для планов выполнения (хотя я был бы удивлен)   -  person Lukas Eder    schedule 19.03.2011


Ответы (5)


В одном из своих комментариев вы сказали:

«Также я проверил различные значения привязки. С переменными привязки я получаю несколько ПОЛНЫХ СКАНИРОВАНИЙ ТАБЛИЦЫ, тогда как с жестко закодированными значениями план выглядит намного лучше».

Есть два пути. Если вы передаете NULL для параметров, вы выбираете все записи. В этих обстоятельствах полное сканирование таблицы является наиболее эффективным способом извлечения данных. Если вы передаете значения, индексированные чтения могут быть более эффективными, потому что вы выбираете только небольшое подмножество информации.

Когда вы формулируете запрос, используя переменные связывания, оптимизатор должен принять решение: должен ли он предполагать, что большую часть времени вы будете передавать значения или что вы будете передавать нули? Трудный. Итак, посмотрите на это с другой стороны: более неэффективно выполнять полное сканирование таблицы, когда вам нужно выбрать только подмножество записей, или выполнять индексированное чтение, когда вам нужно выбрать все записи?

Похоже, что оптимизатор выбрал полное сканирование таблицы как наименее неэффективную операцию для покрытия всех возможных ситуаций.

В то время как при жестком кодировании значений Оптимизатор сразу узнает, что 10 IS NULL оценивается как FALSE, и поэтому он может взвесить достоинства использования индексированных чтений для поиска желаемых записей подмножества.


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


«Кстати, удаление предложения :R1 IS NULL не сильно меняет план выполнения, что оставляет меня с другой стороной условия ИЛИ, :R1 ‹= org.no, где NULL в любом случае не имеет смысла, поскольку org. нет НЕ НУЛЕВОЕ"

Итак, дело в том, что у вас есть пара переменных связывания, которые определяют диапазон. В зависимости от распределения значений разные диапазоны могут соответствовать разным планам выполнения. То есть этот диапазон (вероятно) подойдет для сканирования индексированного диапазона...

WHERE org.id BETWEEN 10 AND 11

... тогда как это, вероятно, больше подходит для полного сканирования таблицы ...

WHERE org.id BETWEEN 10 AND 1199999

Именно здесь в игру вступает Bind Variable Peeking.

(в зависимости от распределения значений, конечно).

person APC    schedule 18.03.2011
comment
Спасибо за ответ. Это тоже мое интуитивное мышление. Я просто хотел убедиться. Кстати, удаление предложения :R1 IS NULL не сильно меняет план выполнения, что оставляет меня с другой стороной условия OR, :R1 <= org.no, где NULL в любом случае не имеет смысла, поскольку org.no равно NOT NULL - person Lukas Eder; 18.03.2011
comment
Спасибо за обновления. Чем больше я читаю ваши предложения, ребята, тем больше прихожу к выводу, что независимо от того, какие значения я привязываю к запросу, повторно используется один и тот же план, точно так же, как в статье, опубликованной EvilTeach. В этой статье также упоминаются предикаты диапазона. На самом деле удаление предиката диапазона и замена его простым org.no = :B1 также кажется приемлемым планом. Однако, поскольку я использую 11 г, а не 10 г, я еще раз проверю и проверю также предложения ik_zelf. После обновления 11g в Oracle было добавлено много функций. - person Lukas Eder; 18.03.2011

Поскольку планы запросов на самом деле постоянно различаются, это означает, что оценки кардинальности оптимизатора по какой-то причине отключены. Можете ли вы подтвердить из планов запроса, что оптимизатор ожидает, что условия будут недостаточно избирательными, когда используются переменные связывания? Поскольку вы используете 11.2, Oracle должен использовать адаптивное совместное использование курсора, поэтому это не должно быть проблемой просмотра переменных связывания (при условии, что вы много раз вызываете версию с переменными связывания с разными значениями NO в тестировании.

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

Вы всегда можете использовать подсказку в запросе, чтобы принудительно использовать определенный индекс (хотя и с помощью хранимая схема или стабильность плана оптимизатора предпочтительнее с точки зрения долгосрочного обслуживания). Любой из этих вариантов предпочтительнее использования динамического SQL.

Однако можно попробовать еще один тест: заменить синтаксис соединения SQL 99 старым синтаксисом Oracle, т.е.

SELECT <<something>>
  FROM <<some other table>> cust,
       organization org
 WHERE cust.org_id = org.id
   AND (    ((org_from IS NULL) OR (org_from <= org.no)) 
        AND ((org_to   IS NULL) OR (org_to   >= org.no)))

Это, очевидно, ничего не должно изменить, но были проблемы с парсером с синтаксисом SQL 99, так что это нужно проверить.

person Justin Cave    schedule 18.03.2011
comment
спасибо за ваш развернутый отзыв! Я попробую то, что вы предлагаете, и перепроверю статистику и гистограммы. В этом случае долгосрочное обслуживание не является большой проблемой, поскольку в нашей базе данных редко что-то меняется. Но опять же, я не эксперт по Oracle, поэтому, возможно, вы указываете на что-то, о чем я совершенно не знаю. - person Lukas Eder; 18.03.2011
comment
Хороший отзыв о неверных оценках количества элементов. Я всегда получаю одни и те же мощности для каждого встроенного значения, поэтому, возможно, я столкнусь с той же проблемой, что и с переменными связывания, с той крошечной разницей, что мне повезло, что я получил хороший план с первой попытки. Я продолжу расследование... - person Lukas Eder; 18.03.2011
comment
Я просто попытался избежать синтаксиса SQL 99 JOIN, заменив его перекрестным соединением и предикатами. Это полностью меняет план выполнения, но результаты остаются прежними. С переменными связывания у меня плохой план, со встроенными значениями у меня хороший план - person Lukas Eder; 18.03.2011

Это пахнет Bind Peeking, но я работаю только на Oracle 10, поэтому не могу утверждать, что такая же проблема существует и в 11.

person EvilTeach    schedule 18.03.2011
comment
спасибо за вклад. Я немедленно проверю эту статью! - person Lukas Eder; 18.03.2011
comment
проверьте ответ Джастина, он также ссылается на просмотр переменных - person Lukas Eder; 18.03.2011
comment
Ваша статья действительно описывает симптомы, которые я испытываю. Кардиналы из планов различных исполнений, похоже, не меняются, даже если я изменю значения привязки. - person Lukas Eder; 18.03.2011
comment
@lukas - кажется, что запах переключается с переменных связывания на жестко запрограммированные, чтобы решить проблему с производительностью. У меня есть многосессионное приложение, когда некоторые из них работали быстро, а некоторые — медленно, из-за того, что план был создан для небольшого N (полное сканирование таблицы), который затем был применен к большому N. Причиной оказался просмотр привязки. - person EvilTeach; 18.03.2011

Это очень похоже на потребность в Adaptive Cursor Sharing в сочетании со стабильностью SQLPlan. Я думаю, что происходит то, что capture_sql_plan_baselines parameter is true. И то же самое для use_sql_plan_baselines. Если это правда, то происходит следующее:

  1. При первом запуске запроса он анализируется и получает новый план.
  2. Во второй раз этот план сохраняется в sql_plan_baselines как принятый план.
  3. Все последующие запуски этого запроса используют этот план, независимо от переменных связывания.

Если Adaptive Cursor Sharing уже активен, оптимизатор создаст новый/лучший план, сохранит его в sql_plan_baselines, но не сможет его использовать, пока кто-то не примет этот более новый план в качестве приемлемого альтернативного плана. Проверьте dba_sql_plan_baselines и посмотрите, есть ли в вашем запросе записи с accepted = 'NO' and verified = null. Вы можете использовать dbms_spm.evolve для разработки нового плана и его автоматического принятия, если производительность плана как минимум в 1,5 раза выше, чем без нового плана.

Надеюсь, это поможет.

person ik_zelf    schedule 18.03.2011
comment
С каждым днем ​​мне все больше и больше нравятся твои знания! Может быть, мы должны принять вас в нашу компанию! :-) Я проверю ваши предложения как можно скорее. Спасибо за ответ! - person Lukas Eder; 18.03.2011
comment
@ Лукас Эдер, я всегда открыт для предложений. Спасибо д - person ik_zelf; 21.03.2011

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

Кажется, в таблице вашей организации есть столбец № (org.no), который определяется как число. В вашем жестко запрограммированном примере вы используете числа для сравнения.

JOIN organisations org
    ON (cust.org_id = org.id
   AND ((10 IS NULL) OR (10 <= org.no))
   AND ((20 IS NULL) OR (20 >= org.no)))

В вашей процедуре вы передаете varchar2:

PROCEDURE DO_STUFF(
    org_from VARCHAR2 := NULL,
    org_to   VARCHAR2 := NULL)

Таким образом, чтобы сравнить varchar2 с числом, Oracle должен будет выполнить преобразования, так что это может привести к полному сканированию.

Решение: измените процедуру, чтобы она передавалась в числах

person tbone    schedule 18.03.2011
comment
хорошее наблюдение, я не подумал об этом. На самом деле всё наоборот. Во встроенном примере должны использоваться строки, а не числа, поскольку org.no имеет тип VARCHAR2. - person Lukas Eder; 19.03.2011
comment
@лукас, спасибо. Казалось упрощенным, учитывая другие комментарии/решения. Не может изменить план объяснения, который использует переменные связывания, но фактическое время выполнения должно быть затронуто при сравнении чисел с varchars (или наоборот). Во всяком случае, надеюсь, что это поможет как-то. - person tbone; 21.03.2011