Как устроен поиск блоков данных в базе Oracle

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

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

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

Решение заключается в использовании большой хэш-таблицы с большим числом хэш-блоков: к каждому хэш-блоку присоединяется очень короткий связанный список из заголовков буферов, сами хэш-блоки объединяются в небольшие группы и каждая группа защищается собственной защелкой – печально известной защелкой цепочки буферов. На рис. 1 изображен миниатюрный кэш буферов с 4 защелками, 16 хэш-блоками и всего 23 заголовками буферов.

Очень маленький кэш буферов в базе данных Oracle

Рис. 1. Очень маленький кэш буферов в Oracle

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

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

Рисунок 1 прост и понятен (я надеюсь) и достаточно близок к истине. Не забывайте, однако, что данная конкретная схема – лишь один из множества способов представления сложной паутины указателей, пронизывающих кэш данных.

Если бы я наложил на рис. 1 связи, образующие списки замены, то получил бы две запутанные линии, беспорядочно соединяющие заголовки буферов и повинующиеся двум правилам: (a) каждый заголовок должен находиться на одной из линий и (b) ни один заголовок не может находиться на двух линиях сразу. С другой стороны, если вернуться к упорядоченной схеме на рис. 4 с двумя простыми линиями списков замены и наложить на нее связи, образующие цепочки буферов, получится масса тонких линий, соединяющих по два или три буфера, разбросанных по спискам замены.

Сложность указателей легко ощутить, если вывести список буферов на уровне 1. Ниже приводится содержимое одного заголовка буфера из такого списка: 

BH (66BEAF5C) file#: 1 rdba: 0x004096b0 (1/38576) class: 1 ba: 668D8000
set: 3 blksize: 8192 bsi: 0 set-flg: 0 pwbcnt: 0
dbwrid: 0 obj: 18 objn: 18 tsn: 0 afn: 1
hash: [6ffa0870,6ffa0870] lru: [66bfab94,66bed2f0]
lru-flags: moved_to_tail on_auxiliary_list
ckptq: [NULL] fileq: [NULL] objq: [66bed348,66bfabec]
st: XCURRENT md: NULL tch: 0
flags: only_sequential_access
LRBA: [0x0.0.0] HSCN: [0xffff.ffffffff] HSUB: [65535]

В этом дампе есть две строки, сообщающие о наличии связанного списка некоторого типа: в четвертой строке присутствуют указатели на цепочки буферов (hash) и на списки замены (lru), и в шестой строке присутствуют указатели на очередь контрольных точек (checkpoint queue, ckptq), очередь файлов (fileq) и очередь объектов (objq). Блок не был изменен, поэтому в нем отсутствуют очереди контрольных точек и файлов, но мы будем исследовать эти очереди, а также очередь объектов. Совпадение прямого и обратного указателей на цепочку hash в данном примере говорит о том, что в этой цепочке имеется только один заголовок буфера – этот прием широко используется в Oracle. Чем меньше заголовков будет связано с каждым хэш-блоком, тем быстрее будет выполняться поиск, и когда выполняется поиск какого-то определенного блока, чтобы убедиться в его присутствии (или отсутствии) в памяти, было бы крайне желательно, чтобы такой поиск выполнялся как можно быстрее.

На рис. 1 изображены только четыре защелки цепочек буферов, каждая из которых охватывает несколько хэш-блоков, с небольшим числом заголовков в каждом. Число заголовков на хэш-блок очень близко к тому, что можно наблюдать в промышленных системах, но с числом хэш-блоков и защелок ситуация обстоит иначе. Oracle создает огромное число хэш-блоков (см. врезку ниже) и затем охватывает группы хэш-блоков защелками (обычно по 32 хэш-блока на защелку).


Хэш-блоки кэша данных


В ранних версиях Oracle (включая 8.0) число хэш-блоков было примерно равно db_block_buffers/4 (с округлением до простого числа) и для каждого из них назначалась отдельная защелка.

