Hard parse oracle что это

Обновлено: 07.07.2024

Про этот параметр писали уже много где, например, просто и толково на русском языке написано тут и тут. Причём много где утверждают, что установка данного параметра в значение FORCE или SIMILAR - априори хорошо и безвредно. В этой заметке я постараюсь разъяснить, почему это не совсем так.
Собственно, как происходит обработка запроса в режиме EXACT (приблизительно):

  1. Считается хэш текста запроса
  2. Ищется запрос с таким же хэшем в shared pool
  3. Нашли? Отлично, используем уже готовый план, если нет, то создаём новый план, т.е. выполняется hard parse

Всё быстро, просто и понятно.
А вот как обрабатывается запрос в режиме FORCE и SIMILAR :

  1. Заменяеются литералы в запросе на сгенерированные системой имена переменных связывания
  2. В случае SIMILAR производится также оценка надёжности связанной переменной
  3. Ищется похожий запрос с в shared pool
  4. Если нашли, используем уже готовый план, если нет, то создаём новый план, т.е. выполняется hard parse

Если наши приложения создают шквал одинаковых запросов с разными литералами, то установка cursor_sharing=FORCE действительно может помочь прежде всего за счёт уменьшения числа hard parses и экономии места в shared pool .
Так почему же значение по умолчанию для этого параметра всё-таки EXACT ? Дело в том, что лучше всего не полагаться на механизмы анализа запросов и сразу писать их так, чтобы они были разделяемыми. Если у нас много одинаковых, но сложных запросов, отличающихся только литералами, то на их анализ и подстановку bind variables может тратиться много процессорного времени. Да, установка FORCE поможет в таком случае избежать hard parses , но soft parses будут занимать больше времени сами по себе за счёт чрезмерно сложного анализа запросов.
Вывод: установка параметра cursor_sharing в значения, отличные от EXACT - акт отчаяния (возможно, приносящий отличный результат), когда не удаётся переписать приложения, чтобы они генерировали легко разделяемые запросы.
Если система разрабатывается "с нуля", то лучше сразу разрабатывать её для настройки EXACT .

Весьма неплохо указать ссылочку на металинк - High SQL Version Counts - Script to determine reason(s) (Doc ID 438755.1) и на раздел AWR-отчета "SQL ordered by Version Count".
Очень меткое выражение - "значения, отличные от EXACT - акт отчаяния ", был опыт борьбы с девелоперами, которые не хотели использовать bind:)

1. При первом разборе происходит полный разбор запроса (hard parse)
План запроса помещается в глобальный кэш БД с определенным sql_id
2. При повторном выполнении происходит частичный разбор (soft parse)
Происходит только синтаксический разбор, проверки прав доступа и проверки bind переменных. Что для разных вариаций sql_id создает дочерние child_sql_id

Из-за такого механизма работы Oracle вытекает частая проблема oltp систем, где существует огромное число маленьких запросов, отличающихся друг от друга только фильтрами или параметрами. Это приводит к быстрому вытеснению планов из кэша и их последующему повторному hard parse.
В итоге может оказаться, что большую часть времени БД занимается разбором запросов, а не собственно их выполнением.
Отсюда вывод: по возможности используйте bind переменные в вариациях одного запроса, замен константных фильтров, т.к. это даст нам только один план запроса (child_sql_id) при разных значениях переменных на равномерно распределенном столбце.

Я не зря сказал ранее "на равномерно распределенном столбце", т.к. с bind переменными есть проблема: по умолчанию Oracle не знает какие данные будут переданы в запрос и из-за этого может сгенерить неверный план запроса.

Посмотрим на примере по умолчанию. Создадим таблицу с неравномерно распределенным столбцом "n" (9 строк со значением = 1, и 1млн-9 строк со значением 2):
Столбец не имеет гистограмм, но есть статистика по уникальным значениям. Построим план запроса с bind переменной = 1:
Oracle закономерно ожидает в результате половину таблицу и выбирает full scan, хотя мы то знаем, что тут был бы лучше Index scan.

