Управление пространством внутри блока данных в СУБД Oracle
Скулкин Дмитрий, Oracle DBA
Целью статьи является описание процессов, которые происходят при модификации содержимого блока данных. Материал носит в основном теоретический характер и может быть интересен тем, кто стремится лучше понять внутренние алгоритмы СУБД Oracle. Весь материал получен опытным путем и не претендует на полноту и абсолютную достоверность. Эксперименты ставились на обычной heap-таблице без индексов, версия Oracle - 9.2.0.6. Весь материал получен опытным путем и не претендует на полноту и абсолютную достоверность, потому я буду благодарен всем заинтересованным читателям, кто захочет высказать свое мнение по поводу содержащейся в статье информации или дополнить этот материал.
Итак, попытаемся понять, как хранятся строки и что происходит внутри блока данных, когда мы вставляем/обновляем/удаляем строки. Для того, чтобы читателям было легче ориентироваться в терминах и более наглядно представлять себе картину, я привел несколько дампов в качестве иллюстраций.
Для начала давайте разберемся, как хранится строка в блоке. Схематично это можно изобразить так (учитывая, что таблица не в кластере):
{ Flag byte / Lock byte / Column Count byte } { Column length / Column Data / Column length / Column Data /...}
Три байта - Flag + Lock + Column Count вместе составляют row header - заголовок строки.
- Различные биты Flag байта описывают состояние/расположение строки,
- Lock байт соответственно показывает, модифицируется ли строка и если да, то с каким ITL-слотом ведется работа,
- Column Count показывает количество столбцов в строке. Это число может отличаться от реального количества столбцов в таблице, например, если строка сцеплена и ее остаток в другом блоке или если последние столбцы таблицы содержат NULL (в этом случае они просто не хранятся)
Непосредственно данным каждого столбца предшествует его размерность, т.е. сколько байт используется для хранения значения столбца таблицы. Column length принимает значения:
- FF - если столбец содержит NULL
- FE+(еще 2 байта длины) - если ширина столбца больше 253 байт, то используется такая конструкция
- иначе показывает актуальное число байт, используемых для хранения значения столбца
Итак, давайте произведем вставку нескольких строк:
SQL> create table test1 (a number,b varchar(1000));
Table created.
SQL> insert into test1 values (1,'first');
1 row created.
SQL> insert into test1 values (2,'second');
1 row created.
SQL> insert into test1 values (3,'third');
1 row created.
SQL> select file_id,block_id from dba_extents where segment_name='TEST1';
FILE_ID BLOCK_ID
---------- ----------
1 20136
SQL> alter system dump datafile 1 block 20137;
System altered. |
Ниже приведен дамп блока данных с моими комментариями, в который вставились наши 3 строчки:
Start dump data blocks tsn: 0 file#: 1 minblk 20137 maxblk 20137
buffer tsn: 0 rdba: 0x00404ea9 (1/20137)
scn: 0x0000.002006eb seq: 0x01 flg: 0x00 tail: 0x06eb0601
frmt: 0x02 chkval: 0x0000 type: 0x06=trans data
Block header dump: 0x00404ea9
Object id on Block? Y
seg/obj: 0x193c csc: 0x00.2006df itc: 2 flg: O typ: 1 - DATA
fsl: 0 fnx: 0x0 ver: 0x01
Itl Xid Uba Flag Lck Scn/Fsc
0x01 0x0004.00f.00000378 0x0080003b.0078.07 ---- 3 fsc 0x0000.00000000
-- Flag "----" показывает, что транзакция активна
0x02 0x0000.000.00000000 0x00000000.0000.00 ---- 0 fsc 0x0000.00000000
data_block_dump,data header at 0x10340845c
===============
tsiz: 0x1fa0
hsiz: 0x18
pbl: 0x10340845c
bdba: 0x00404ea9
76543210
flag=--------
ntab=1
nrow=3
frre=-1
fsbo=0x18 -- смещение начала непрерывной области свободного места в блоке
fseo=0x1f7b -- конец этой области
avsp=0x1f63 -- общий размер свободной области в блоке, включая фрагментированное
tosp=0x1f63 -- размер свободной области в блоке, доступное после commit
0xe:pti[0] nrow=3 offs=0
0x12:pri[0] offs=0x1f94 -- смещение первой строки
0x14:pri[1] offs=0x1f87 -- второй
0x16:pri[2] offs=0x1f7b -- третьей
block_row_dump:
tab 0, row 0, @0x1f94
tl: 12 fb: --H-FL-- lb: 0x1 cc: 2
-- Lock byte показывает, что транзакция работает с 1-ым ITL-слотом
col 0: [ 2] c1 02 -- 1-ый столбец - [длина] значение
col 1: [ 5] 66 69 72 73 74 -- 2-ой столбец - [длина] значение
tab 0, row 1, @0x1f87
tl: 13 fb: --H-FL-- lb: 0x1 cc: 2
col 0: [ 2] c1 03
col 1: [ 6] 73 65 63 6f 6e 64
tab 0, row 2, @0x1f7b
tl: 12 fb: --H-FL-- lb: 0x1 cc: 2
col 0: [ 2] c1 04
col 1: [ 5] 74 68 69 72 64
end_of_block_dump
End dump data blocks tsn: 0 file#: 1 minblk 20137 maxblk 20137
|
Видно, что строки вставляются "снизу вверх", при условии, что низ - это хвост блока, а верх - заголовок блока. Новая строка в большинстве случаев добавляется "выше" существующих, даже если между строками или между строками и хвостом блока есть достаточное для вставки место. Я полагаю, что основание этого - существующий механизм адресации внутри блока, согласно которому нельзя построить матрицу занятого/свободного места в блоке без его полного сканирования (т.к. длина строк хранится только в row header`ах). Даже если строка обновляется с уменьшением размера, то она (фактически это уже новая строка) вставится самой "верхней", а не останется на прежнем месте. Таким образом, при изменениях (update/delete) блок становится фрагментированным - между строк появляются куски свободного места. Единственный случай, когда обновляемая строка остается на своем месте - когда размерности всех полей новой строки идентичны старой (в этом плане использование типа char имеет преимущество над varchar тем, что всегда обеспечивает одинаковую размерность полей, а следовательно и отсутствие фрагментации блока при update`ах).
Можно отметить, что при удалении строк они физически остаются на своих местах, но к их row header`ам добавляется бит D - признак удаленности строки. В row directory не происходит никаких изменений - смещения удаленных строк остаются выставленными в актуальные значения, число строк тоже не изменяется. Последующий rollback (если имеет место быть), тем не менее, не очищает просто бит D, а вставляет удаленную строку обратно в блок как обычно - уже на новое место, "выше" всех. Если же транзакция фиксируется (commit), то дальнейшие действия Oracle зависят от того, возможно ли очистить блок быстро (fast cleanout) или нет.
Если строки удаляются или обновляются с уменьшением размера:
- Транзакции, освободившей место, присваивается free space credit (fsc), хранящийся в ITL-слоте транзакции. При удалении Fsc считается как (кол-во_строк)*(длина_строки-2) байт. Два байта, которые вычитаются из длины строки - Flag и Lock байты, они оставляются как идентификатор удаленности и заблокированности строки. Fsc используется транзакцией-владельцем в том случае, когда для вновь вносимых данных в блок без fsc не хватает места (при этом блок дефрагментируется, удаленные строки, если есть, дефрагментируются с длиной 2 байта). Fsc других транзакций может использоваться при условии, что они подтверждены, блок в этом случае еще и полностью очищается (для подтверждения факта фиксации транзакции).
- Для того, чтобы транзакции могли использовать освобожденное место, блок дефрагментируется.
Ок, давайте посмотрим, как выглядит блок после удаления нескольких строк (приведены только те части дампа, которые имеют для нас интерес):
SQL> delete from test1 where a in (1,2);
2 rows deleted. |
Itl Xid Uba Flag Lck Scn/Fsc
0x01 0x0004.00f.00000378 0x0080003b.0078.09 ---- 3 fsc 0x0015.00000000
--fsc для 2х удаленных строк (12+13)-2*2=21=0х15
0x02 0x0000.000.00000000 0x00000000.0000.00 ---- 0 fsc 0x0000.00000000
data_block_dump,data header at 0x10340845c
===============
fsbo=0x18
fseo=0x1f7b
avsp=0x1f63
tosp=0x1f7c
0xe:pti[0] nrow=3 offs=0 -- в row directory число строк осталось прежним
0x12:pri[0] offs=0x1f94 -- и смещения у удаленных строк остались те же самые
0x14:pri[1] offs=0x1f87
0x16:pri[2] offs=0x1f7b
block_row_dump:
tab 0, row 0, @0x1f94
tl: 2 fb: --HDFL-- lb: 0x1
-- к байту флага добавлен бит D - признак удаленности строки, длина равна 2м байтам
tab 0, row 1, @0x1f87
tl: 2 fb: --HDFL-- lb: 0x1
tab 0, row 2, @0x1f7b
tl: 12 fb: --H-FL-- lb: 0x1 cc: 2
col 0: [ 2] c1 04
col 1: [ 5] 74 68 69 72 64 |
В результате удаления появилось свободное место в размере 21 байт. Когда же оно будет доступно для новых вставок/обновлений? Оно по-настоящему освободится, т.е. прибавится к непрерывному пулу свободного места (fseo-fsbo) после дефрагментации блока. Дефрагментация происходит когда для вставки новой строки недостаточно места в непрерывной области свободного места (fseo-fsbo), но общего свободного места в блоке для вставки достаточно. При дефрагментации все строки в порядке их следования в row directory заново располагаются со дна блока к заголовку и фрагментация свободного пространства таким образом устраняется.
Ок, дефрагментация позволяет вновь использовать освободившееся пространство в блоке, но не корректирует (т.к. при быстрой очистке удаления строк в нем не отображаются) row directory. Row directory корректируется при полной очистке блока. Рассмотрим типы очисток более подробно.
Очистка блока бывает:
- быстрая (fast cleanout). При commit`е Oracle в первую очередь пытается сделать быструю очистку блока. Это означает выставление бита U (commit upper bound) и commit scn в ITL-слоте транзакции. Этого достаточно, чтобы определить факт и время подтверждения транзакции. ITL-слот, прошедший быструю очистку, все равно должен быть в последующем полностью очищен для его использования другой транзакцией, но для этого уже не потребуется чтения заголовка сегмента отката.
- частичная (partial cleanout) - очищается только один необходимый ITL-слот. Так, например, делает rollback.
- полная (total cleanout). Очищаются все неактуальные lock байты в заголовках строк и в ITL-слотах, освобождвются fsc`ы, устанавливается cleanout scn блока и при необходимости устанавливаются commit scn`ы. Кроме того, корректируется row directory - из него удаляются записи об удаленных строках, и соответстветствующие его слоты могут быть переиспользованы (но не место в блоке, на которое ссылается этот слот - как мы уже говорили, место становится доступно только после дефрагментации). Блок подвергается полной очистке либо при проверке факта заблокированности строки, либо когда транзакция пытается использовать ITL-слот, подвергшийся быстрой очистке, либо когда транзакция пытается использовать fsc другой транзакции. Полная очистка генерирует дополнительное редо для той транзакции, которая делает очистку.
Полную и частичную очистки называют еще отложенными (deferred)
Фрагмент дампа блока с откорректированным после полной очистки row directory:
0xe:pti[0] nrow=16 offs=0
0x12:pri[0] offs=0x1f95
-- skip --
0x20:pri[7] offs=0x1f49
0x22:pri[8] sfll=9 -- эти слоты row directory теперь могут быть
0x24:pri[9] sfll=-1 -- перезаписаны
0x26:pri[10] offs=0x1765 |
|
Давайте также рассмотрим вкратце мигрированные/сцепленные (migrated/chained) строки.
- мигрированная строка - строка, перемещенная из одного блока в другой при ее модификации из-за нехватки места в оригинальном блоке. При этом в оригинальном блоке остается указатель на тот блок, куда переместилась строка, в котором, в свою очередь, есть указатель на оригинальный блок. Если строка вновь модифицируется и вновь требуется ее миграция, то, если в оригинальном блоке есть достаточное место, то строка возвращается в оригинальный блок (т.е. мигрированность ликвидируется), иначе строка мигрирует в новый блок-приемник, а указатель (nrid) в оригинальном блоке с учетом этого корректируется (т.е. Oracle не создает цепочек, когда мигрировавшая строка расположена больше чем в двух блоках). Существование мигрированности или сцепленности определяется Oracle`ом по битам байта flag в row header. При мигрировании/сцеплении создаются дополнительные ITL-слоты и после завершения операции их количество будет max(количество_мигрирующих_строк + ITL_default, существующее_количество_ITL-слотов_в_блоке), ITL_default=2 для heap-таблиц
Оригинальный блок (rdba: 0x00407222):
tab 0, row 1, @0xbf9
tl: 9 fb: --H----- lb: 0x2 cc: 0
-- во флаге row header установлен только бит H (head)
nrid: 0x00407223.0 -- указатель на блок/слот, в котором хранится строка
Блок-приемник (rdba: 0x00407223):
tab 0, row 0, @0xbe8
tl: 5024 fb: ----FL-- lb: 0x1 cc: 5
-- одновременно установленные биты F (first) и L (last) показывают, что строка не сцеплена
hrid: 0x00407222.1 -- указатель на блок/слот, из которого мигрировала строка
col 0: [1000] -- далее непосредственно находится сама строка |
- сцепленная строка - строка, располагающаяся в нескольких блоках, потому что ее размер больше размера одного блока. Ниже приведены части дампа блоков таблицы, содержащей одну строку из пяти столбцов по 4000 байт каждый. Т.к. размер блока 8 кБ, эта строка занимает 3 блока. Можно отметить, что существует несколько необычный вид мигрированных строк - строки, количество столбцов которых более 255, хранятся сцепленными, но в одном блоке (возникает это потому, что значение "Column Count" в row header имеет размерность 1 байт). В этом случае nrid первой части строки указывает на тот же блок.
Первый блок (rdba: 0x0040722c)
tab 0, row 0, @0xfcc
tl: 4052 fb: --H-F--N lb: 0x1 cc: 2
-- F - первая часть фрагмента строки, N (next) - последний столбец продолжается в др. блоке
nrid: 0x0040722b.0 -- указатель на блок, где находится след. часть строки
col 0: [4000]
col 1: [39] -- из 4000 байт второго столбца в этом блоке находится только 39
Второй Блок (rdba: 0x0040722b)
tab 0, row 0, @0x4d
tl: 7995 fb: ------PN lb: 0x1 cc: 3
-- P (previous) - 1-ый столбец продолжается - его начало в другом блоке
nrid: 0x0040722a.0
col 0: [3961]
col 1: [4000]
col 2: [18]
Третий блок (rdba: 0x0040722a)
tab 0, row 0, @0x51
tl: 7991 fb: -----LP- lb: 0x1 cc: 2 -- бит L показывает, что это конец строки
col 0: [3982]
col 1: [4000] |
Дополнительная литература:
- "Oracle Block Structure", Veljko Lavrnic
- "A Close Look at Oracle8i Data Block Internals", Dan Hotka
- Учебный курс "DSI402 Space and Transaction Management"
Обсуждение статьи и вопросы:
Анатолий Бачин: Прошу уточнить следующее Ваше положение: "Можно отметить, что при удалении строк они физически остаются на своих местах... Таким образом, при изменениях (update/delete) блок становится фрагментированным - между строк появляются куски свободного места... Последующий rollback (если имеет место быть), тем не менее, не очищает просто бит D, а вставляет удаленную строку обратно в блок как обычно - уже на новое место, "выше" всех. " (конец цитаты) Получается, что в случае нескольких последовательных откатов (rollback) блок может переполниться и появятся вытесненные в другие блоки строки.
Дмитрий Скулкин: В случае rollback не может произойти вытеснение строк в другие блоки, т.к. транзакции, вследствие прошедшей операции удаления,присвоен определенный free space credit, которым она при отсутствии другого свободного места воспользуется. Таким образом все строки останутся в оригинальном блоке.
Давайте за подробностями обратимся к дампам
SQL> create table test1 (a number,b varchar(3000));
Table created.
SQL> alter table test1 pctfree 0;
Table altered.
SQL> insert into test1 values (1,'qqqqqq');
1 row created.
SQL> insert into test1 values (2,'wwwwww');
1 row created.
SQL> insert into test1 values (3,rpad('e',3000,'e'));
1 row created.
SQL> insert into test1 values (4,rpad('r',3000,'r'));
1 row created.
SQL> commit;
Commit complete.
SQL> select file_id,block_id,blocks from dba_extents
where segment_name='TEST1';
FILE_ID BLOCK_ID BLOCKS
---------- ---------- ----------
1 29225 8
SQL> alter system dump datafile 1 block 29226;
System altered. |
Я, как и прежде, буду приводить только значимые фрагменты дампов:
Itl Xid Uba Flag Lck Scn/Fsc
0x01 0x0002.00a.000003c6 0x008000cc.007f.24 --U- 4 fsc 0x0000.001f71d0
0x02 0x0000.000.00000000 0x00000000.0000.00 ---- 0 fsc 0x0000.00000000
fsbo=0x1a
fseo=0x804
avsp=0x7ea
tosp=0x7ea
0xe:pti[0] nrow=4 offs=0
0x12:pri[0] offs=0x1f93
0x14:pri[1] offs=0x1f86
0x16:pri[2] offs=0x13c5
0x18:pri[3] offs=0x804
block_row_dump:
tab 0, row 0, @0x1f93
tl: 13 fb: --H-FL-- lb: 0x1 cc: 2
col 0: [ 2] c1 02
col 1: [ 6] 71 71 71 71 71 71
tab 0, row 1, @0x1f86
tl: 13 fb: --H-FL-- lb: 0x1 cc: 2
col 0: [ 2] c1 03
col 1: [ 6] 77 77 77 77 77 77
tab 0, row 2, @0x13c5
tl: 3009 fb: --H-FL-- lb: 0x1 cc: 2
col 0: [ 2] c1 04
col 1: [3000] -- для читаемости показаны не все данные больших столбцов
65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65
tab 0, row 3, @0x804
tl: 3009 fb: --H-FL-- lb: 0x1 cc: 2
col 0: [ 2] c1 05
col 1: [3000]
72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72
end_of_block_dump
End dump data blocks tsn: 0 file#: 1 minblk 29226 maxblk 29226
|
Мы видим, что свободного места в блоке осталось (0x804-0x1a)=0x7ea=2026 байт. Теперь произведем удаление одной из больших строк и посмотрим, что произойдет.
SQL> delete from test1 where a=3;
1 row deleted.
SQL> alter system dump datafile 1 block 29226;
System altered. |
Itl Xid Uba Flag Lck Scn/Fsc
0x01 0x0002.00a.000003c6 0x008000cc.007f.24 C--- 0 scn 0x0000.001f71d0
0x02 0x0001.00d.000003c6 0x00800010.007e.01 ---- 1 fsc 0x0bbf.00000000
fsbo=0x1a
fseo=0x804
avsp=0x7ea
tosp=0x13ab
0xe:pti[0] nrow=4 offs=0
0x12:pri[0] offs=0x1f93
0x14:pri[1] offs=0x1f86
0x16:pri[2] offs=0x13c5
0x18:pri[3] offs=0x804
block_row_dump:
tab 0, row 0, @0x1f93
tl: 13 fb: --H-FL-- lb: 0x0 cc: 2
col 0: [ 2] c1 02
col 1: [ 6] 71 71 71 71 71 71
tab 0, row 1, @0x1f86
tl: 13 fb: --H-FL-- lb: 0x0 cc: 2
col 0: [ 2] c1 03
col 1: [ 6] 77 77 77 77 77 77
tab 0, row 2, @0x13c5
tl: 2 fb: --HDFL-- lb: 0x2
tab 0, row 3, @0x804
tl: 3009 fb: --H-FL-- lb: 0x0 cc: 2
col 0: [ 2] c1 05
col 1: [3000]
72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72
end_of_block_dump
End dump data blocks tsn: 0 file#: 1 minblk 29226 maxblk 29226 |
Удаленная строка пометилась флагом D в заголовке строки (row header). Теперь, выполняя откат транзакции, Oracle будет вставлять удаленную ранее строчку "выше" остальных. Но, т.к. свободного места выше всех строк недостаточно для вставки 3009-байтной строки, транзакция израсходует свой free space credit (0xbbf=3007 байт), произведет дефрагментацию блока и вставит строку:
SQL> rollback;
Rollback complete.
SQL> alter system dump datafile 1 block 29226;
System altered.
Itl Xid Uba Flag Lck Scn/Fsc
0x01 0x0002.00a.000003c6 0x008000cc.007f.24 C--- 0 scn 0x0000.001f71d0
0x02 0x0000.000.00000000 0x00000000.0000.00 ---- 0 fsc 0x0000.00000000
fsbo=0x1a
fseo=0x804
avsp=0x7ea
tosp=0x7ea
0xe:pti[0] nrow=4 offs=0
0x12:pri[0] offs=0x1f93
0x14:pri[1] offs=0x1f86
0x16:pri[2] offs=0x804
0x18:pri[3] offs=0x13c5
block_row_dump:
tab 0, row 0, @0x1f93
tl: 13 fb: --H-FL-- lb: 0x0 cc: 2
col 0: [ 2] c1 02
col 1: [ 6] 71 71 71 71 71 71
tab 0, row 1, @0x1f86
tl: 13 fb: --H-FL-- lb: 0x0 cc: 2
col 0: [ 2] c1 03
col 1: [ 6] 77 77 77 77 77 77
tab 0, row 2, @0x804
tl: 3009 fb: --H-FL-- lb: 0x0 cc: 2
col 0: [ 2] c1 04
col 1: [3000]
65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65
tab 0, row 3, @0x13c5
tl: 3009 fb: --H-FL-- lb: 0x0 cc: 2
col 0: [ 2] c1 05
col 1: [3000]
72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72 72
end_of_block_dump
End dump data blocks tsn: 0 file#: 1 minblk 29226 maxblk 29226
|
Обратите внимание, что строка, удовлетворяющая условию a=3, которую мы удаляли, хоть и занимает по-прежнему 3-ий слот row directory, но располагается по смещению 0x804 - т.е. она вставлена "выше" всех, после дефрагментации блока (в результате которой строка a=4 теперь находится там, где ранее располагалась удаленная строка - по смещению 0x13c5)
|