С увеличением размера базы данных увеличивались и размеры кэша данных, поэтому в Oracle был изменен алгоритм определения числа хэш-блоков – теперь их число примерно в два раза меньше числа буферов и каждая защелка охватывает несколько хэш-блоков – обычно от 32 до 128. Число хэш-блоков все так же округляется вверх до ближайшего простого числа (хотя скрытый параметр _db_block_hash_buckets не всегда отражает этот факт – прямо сейчас передо мной версия 8.1.7.4, чей параметр сообщает о 16384 хэш-блоках, тогда как переменная kcbnhb (см. Приложение) хранит простое целое число 16411).

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


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

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

 

Закрепленные буферы

Помимо привязки блока данных к хэш-блоку остается позаботиться о выполнении еще нескольких операций: добавление буфера в хэш-блок после чтения блока данных с диска; удаление буфера из хэш-блока, если возникнет необходимость использовать его для другого блока данных; доступ к текущей версии буферизованного блока; изменение буферизованного блока; и копирование блока, хранящегося в буфере, для создания согласованной версии. Все эти операции требуют выполнить обход связанного списка и, возможно, вставить или удалить элемент списка. Это означает, что может потребоваться выполнить такую последовательность операций:

  1. Получить нужный хэш-блок из адреса блока данных.
  2. Приобрести соответствующую защелку.
  3. Начать обход списка, связанного с хэш-блоком, чтобы найти буфер.
  4. Выполнить некоторые действия с содержимым буфера.
  5. Освободить защелку.

Этот краткий перечень подчеркивает одну существенную угрозу. Несмотря на то, что односвязный список можно обойти очень быстро, прежде, чем «что-то сделать» нужно приобрести защелку. Поэтому, все операции должны выполняться очень быстро, потому что доступ к буферизованным данным является весьма популярным времяпрепровождением в Oracle и параллельно с нами многие другие сеансы могут пытаться получить ту же самую защелку.

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

Поскольку для того, чтобы «что-то сделать» с содержимым буфера, может потребоваться относительно много времени, Oracle часто применяет двухфазную стратегию приобретения защелки, с целью избежать удержания защелки на все время работы. Некоторые операции могут быть выполнены при удерживаемой защелке, но Oracle часто применяет следующую стратегию:

  1. Приобретает защелку.
  2. Находит и закрепляет буфер.
  3. Освобождает защелку.
  4. Выполняет требуемые операции с содержимым буфера.
  5. Приобретает защелку.
  6. Открепляет буфер.
  7. Освобождает защелку.

Чтобы пояснить происходящее более наглядно, необходимо добавить деталей в нашу картину кэша буферов. На рис. 2 изображена единственная хэш-цепочка с двумя заголовками буферов и несколькими закреплениями буферов (buffer pins).

Единственная хэш-цепочка с двумя закрепленными буферами

Рис. 2. Единственная хэш-цепочка с двумя закрепленными
буферами

Как показано на рис. 2, каждый буфер имеет список действующих сеансов и список ожидающих сеансов, и если заглянуть в структуру x$bh, можно увидеть уже знакомое соглашение об именовании пар столбцов – окончания NXT и PRV в именах предупреждают, что мы видим еще несколько связанных списков. В данном случае – действующих (US) и ожидающих (WA) сеансов: 

Name                          Null?    Type
----------------------------- -------- ---------
US_NXT                                 RAW(4)
US_PRV                                 RAW(4)
WA_NXT                                 RAW(4)
WA_PRV                                 RAW(4)

Обратите внимание, что один заголовок буфера на рис. 2 отмечен символом S, а другой – символом X. Буфер можно закрепить в разделяемом (shared, S) или монопольном (exclusive, X) режиме. Если в настоящий момент буфер не закреплен или закреплен в совместимом режиме, закрепление (представляемое структурой x$kcbbf) можно присоединить к списку действующих сеансов. Если буфер закреплен в несовместимом режиме, закрепление должно присоединяться к списку ожидающих сеансов, после чего сеанс переводится в состояние ожидания, пока буфер не станет доступным. Состояние ожидания – это печально известное событие buffer busy waits. Однако, в версии 10g был реализован специальный вариант этого события – если сеанс ждет, пока другой сеанс завершит чтение блока с диска в буфер, возникает событие read by other session.