К счастью с 11 версии Oracle может заглядывать в значения bind переменных и подбирать под них нужные планы.
Для этого соберем гистограмму с 2 вершинами и повторим эксперимент:
Oracle сгенерировал новый child_sql_id под новое значение bind переменной и выбрал правильный доступ по индексу.

Данный план был закеширован в глобальную память и если прямо сейчас выполнить заново с параметром 2, то мы получим тотже план (child number 2).

Замечу что на этом этапе уже надо смотреть план уже выполненного запроса, т.к. oracle не умеет показывать план и заглядывать в bind переменные, но при реальном выполнении запроса значения bind переменных смотрятся.

но oracle пометит этот запрос на пересмотр, т.к. план совсем не сошелся с реальными данными и при последующем применении сгенерирует новый child_sql_id (child number 3) под нашу bind переменную:

Из всего этого можно сделать вывод, что вопреки частому заблуждению, Oracle умеет генерировать правильные планы по bind переменным, но делает это не сразу, а при повторном вызове и при наличии гистограммы.
Второе: реальный план запроса с bind переменными можно узнать только во время или после выполнения запроса, т.к. "explain plan" не подсматривает в bind переменные.

Cardinality feedback

Ora Blog
Oracle мониторит и исправляет следующии "estimated rows" оценки на основе реальных "actual rows"
* Single table cardinality (after filter predicates are applied)
* Index cardinality (after index filters are applied)
* Cardinality produced by a group by or distinct operator

Dynamic Sampling

Ora Blog
Применимо для запросов со сложными предикатами фильтрации, которые дают существенную ошибку оптимизатора.
Включение dynamic sampling в зависимости от параметра пробирует от 32 блоков таблицы на предикаты фильтрации и определяем реальный лучший план.

Автор статьи – Виктор Варламов(varlamovVp18), OCP.
Оригинал статьи опубликован 07.07.2017.
Отдельное спасибо автору перевода — brutaltag.

В нашей системе подготовки отчетности обычно выполняются сотни длительных запросов, которые вызываются различными событиями. Параметрами запросов служат список клиентов и временной интервал (дневной, недельный, месячный). Из-за неравномерных данных в таблицах один запрос может выдать как одну строку, так и миллион строк, в зависимости от параметров отчета (у разных клиентов — различное количество строк в таблицах фактов). Каждый отчет выполнен в виде пакета с основной функцией, которая принимает входные параметры, проводит дополнительные преобразования, затем открывает статический курсор со связанными переменными и в конце возвращает этот открытый курсор. Параметр БД CURSOR_SHARING выставлен в FORCE.
В такой ситуации приходится сталкиваться с плохой производительностью, как в случае повторного использования плана запроса оптимизатором, так и при полном разборе запроса с параметрами в виде литералов. Связанные переменные могут вызвать неоптимальный план запроса.

В своей книге “Oracle Expert Practices” Алекс Горбачев приводит интересную историю, рассказанную ему Томом Кайтом. Каждый дождливый понедельник пользователям приходилось сталкиваться с измененным планом запроса. В это трудно поверить, но так и было:

«Согласно наблюдениям конечных пользователей, в случаях, когда в понедельник шел сильный дождь, производительность базы данных была ужасной. В любой другой день недели или же в понедельник без дождя проблем не было. Из разговора с администратором БД Том Кайт узнал, что трудности продолжались до принудительного рестарта базы данных, после чего производительность становилась нормальной. Вот такой был обходной маневр: дождливый понедельник – рестарт».

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

Другими словами, у нас есть таблица VVP_HARD_PARSE_TEST с миллионом строк, где в 10.000 случаев поле C2 = 99, 8 записей с C2 = 1, а остальные с C2 = 1000000. Гистограмма по полю С2 указывает оптимизатору Oracle об этом распределении данных. Такая ситуация известна как неравномерное распределение данных, и гистограмма может помочь выбрать правильный план запроса в зависимости от запрашиваемых данных.

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

SELECT * FROM VVP_HARD_PARSE_TEST WHERE c2 = :p

