Вызов парсера в базе данных Oracle - что это?

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

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

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

Name (literal string test) Value
----                       -----
parse count (total)        1,004
parse count (hard)         1,001
Name (bind variable test)  Value
----                       -----
parse count (total)            5
parse count (hard)             2

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

Name (bind variable test 9i) Value
---------------------------- -----
parse count (total)          1,004
parse count (hard)               2

Статистика 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

for i in 1..1000 loop
m_cursor := dbms_sql.open_cursor;
dbms_sql.parse(
m_cursor,
‘select n1 from t1 where id = :n’,
dbms_sql.native
);
dbms_sql.define_column(m_cursor,1,m_n);
dbms_sql.bind_variable(m_cursor, ‘:n’, i);
m_rows_processed := dbms_sql.execute(m_cursor);
if dbms_sql.fetch_rows(m_cursor) > 0 then
dbms_sql.column_value(m_cursor, 1, m_n);
end if;
dbms_sql.close_cursor(m_cursor);
end loop;

В этом испытании я извлекаю статистики сеанса и число приобретений защелок из 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 в загружаемом пакете примеров): 

begin
m_cursor := dbms_sql.open_cursor;
dbms_sql.parse(
m_cursor,
‘select n1 from t1 where id = :n’,
dbms_sql.native
);
dbms_sql.define_column(m_cursor,1,m_n);
for i in 1..1000 loop
dbms_sql.bind_variable(m_cursor, ‘:n’, i);
m_rows_processed := dbms_sql.execute(m_cursor);
if dbms_sql.fetch_rows(m_cursor) > 0 then
dbms_sql.column_value(m_cursor, 1, m_n);
end if;
end loop;
dbms_sql.close_cursor(m_cursor);
end;

В процессе выполнения этого кода, число приобретений различных защелок библиотечного кэша (в 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).

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

 

Вас заинтересует / Intresting for you:

Кэш словаря (dictionary cache)...
Кэш словаря (dictionary cache)... 4501 просмотров Валерий Павлюков Sun, 25 Feb 2018, 16:20:03
Библиотечный кэш в СУБД Oracle...
Библиотечный кэш в СУБД Oracle... 4147 просмотров Илья Дергунов Sun, 25 Mar 2018, 13:58:24
кэш строки - data dictionary c...
кэш строки - data dictionary c... 4912 просмотров Antoniy Tue, 21 Nov 2017, 13:32:12
Адаптивное разделение курсора ...
Адаптивное разделение курсора ... 4354 просмотров Алексей Вятский Wed, 03 Jan 2018, 07:02:09
Войдите чтобы комментировать