Структура закрепления (pin) включает адрес сеанса, удерживающего закрепление или ожидающего на закреплении. Поэтому, когда держатель блокировки снимает закрепление с заголовка буфера, он может переместить ожидающий сеанс в начало списка. Время ожидания освобождения буфера ограничивается 1 секундой (настраивается с помощью скрытого параметра _buffer_busy_wait_timeout), то есть, если ожидающий сеанс не был перемещен в очереди в течение этого времени, он возобновляет выполнение, предполагает, что возникло состояние взаимоблокировки между держателем блокировки и ожидающим сеансом, генерирует событие buffer deadlock wait (хотя, в действительности, не переходит в режим ожидания) и пытается решить проблему, снимая все удерживаемые закрепления и пытаясь вновь установить их. Интервал в одну секунду выглядит достаточно продолжительным для ожидания отдельного буфера, но, если нет причин считать, что на ожиданиях теряется слишком много времени, я бы не стал экспериментировать с настройкой скрытого параметра.

Примечание. Удерживая защелку цепочки буферов можно: добавлять в цепочку или удалять из нее заголовки буферов; добавлять или удалять закрепления из списка закреплений, изменять режим закрепления. Удерживая защелку цепочки буферов нежелательно или нельзя изменять содержимое буфера. Делать это можно, только удерживая закрепление для блока в монопольном режиме, и в это же время нет необходимости удерживать защелку. (Исключением из правила является фиксирующая очистка (commit cleanout).)

И последнее, что хотелось бы сказать о закреплении буферов: после присоединения закрепления к заголовку буфера, его нельзя снять немедленно. Если механизм запросов посчитает, что инструкция SQL повторно обратится к буферу в ближайшем будущем, он оставит заголовок буфера закрепленным до конца выполнения инструкции. Это как раз та ситуация, которую отражает статистика buffer is pinned count – Oracle закрепил буфер в начале выполнения запроса и обращался к нему повторно, не открепляя между обращениями: это самое лучшее, что Oracle может сделать.

Поиск блоков данных в базе Oracle - что нужно знать про кэши, буферы и хэш-таблицы 

Логический ввод/вывод

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

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

После окончания поиска все готово к исследованию блока и защелку можно освободить. Это действие отражается в статистике экземпляра consistent get - examination. Далее для некоторых блоков существует менее дорогостоящий путь, не требующий повторного приобретения защелки. Такими блоками являются индексные блоки – корневой (root) и ветвления (branch), индексные листовые блоки (leaf blocks) для индексов по уникальным ключам, блоки таблиц, доступные из индексов по уникальным ключам, и блоки отмены.

Если блок находится в памяти, но не является кандидатом на менее дорогостоящее исследование, тогда выполняется более традиционный «двухфазный» алгоритм. В этом случае, при удерживаемой защелке, к заголовку буфера присоединяет разделяемое закрепление (shared pin), защелка освобождается, просматривается содержимое блока, приобретается защелку, снимается закрепление и защелка вновь освобождается.

Однако, если Oracle сделает предположение, что в течение ближайших микросекунд придется вновь обратиться к буферу, может быть принято решение оставить буфер закрепленным до конца выполнения текущего запроса к базе данных.

Примечание. Закрепления буферов (buffer pins) называют также «дескрипторами буферов» (buffer handles). Имеется несколько параметров, позволяющих управлять количеством закреплений и особенностями их использования: параметр _db_handles определяет размер массива структур закреплений, а параметр _db_handles_cached определяет число закреплений, которое может быть «зарезервировано» сеансом. По умолчанию параметр _db_handles_cached имеет значение 5. Имеется также параметр _cursor_db_buffers_pinned, определяющий общее число буферов, которые сеанс сможет закрепить в любой момент. Статистика buffer is pinned count сообщает, сколько раз сеанс смог воспользоваться закреплениями для повторного обращения к буферам.

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

 

Изменение данных в базе Oracle

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

Ключевым словом здесь является «монопольный». Если какой-то другой сеанс закрепил буфер, наш сеанс не сможет сделать то же самое и вынужден будет присоединить свое закрепление к списку ожидающих сеансов и послать событие buffer busy wait.