если p = 1, то наилучшим выбором будет INDEX RANGE SCAN, для случая p = 1000000 лучше использовать FULL TABLE SCAN. Запросы Query1 и Query1000000 идентичны, за исключением текста в комментариях, это сделано чтобы получить различные идентификаторы планов запроса.


Теперь посмотрим на планы запросов:


Как можно видеть, план для разных запросов создается только один раз, в момент первого выполнения (только один дочерний курсор с CHILD_NUMBER = 0 существует для каждого запроса). Каждый запрос выполняется дважды (EXECUTION = 2). Во время жесткого разбора Oracle получает значения связанных переменных и выбирает план соответственно этим значениям. Но он использует тот же самый план и для следующего запуска, несмотря на то что связанные переменные изменились во втором запуске. Используются неоптимальные планы – Query1000000 с переменной C2 = 1 использует FULL TABLE SCAN вместо INDEX RANGE SCAN, и наоборот.

Понятно, что исправление приложения и использование параметров как литералов в запросе – это самый подходящий способ решения проблемы, но он ведет к динамическому SQL с его известными недостатками. Другой путь – отключение запроса связанных переменных ( ALTER SESSION SET "_OPTIM_PEEK_USER_BINDS" = FALSE ) или удаление гистограмм (ссылка).

Одно из возможных решений — это альтернативное использование политик на доступ к данным, также известных как Virtual Private Database (детальный контроль доступа, Fine Grained Access Control, контроль на уровне строк). Это позволяет менять запросы на лету и поэтому может вызвать полный разбор плана запроса каждый раз, когда запрос использует детальный контроль доступа. Эта техника подробно описана в статье Рэндальфа Гейста. Недостатком этого метода является возрастающее число полных разборов и невозможность манипулировать планами запросов.

Посмотрите, что мы сейчас сделаем. После анализа наших данных мы решаем разбить клиентов на три категории – Большие, Средние и Маленькие (L-M-S или 9-5-1) – согласно количествам сделок или транзакций в течение года. Также количество строк в отчете строго зависит от периода: Месячный – Large, Недельный – Middle, Дневной – Small или 9-5-1. Далее решение простое – сделаем предикат политики безопасности зависящим от каждой категории и от каждого периода. Так, для каждого запроса мы получим 9 возможных дочерних курсоров. Более того, запросы с разными политиками приведут нас к одним и тем же идентификаторам запросов, это дает возможность реализовать SQL PLAN MANAGEMENT (sql plan baseline).


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

Посмотрим, как эта техника может преобразить предыдущий пример:


Посмотрим на планы выполнения:


Выглядит здорово! Каждый запрос выполняется дважды, с различными дочерними курсорами и разными планами. Для параметра C2 = 1000000 мы видим FULL TABLE SCAN в обоих запросах, а для параметра C1 = 1 мы видим всегда INDEX RANGE SCAN.

Парсинг и кэш курсора PL/SQL в базе данных Oracle

Существует два ключевых понятия, которые следует отделять друг от друга, говоря о парсинге. Первое – парсинг , как комплекс операций, и второе – подпрограмма, которую называют парсер ом. Это может показаться странным, но:

  • вызов парсера выполняется не для всякого кода;
  • вызов парсера не всегда сопровождается оптимизацией его результатов;
  • вызов парсера не всегда выполняется в ходе парсинга;
  • парсинг и оптимизация могут иметь место в ходе выполнения, а не только в вызове парсера.

Если вернуться обратно к примеру с циклом и исследовать статистики сеанса для обоих вариантов инструкции SQL (см. core_dc_activitiy_03 . sql в загружаемом пакете примеров), можно увидеть следующие результаты, касающиеся вызовов парсера:

Статистика parse count (total) отражает число вызовов парсера, выполненных ядром Oracle. Результаты выполнения второго теста показывают, что ваш код не всегда приводит к вызову парсера – исходный код второго примера лишь чуть-чуть отличается от исходного кода первого примера, но обрабатываются они совершенно по-разному. Это результат ввода в действие кэша курсора PL/SQL – интерпретатор PL/SQL замечает, что внутри цикла выполняется одна и та же инструкция, задействует внутренний механизм для создания локальной курсорной переменной и удерживает этот курсор открытым. Это означает, что не требуется вызывать парсер в каждой итерации цикла, а достаточно вызвать процедуру выполнения после самой первой итерации. Эта оптимизация не применялась к механизму execute immediate до версии 10g – если провести тестирование в Oracle 9i, можно увидеть, что в версии с переменной связывания парсер вызывается в каждой итерации цикла:

Статистика parse count (hard) – это счетчик числа оптимизаций инструкции. В первом примере, где в цикле выполнялись разные инструкции, Oracle был вынужден оптимизировать каждую из 1000 выполненных инструкций, потому что все они отличаются друг от друга. Как вы можете увидеть в этом блоге, «полный» («hard») парсинг появляется также в выводе tkprof как Miss in library cache during parse . (Разница в 3 вызова между статистиками total и hard обусловлена искажениями, вносимыми самим механизмом тестирования.)

Но в третьем примере Oracle фактически не производит оптимизацию инструкции при каждом ее выполнении – напомню, что не каждый вызов парсера ( parse count(total) ) сопровождается оптимизацией ( parse count(hard) ). Это объясняется вступлением в игру библиотечного кэша. Когда цикл выполняется в первый раз, Oracle анализирует и оптимизирует инструкцию, и загружает ее в библиотечный кэш. Когда цикл выполняется во второй раз, Oracle просматривает библиотечный кэш, прежде чем выполнить полный парсинг и оптимизацию инструкции. Если в кэше обнаруживается готовая инструкция, она используется повторно и сеансу не приходится снова платить за оптимизацию – это объясняет меньшее значение статистики parse count(hard ).

Примечание. Последовательность действий при парсинге немного сложнее, чем мои первоначальные предположения. Когда приложение передает в Oracle фрагмент текста, сначала выполняется проверка синтаксиса, чтобы определить допустимость инструкции, затем производится поиск совпадений в библиотечном кэше (для этого используется хэш-значение, вычисленное из текста инструкции). Если обнаруживается текстовое совпадение, Oracle выполняет семантическую проверку – означает ли новый текст то же самое, что имеющийся в библиотечном кэше (те же объекты, те же привилегии и т. д.), Эта проверка известна как проверка курсора (cursor authentication). Если совпадение подтверждается, отпадает необходимость повторной оптимизации инструкции.

Кэш курсора в базе данных Oracle

Однако, мы еще не закончили с исследованием особенностей вызова парсера. В продолжение обсуждаемой темы ниже приводится еще одна версия реализации цикла (см. core_dc_activity_04 . sql в загружаемом пакете примеров), явно вызывающая parse и execute из пакета dbms_sql :

В этом испытании я извлекаю статистики сеанса и число приобретений защелок из v$latch . Данный цикл был выполнен дважды и между прогонами я внес существенные изменения в окружение сеанса. Перед вторым прогоном я уменьшил значение параметра session_cache_cursors до нуля командой: alter session set session_cache_cursors=0 ; (значения по умолчанию: 20 – в 10g и 50 – в 11g). В таблицах 7.1 (статистики сеанса) и 7.2 (число приобретений защелок) показаны ключевые последствия этих изменений.

Примечание. Эти значения получены в 10.2. Результаты в 11g значительно отличаются, потому что в этой версии Oracle перешел на использование мьютексов в некоторых операциях.

Статистика session_cached_cursors со значением по умолчанию (50) session_cached_cursors = 0
parse count (total) 1064 1077
parse count (hard) 6 6
session cursor cache hits 1050 0

Защелка session_cached_cursors со значением по умолчанию (50) session_cached_cursors = 0
library cache 6393 10549
library cache lock 6174 10392

Парсер и кэш курсора инструкций PL/SQL в СУБД Oracle

На первый взгляд статистики сеанса говорят, что число вызовов парсера одинаково в обоих случаях. И даже при том, что в код включена 1000 явных вызовов parse ( dbms_sql.parse ), полный парсинг выполнялся считанное число раз. Это означает, что подобное решение обеспечивает лишь незначительную оптимизацию и приходится повторно использовать инструкцию в цикле. Однако, значительно изменился способ доступа к курсору с этой инструкцией, а также изменился объем работы и степень конкуренции.