Однако, существуют альтернативные пути. Например, если изменить блок в ходе сканирования таблицы, вместо изменения блока на месте Oracle сгенерирует событие switch current to new buffer. Это означает, что есть еще один механизм, который может использовать и изменять списки замены и хэш-цепочки. Когда происходит копирование блока в новый буфер, предыдущая его версия становится согласованной копией, то есть, если в одной инструкции изменить блок пять раз, будет достигнут предел шести копий, определяемый параметром _db_block_max_cr_dba. Поэтому, чтобы избежать появления в кэше большого объема мусора и сохранить хэш-цепочку максимально короткой, Oracle начнет включать старые копии в список замены, если последуют другие изменения в блоке.

 

Загрузка хэш-цепочки

Физический ввод/вывод, создание согласованных версий и копирование текущих блоков в новые буферы требуют удаления буфера из списка замены и добавления его в хэш-цепочку, и этот момент требует пояснений.

Здесь существует интересная проблема. Как отмечалось уже  в наших блогах, процесс не может запросить защелку в режиме готовности к ожиданию (willing-to-wait mode), если уже владеет защелкой более низкого уровня. Так что же это за уровни наших двух защелок? 

SQL> select name, level#
2 from      v$latch
3 where     name in (‘cache buffers lru chain’,‘cache buffers chains’)
4 /
NAME                               LEVEL#
------------------------------ ----------
cache buffers lru chain                 2
cache buffers chains                    1
2 rows selected.

Защелка цепочки буферов имеет более низкий уровень, чем защелка цепочки LRU, поэтому процесс не может запросить приобретение защелки для цепочки LRU в режиме готовности к ожиданию, если уже владеет защелкой цепочки буферов. Давайте разберем, что это значит: процесс владеет защелкой цепочки буферов (которую далее в разделе я буду называть хэш-защелкой (hash latch)), потому что перед этим выполнялся поиск буфера в хэш-цепочке и искомый буфер был найден. По каким-то причинам процессу понадобилось добавить еще один буфер в цепочку, для чего необходимо приобрести защелку цепочки LRU (которую далее в разделе я буду называть LRU-защелкой (lru latch)), чтобы переместить буфер из списка REPL_AUX в среднюю точку списка REPL_MAIN. Но ее нельзя запросить в режиме готовности к ожиданию, потому что процесс уже владеет защелкой более низкого уровня.

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

Примечание. При создании копий буферов важную роль играет выбор пула. Когда процесс генерирует событие switch current to new buffer, новый буфер будет получен из того же пула; если генерируется событие CR blocks created, Oracle будет знать, что создается переходная копия блока и попытается минимизировать влияние этого блока на управление кэшем. Например, если создается CR-копия (согласованная копия) блока, принадлежащего пулу постоянного хранения (keep pool), копия будет создана в пуле временного хранения (recycle pool, если существует), благодаря чему уменьшается разрушительное влияние на пул постоянного хранения. Эта стратегия все еще продолжает развиваться. Как следствие, в последних версиях Oracle в пуле по умолчанию (default pool) можно обнаружить буферы, которые ожидалось увидеть в пуле постоянного хранения.

 

Согласованные копии

Описав основные сложности включения нового буфера в хэш-цепочку, я мало что могу добавить по теме создания согласованных копий. К этому моменту сеанс владеет требуемой защелкой и начинает обход связанного списка. Он обнаруживает, что текущая версия блока (CUR, current) блока находится в памяти, но число SCN выше начального числа SCN, с которым запускался запрос, по этой причине может потребоваться создать согласованную копию блока.

Но, прежде чем приступать к созданию согласованной копии, можно выполнить обход хэш-цепочки и посмотреть, нет ли в ней уже готовой согласованной копии. Для этой цели в хэш-цепочке предусмотрена небольшая оптимизация – CUR-версия блока всегда стоит в начале цепочки. Я также припоминаю замечание, уведенное много лет тому назад, в котором предполагалось, что CR-копии следуют в цепочке в порядке уменьшения чисел SCN и копия с наибольшим числом SCN стоит в списке первой, но первый же тест, который я выполнил, не подтвердил это предположение.

Если копия, соответствующая потребностям, не найдена, тогда либо копируется CUR-версия, либо за отправную точку берется другая, более подходящая CR-версия блока. В последнем случае появляется возможность немного сэкономить на затратах, потому что CR-версия может быть ближе к целевой точке во времени. Для этого потребуется выбрать блок из списка REPL_AUX, вставить его в список REPL_MAIN и одновременно в хэш-цепочку. А так как необходимо предотвратить случайное его чтение или изменение, блок следует закрепить в монопольном режиме. Именно в этот момент становятся очевидными выгоды от наличия списка REPL_AUX, как отдельного источника доступных буферов: поиск буфера должен выполняться очень быстро, потому что любые задержки в операциях с памятью крайне отрицательно скажутся на производительности в высоконагруженных системах.

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

 

Физический ввод/вывод

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

Примечание. Блоки могут читаться с использованием методов, генерирующих события file sequential read (чтение единственного блока), db file parallel read (чтение нескольких блоков из произвольных мест; например, чтение нескольких листовых блоков индексов, определяемых одним блоком ветвления), или db file scattered read (чтение нескольких блоков, логически смежных в файле). Стоит отметить, что максимальный объем чтения вразброс (scattered read) определяется параметром db_file_multiblock_read_count, но, прежде чем выполнить такое чтение, Oracle проверяет каждый блок по очереди – не хранится ли он уже в кэше, и выполняет подобное чтение только до первого блока в кэше.

 

Сканирование таблиц

Услышав от кого-то слово «сканирование таблиц» (tablescans), добавляйте про себя: «и быстрое полное сканирование индексов» (index fast full scans). Сканирование таблиц является одной из самых ресурсоемких задач в Oracle, особенно если таблицы целиком приходится читать с диска. Сканирование больших объектов может привести к выталкиванию из кэша значительной части полезных данных – данных, которые очень скоро придется вновь прочитать с диска. В действительности сканирование таблиц не должно происходить слишком часто в системах OLTP, и когда эти операции случаются, следует гарантировать, что они не вызовут серьезных проблем. Вероятные проблемы, вызываемые сканированием таблиц, вынудили разработчиков Oracle написать код, отличающий «короткие» и «длинные» таблицы (и, совсем недавно, еще и «средние» таблицы, хотя для данного варианта не предусмотрено никаких статистик).

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

Идея «длинной» таблицы заключается в том, что при сканировании по-настоящему большого объекта было бы нежелательно, чтобы его блоками были вытеснены из кэша все остальные данные, поэтому сканирование больших объектов должно выполняться с применением алгоритма, ограничивающего разрушительное влияние. В прошлом (в данном случае имеются в виду версии 8i и 9 i) в Oracle использовалась простая стратегия: если обнаруживалось, что размер таблицы не превышает 2% кэша данных, чтение блоков во время сканирования выполнялось как обычно за исключением того, что счетчик обращений (TCH) в буферах отключался, поэтому объект быстро оказывался в конце LRU списка и не мог попасть в горячую его часть, независимо от числа обращений, даже когда обращения происходили вне рамок сканирования таблицы.

Если оптимизатор обнаруживал, что размер таблицы превышает 2-процентное ограничение, буферы, использовавшиеся для чтения таблицы, немедленно перемещались в конец LRU списка, то есть для чтения таблицы повторно использовалось ограниченное число одних и тех же буферов. Если взглянуть на статистики экземпляра (v$sysstat), можно увидеть, что две из них отражают количественные показатели деятельности, связанной со сканированием таблиц (однако, вы не найдете статистик, которые позволили бы провести сравнение «длинных и коротких таблиц» для быстрого и полного сканирования индексов):

table scans (short tables)
table scans (long tables)

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

Для второго случая («средние» таблицы) установлен 10-процентный предел (правда, я не знаю, как его настроить). В этом случае сканирование таблиц изначально выполняется подобно сканированию длинных таблиц, то есть счетчики обращений в буферах, куда загружаются блоки, не увеличиваются и соответствующие буферы немедленно перемещаются в список REPL_AUX. Однако, если повторить сканирование пока блоки остаются в кэше данных, с этого момента счетчики буферов начнут увеличиваться и операция сканирования будет идентифицирована как сканирование короткой таблицы. Это обстоятельство может повлечь за собой неприятный побочный эффект: большинство прочитанных блоков окажется в буферах, помещенных в список REPL_AUX, но теперь счетчики обращений будут увеличены и похоже нет никакого механизма, который переместил бы эти буферы обратно в REPL_MAIN, пока они не окажутся в конце LRU списка и не попадут в горячую половину REPL_MAIN. Это означает, что в какой-то момент любой сеанс может запросить единственный буфер и «застынет» пока не переместит все эти буферы (вплоть до 10% от объема кэша) в список REPL_MAIN.

Для последнего случая установлен 25-процентный предел. В этом случае счетчики обращений отключаются полностью, и буферы очень быстро обращаются в списке REPL_AUX. Часто выполняется чтение сразу нескольких блоков с извлечением нескольких буферов из REPL_AUX, следующее чтение нескольких блоков вновь приводит к извлечению буферов из REPL_AUX, но при этом первый пакет буферов возвращается в REPL_AUX, и так продолжается в цикле до окончания сканирования. При сканировании очень большой таблицы ее блоки могут занять все буферы из списка REPL_AUX и лишь малую часть из списка REPL_MAIN. Таким способом Oracle защищает кэш данных при сканировании таблиц.

Примечание. Выше я говорил, что поведение Oracle зависит от того, как будут «интерпретироваться» размеры таблиц. В последних версиях Oracle решение принимается на основе статистик объектов, то есть, если вероятность, что Oracle примет неверное решение (например, попытается поместить в кэш огромную таблицу, статистики которой говорят о том, что она очень короткая). Однако такая ошибка может возникнуть только однажды, после сопоставления фактического размера с предсказанным, значения статистик обновятся и в следующих операциях сканирования будет использоваться уже скорректированная информация, пока она не будет вытолкнута из SGA.

 

В заключение

Кэш данных можно разбить на несколько пулов буферов, по размерам блоков или по особенностям использования (пулы постоянного и временного хранения). Однако часто администраторы ограничиваются единственным пулом с размером блока по умолчанию, предлагаемым утилитой Database Configuration Assistant (DBCA) для данной платформы – и это наиболее оправданная стратегия.

Кэш данных можно также разбить на гранулы фиксированного размера: 4, 8 или 16 Мбайт, в зависимости от версии платформы и размера SGA. Гранулирование пула делает возможным динамическое перераспределение памяти между кэшем данных и другими ключевыми компонентами SGA. Каждый пул состоит из множества отдельных гранул, а каждая отдельная гранула принадлежит единственному пулу. Гранулы, составляющие кэш данных, хранятся в виде массива буферов, соответствующего ему массива заголовков буферов и небольшой области для хранения управляющей информации.

Каждый пул может быть разбит на несколько рабочих наборов данных (working data sets), собранных в связанные списки заголовков буферов, которые в свою очередь указывают на буферы. Рабочий набор данных является важной «единицей сложности» в Oracle; каждый набор защищен собственной защелкой цепочки LRU и «очищается» единственным процессом записи (database writer, dbwr). Однако, каждый процесс dbwr может обслуживать несколько рабочих наборов. Число наборов (на один пул буферов) и процессов dbwr зависит от параметра cpu_count.

Каждый рабочий набор разбит на пару связанных списков, основной список замены (main replacement list, REPL_MAIN) и вспомогательный список замены (the auxiliary replacement list, REPL_AUX), между которыми постоянно происходит перемещение буферов. Существуют также другие связанные списки, образующие небольшие подмножества буферов в рабочих наборах, но они имеют отношение к операциям записи. Назначение списка REPL_AUX – держать буфера, которые могут быть повторно использованы немедленно, если сеансу потребуется прочитать блок с диска или создать копию блока, уже хранящегося в памяти. Назначение списка REPL_MAIN – следить за последними использовавшимися блоками (в буферах), гарантировать сохранность в кэше «популярных» блоков и обеспечить быструю очистку «непопулярных».

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

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

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

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

Oracle и непроцедурный доступ ...
Oracle и непроцедурный доступ ... 8522 просмотров Antoni Tue, 21 Nov 2017, 13:32:50
Язык SQL в Oracle
Язык SQL в Oracle 4294 просмотров Ирина Светлова Tue, 21 Nov 2017, 13:26:01
Видеокурс по администрированию...
Видеокурс по администрированию... 10719 просмотров Илья Дергунов Mon, 14 May 2018, 05:08:47
СУБД Oracle: обзор характерист...
СУБД Oracle: обзор характерист... 15813 просмотров Antoni Fri, 24 Nov 2017, 07:35:05
Войдите чтобы комментировать