По умолчанию сеанс обладает кэшем курсора определенного размера. Это означает, что если вызывать инструкцию достаточно часто, Oracle (10g) присоединит блокировку KGL к курсору этой инструкции, чтобы удержать его открытым, и создаст в памяти сеанса объект состояния (state object), связывающий курсор так, что образуется короткий путь в курсор, позволяющий исключить операцию поиска в библиотечном кэше. Вам часто придется видеть утверждения, что курсор кэшируется на третьем вызове инструкции – это не совсем верно. Технически кэширование происходит в вызове, следующем за вызовом после проверки курсора (cursor authentication).

Если выполнить инструкцию, которая прежде не выполнялась, а затем выполнить ее еще несколько раз, можно заметить, что статистика cursor authentications увеличилась на втором вызове, а статистики session cursor cache hits и session cursor cache count увеличились на четвертом вызове, при этом имеет место следующая последовательность событий:

  1. Оптимизация на первом вызове.
  2. Проверка курсора на втором вызове.
  3. Кэширование выполняется после третьего вызова.
  4. Далее начинает использоваться кэш.

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

Одной из необычных характеристик кэша курсора сеанса является его размер – он действительно очень невелик. Во множестве мест можно было бы получить выгоду от увеличения параметра session_cache_cursor . И, вероятно, стоит сделать это, если обнаруживается, что значение статистики session cursor cache hits намного меньше разности значений статистик parse count (total) и parse count (hard) .

Между кэшем курсора сеанса и кэшем PL/SQL много общего – на самом деле, начиная с 9.2.0.5, размеры этих двух кэшей определяются параметром session_cached_cursors (до этого размер кэша курсора PL/SQL определялся параметром open_cursors ). Вы можете даже спросить, вернувшись к результатам второго теста ( core_dc_activity_02 . sql ): как я узнал, что инструкция SQL попала в кэш курсора PL/SQL или в кэш курсора сеанса? Дело в том, что значение session cursor cache hits для этого теста было равно нулю, соответственно курсор должен был храниться в кэше PL/SQL.

Примечание. Кэш курсора PL/SQL имеет важное отличие от кэша курсора сеанса, которое проявляется только в версии Oracle 11g, где появился механизм динамически разделяемых курсоров (adaptive cursor sharing). Если выполнить запрос, уже хранящийся в кэше курсора сеанса, он попадет во власть механизма динамически разделяемых курсоров и может оптимизироваться повторно. Если запрос хранится в кэше курсора PL/SQL, механизм динамически разделяемых курсоров вообще не будет задействован.

Удержание курсоров

Из моего тестового примера можно выжать еще одно наблюдение, касающееся эффекта удержания курсоров. Предкомпиляторы (precompilers) Oracle позволяют генерировать код, удерживающий курсоры без необходимости предусматривать выполнение каких-то специальных операций, простым использованием директивы предкомпилятора. Но иногда для удержания курсоров приходится явно включать специальный код. Если взглянуть на код в сценарии core_dc_activity_04 . sql , можно заметить, что внутри цикла он вызывает процедуры открытия и закрытия курсора. В действительности в этом нет необходимости. Если заранее известно, что инструкция часто будет использоваться повторно, можно объявить переменную курсора с более широкой областью видимости и удерживать курсор открытым, сколько потребуется. После этого остается только выполнять курсор, когда это необходимо. Для демонстрации, ниже приводится измененная версия (см. core_dc_activity_05 . sql в загружаемом пакете примеров):

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

Однако эту стратегию необходимо применять с осторожностью. Мне приходилось сталкиваться с двумя ключевыми ошибками, когда люди делали это неправильно, и обе, так уж случилось, были допущены в Java-окружении. Первая ошибка – использование функций открытия и закрытия курсора в программном коде на Java, подобно тому, как я это сделал в примере кода PL/SQL. Проблема в том, что используя описанную стратегию, класс открывает и закрывает курсор, но выполняет инструкцию только один раз – вы не получаете никаких преимуществ, если инструкция не выполняется достаточно часто перед закрытием курсора. (Я полагаю, что в последних версиях драйверов JDBC эта проблема может быть решена удержанием курсора открытым на уровне библиотечного кода, на котором основывается прикладной код программ, потому что примерно так используется кэш курсора сеанса в Oracle.)

Вторая ошибка состоит в том, что многие часто забывают закрыть курсор – часто в обработчиках исключений – и уничтожают класс. Программы (и программисты) на Java должны закрывать курсор; если курсор останется открытым в сеансе, курсор в библиотечном кэше не будет закрыт до конца сеанса. Когда такое происходит, программы обычно завершаются аварийно с ошибкой ORA-01000 : maximum open cursors exceeded (превышено максимальное число курсоров).

Выше я отмечал (в списке «сюрпризов»), что парсинг и оптимизация могут производиться непосредственно в вызове execute . Это побочный эффект удержания курсора – явно, в программном коде, или неявно, в результате скрытых оптимизаций PL/SQL. Удерживая курсор, пользовательскому коду приходится работать всего лишь с простой числовой переменной, но внутри она идентифицирует различные структуры, которые ведут к дочернему курсору и его родителю в библиотечном кэше. Если для нужд других сеансов потребуется освободить память, Oracle может удалить из памяти почти все, что относится к дочернему курсору – даже когда курсор удерживается открытым – оставляя лишь минимум информации, с помощью которой сеанс сможет воссоздать план выполнения. Когда такое случается, при следующей попытке выполнить инструкцию фиксируется промах библиотечного кэша в ходе выполнения ( Miss in library cache during execute ) и увеличивается статистика parse count (hard) , но при этом значение статистики parse count (total) остается прежним. По этой причине иногда можно увидеть значение parse count (hard) , превышающее значение parse count (total) .

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

В очередной раз столкнувшись с ранее описанной проблемой типа Медленный разбор SQL запроса (long parse time): вариант №2, решил протестировать, в какой степени могут быть полезны для уменьшения времени разбора (hard parse) штатные методы фиксации / модификации планов выполнения, предлагаемые Oracle:

Первое выполнение оригинального запроса происходит медленно:

Статистика первого выполнения однозначно указывает на длительный hard parse:

Причиной длительного разбора по-прежнему является большое количество используемых таблиц + громоздкая / неоптимальная конструкция запроса с использованием множественных вложенных параметризованных обзоров:

Судя по 10053 трейсу (размером > 240 MB) при подготовке плана оптимизатор достаточно долго тестирует более 1400 комбинаций перестановок таблиц:

Описанный ранее метод прямого отключения трансформаций и ограничения количества пермутаций через подсказки:

, статистика выпонения также выглядит значительно лучше:

SPM Baseline

Создание фиксированного baseline лишь незначительно сокращает время hard parse:

Несмотря на то, что запрос был обнаружен в SPM в первых строках трейса:

В описываемом случае ни окружение сессии/ системы, ни объекты бд не менялись и новый baseline создан не был:

Outline

Если же создать outline для того же запроса:

Интересно, что при компиляции курсоров используется либо baseline,либо outline:

10053 трейс показывает, что в присутствии outline использование SPM автоматически блокируется:

Использование outline значительно сокращает размер трейса / время hard parse и количество пермутаций:

Судя по трейсу сокращение числа пермутаций достигается за счёт точного следования подсказкам, составляющим outline, на протяжении всей компиляции плана, исключая любые противоречащие сохранённому плану трансформации:

Подсказки, составляющие планы, зафиксированные в outline и в baseline, естественно полностью совпадают:

SQL Patch

Трейс оптимизатора подтверждает, что как и в случае с baseline присутствие запроса в SPM обнаруживается в начале компиляции, но сдержащиеся в sql patch подсказки используется с самого начала компиляции плана:

Интересно отметить, что стоимость этих автоматически добавленных планов выше ручного оригинала, что формально противоречит принципам Cost-Based оптимизации :)

Параметр, формально отвечающий за автоматическое формирование истории планов выполнения, установлен в значение по умолчанию, т.е. отключен:

Читайте также: