73.6. Компоновка страницы базы данных
В данном разделе рассматривается формат страницы, используемый в таблицах и индексах Postgres Pro.[16] Последовательности и таблицы TOAST форматируются как обычные таблицы.
В дальнейшем подразумевается, что байт содержит 8 бит. В дополнение, термин элемент относится к индивидуальному значению данных, которое хранится на странице. В таблице элемент — это строка; в индексе — элемент индекса.
Каждая таблица и индекс хранятся как массив страниц фиксированного размера (обычно 8 kB, хотя можно выбрать другой размер страницы при компиляции сервера). В таблице все страницы логически эквивалентны, поэтому конкретный элемент (строка) может храниться на любой странице. В индексах первая страница обычно резервируется как метастраница, хранящая контрольную информацию, а внутри индекса могут быть разные типы страниц, в зависимости от метода доступа индекса.
Таблица 73.2 показывает общую компоновку страницы. Каждая страница имеет пять частей.
Таблица 73.2. Общая компоновка страницы
| Элемент | Описание |
|---|---|
| Данные заголовка страницы | Длина — 24 байта. Содержит общую информацию о странице, включая указатели свободного пространства. |
| Данные идентификаторов элементов | Массив идентификаторов, указывающих на фактические элементы. Каждый идентификатор представляет собой пару «смещение, длина» и занимает 4 байта. |
| Свободное пространство | Незанятое пространство. Новые идентификаторы элементов размещаются с начала этой области, сами новые элементы — с конца. |
| Элементы | Сами элементы данных как таковые. |
| Специальное пространство | Специфические данные метода доступа. Для различных методов хранятся различные данные. Кроме того, сюда помещается информация о 64-битных идентификаторах транзакций. |
Первые 20 байт каждой страницы образуют заголовок страницы (PageHeaderData). Его формат подробно описан в Таблице 73.3. В первом поле отслеживается самая последняя запись в WAL, связанная с этой страницей. Второе поле содержит контрольную сумму страницы, если включён режим Контрольные суммы данных. Затем идёт двухбайтовое поле, содержащее биты флагов. За ним следуют три двухбайтовых целочисленных поля (pd_lower, pd_upper и pd_special). Они содержат смещения в байтах от начала страницы до начала незанятого пространства, до конца незанятого пространства и до начала специального пространства. В следующих 2 байтах заголовка страницы, в поле pd_pagesize_version, хранится размер страницы и индикатор версии. Начиная с PostgreSQL 8.3, используется версия 4; в PostgreSQL 8.1 и 8.2 использовалась версия 3; в PostgreSQL 8.0 — версия 2; в PostgreSQL 7.3 и 7.4 — версия 1; а в предыдущих выпусках — версия 0. В Postgres Pro Enterprise используется версия 0x00FF − 1. (Основная структура страницы и формат заголовка почти во всех этих версиях одни и те же, но структура заголовка строк в куче изменялась.) Размер страницы присутствует, в основном, только для перекрёстной проверки; возможность использовать в одной инсталляции разные размеры страниц не поддерживается.
Таблица 73.3. Данные заголовка страницы (PageHeaderData)
| Поле | Тип | Длина | Описание |
|---|---|---|---|
| pd_lsn | PageXLogRecPtr | 8 байт | LSN: Следующий байт после последнего байта записи WAL для последнего изменения на этой странице |
| pd_checksum | uint16 | 2 байта | Контрольная сумма страницы |
| pd_flags | uint16 | 2 байта | Биты признаков |
| pd_lower | LocationIndex | 2 байта | Смещение до начала свободного пространства |
| pd_upper | LocationIndex | 2 байта | Смещение до конца свободного пространства |
| pd_special | LocationIndex | 2 байта | Смещение до начала специального пространства |
| pd_pagesize_version | uint16 | 2 байта | Информация о размере страницы и номере версии компоновки |
За заголовком страницы следуют идентификаторы элемента (ItemIdData), каждому из которых требуется 4 байта. Идентификатор элемента содержит байтовое смещение до начала элемента, его длину в байтах и несколько битов атрибутов, которые влияют на его интерпретацию. Новые идентификаторы элементов размещаются по мере необходимости от начала свободного пространства. Количество имеющихся идентификаторов элементов можно определить через значение pd_lower, которое увеличивается при добавлении нового идентификатора. Поскольку идентификатор элемента никогда не перемещается до тех пор, пока он не освобождается, его индекс можно использовать в течение длительного периода времени, чтобы ссылаться на элемент, даже когда сам элемент перемещается по странице для уплотнения свободного пространства. Фактически каждый указатель на элемент (ItemPointer, также известный как CTID), созданный Postgres Pro, состоит из номера страницы и индекса идентификатора элемента.
Сами элементы хранятся в пространстве, выделяемом в направлении от конца к началу незанятого пространства. Точная структура меняется в зависимости от того, каким будет содержимое таблицы. Как таблицы, так и последовательности используют структуру под названием HeapTupleHeaderData, которая описывается ниже.
Последний раздел является «особым разделом», который может содержать всё, что необходимо методу доступа для хранения. Например, индексы-B-деревья хранят ссылки на страницы слева и справа, равно как и некоторые другие данные, соответствующие структуре индекса. Обычные таблицы и последовательности хранят в этом разделе структуру HeapPageSpecialData. Её формат описан в Таблице 73.4.
Рисунок 73.1 показывает, как эти компоненты размещаются в странице.
Рисунок 73.1. Компоновка страницы
73.6.1. Компоновка строки таблицы
Структура HeapPageSpecialData состоит из 24 байт. Первыми в ней идут два 8-байтовых поля, содержащих базу, то есть смещение, для коротких (4-байтовых) идентификаторов транзакций на этой странице. Следующее поле подсказывает, насколько вероятна возможность получить выигрыш, произведя очистку страницы: оно отслеживает самый старый XMAX на странице, не подвергавшийся очистке. Последнее поле содержит число, обозначающее тип страниц (0x1010 — для обычных таблиц, 0x1717 — для последовательностей).
Таблица 73.4. Спецданные страницы кучи (HeapPageSpecialData)
| Поле | Тип | Длина | Описание |
|---|---|---|---|
| pd_xid_base | TransactionId | 8 байт | База для коротких 4-байтных идентификаторов транзакций на этой странице |
| pd_multi_base | TransactionId | 8 байт | База для коротких 4-байтных идентификаторов мультитранзакций на этой странице |
| pd_prune_xid | ShortTransactionId | 4 байта | Самый старый неочищенный идентификатор XMAX на странице или ноль при отсутствии такового |
| pd_magic | uint32 | 4 байта | Магическое число, обозначающее тип страницы |
Все строки таблицы имеют одинаковую структуру. Они включают заголовок фиксированного размера (занимающий 40 байт на большинстве машин), за которым следует необязательная битовая карта пустых значений, необязательное поле идентификатора объекта и данные пользователя. Подробное описание заголовка представлено в Таблице 73.5. Актуальные пользовательские данные (столбцы строки) начинаются после смещения, заданного в t_hoff, которое должно всегда быть кратным величине MAXALIGN для платформы. Битовая карта пустых значений имеется тогда, когда бит HEAP_HASNULL установлен в значении t_infomask. В случае наличия она начинается сразу после фиксированного заголовка и занимает столько байтов, сколько требуется для размещения битов по количеству столбцов (т. е. число битов равно количеству атрибутов, определяемому полем t_infomask2). В этом списке битов установленный бит означает непустое значение, а сброшенный соответствует пустому значению. Когда битовая карта отсутствует, все столбцы считаются непустыми. Идентификатор объекта присутствует, если только в значении t_infomask установлен бит HEAP_HASOID_OLD. Если он есть, он расположен сразу перед началом t_hoff. Любое заполнение, необходимое для того, чтобы сделать t_hoff кратным MAXALIGN, будет добавлено между битовой картой пустых значений и идентификатором объекта. (Это в свою очередь гарантирует, что идентификатор объекта будет правильно выровнен.)
Таблица 73.5. Данные заголовка строки таблицы (HeapTupleHeaderData)
| Поле | Тип | Длина | Описание |
|---|---|---|---|
| t_xmin | ShortTransactionId | 4 байта | значение XID вставки |
| t_xmax | ShortTransactionId | 4 байта | значение XID удаления |
| t_cid | CommandId | 4 байта | значение CID для вставки и/или удаления (пересекается с t_xvac) |
| t_xvac | ShortTransactionId | 4 байта | XID для операции VACUUM, которая перемещает версию строки |
| t_ctid | ItemPointerData | 6 байт | текущее значение TID этой или более новой версии строки |
| t_infomask2 | uint16 | 2 байта | количество атрибутов плюс различные биты флагов |
| t_infomask | uint16 | 2 байта | различные биты флагов |
| t_hoff | uint8 | 1 байт | отступ до пользовательских данных |
Интерпретировать текущие данные можно только с использованием информации, полученной из других таблиц, в основном из pg_attribute. Ключевыми значениями, необходимыми для определения расположения полей, являются attlen и attalign. Не существует способа непосредственного получения заданного атрибута, кроме случая, когда имеются только поля фиксированной длины и при этом нет значений NULL. Все эти особенности учитываются в функциях heap_getattr, fastgetattr и heap_getsysattr.
Чтобы прочитать данные, необходимо просмотреть каждый атрибут по очереди. В первую очередь нужно проверить, является ли значение поля пустым согласно битовой карте пустых значений. Если это так, можно переходить к следующему полю. Затем следует убедиться, что выравнивание является верным. Если это поле фиксированной ширины, берутся просто все его байты. Если это поле переменной длины (attlen = -1), всё несколько сложнее. Все типы данных с переменной длиной имеют общую структуру заголовка struct varlena, которая включает общую длину сохранённого значения и некоторые биты флагов. В зависимости от установленных флагов, данные могут храниться либо локально, либо в таблице TOAST. Также, возможно сжатие данных (см. Раздел 73.2).
[16] На самом деле этот формат страниц не является обязательным ни для табличных, ни для индексных методов доступа. Его всегда использует табличный метод heap и все существующие индексные методы, но в метастраницах индексов данные обычно компонуются по другим правилам.
73.6. Database Page Layout
This section provides an overview of the page format used within Postgres Pro tables and indexes.[16] Sequences and TOAST tables are formatted just like a regular table.
In the following explanation, a byte is assumed to contain 8 bits. In addition, the term item refers to an individual data value that is stored on a page. In a table, an item is a row; in an index, an item is an index entry.
Every table and index is stored as an array of pages of a fixed size (usually 8 kB, although a different page size can be selected when compiling the server). In a table, all the pages are logically equivalent, so a particular item (row) can be stored in any page. In indexes, the first page is generally reserved as a metapage holding control information, and there can be different types of pages within the index, depending on the index access method.
Table 73.2 shows the overall layout of a page. There are five parts to each page.
Table 73.2. Overall Page Layout
| Item | Description |
|---|---|
| PageHeaderData | 24 bytes long. Contains general information about the page, including free space pointers. |
| ItemIdData | Array of item identifiers pointing to the actual items. Each entry is an (offset,length) pair. 4 bytes per item. |
| Free space | The unallocated space. New item identifiers are allocated from the start of this area, new items from the end. |
| Items | The actual items themselves. |
| Special space | Index access method specific data. Different methods store different data. Additionally, this space is used to store information about 64-bit transaction IDs. |
The first 20 bytes of each page consists of a page header (PageHeaderData). Its format is detailed in Table 73.3. The first field tracks the most recent WAL entry related to this page. The second field contains the page checksum if data checksums are enabled. Next is a 2-byte field containing flag bits. This is followed by three 2-byte integer fields (pd_lower, pd_upper, and pd_special). These contain byte offsets from the page start to the start of unallocated space, to the end of unallocated space, and to the start of the special space. The next 2 bytes of the page header, pd_pagesize_version, store both the page size and a version indicator. Beginning with PostgreSQL 8.3 the version number is 4; PostgreSQL 8.1 and 8.2 used version number 3; PostgreSQL 8.0 used version number 2; PostgreSQL 7.3 and 7.4 used version number 1; prior releases used version number 0. Postgres Pro Enterprise uses version number 0x00FF − 1. (The basic page layout and header format has not changed in most of these versions, but the layout of heap row headers has.) The page size is basically only present as a cross-check; there is no support for having more than one page size in an installation.
Table 73.3. PageHeaderData Layout
| Field | Type | Length | Description |
|---|---|---|---|
| pd_lsn | PageXLogRecPtr | 8 bytes | LSN: next byte after last byte of WAL record for last change to this page |
| pd_checksum | uint16 | 2 bytes | Page checksum |
| pd_flags | uint16 | 2 bytes | Flag bits |
| pd_lower | LocationIndex | 2 bytes | Offset to start of free space |
| pd_upper | LocationIndex | 2 bytes | Offset to end of free space |
| pd_special | LocationIndex | 2 bytes | Offset to start of special space |
| pd_pagesize_version | uint16 | 2 bytes | Page size and layout version number information |
Following the page header are item identifiers (ItemIdData), each requiring four bytes. An item identifier contains a byte-offset to the start of an item, its length in bytes, and a few attribute bits which affect its interpretation. New item identifiers are allocated as needed from the beginning of the unallocated space. The number of item identifiers present can be determined by looking at pd_lower, which is increased to allocate a new identifier. Because an item identifier is never moved until it is freed, its index can be used on a long-term basis to reference an item, even when the item itself is moved around on the page to compact free space. In fact, every pointer to an item (ItemPointer, also known as CTID) created by Postgres Pro consists of a page number and the index of an item identifier.
The items themselves are stored in space allocated backwards from the end of unallocated space. The exact structure varies depending on what the table is to contain. Tables and sequences both use a structure named HeapTupleHeaderData, described below.
The final section is the “special section” which can contain anything the access method wishes to store. For example, b-tree indexes store links to the page's left and right siblings, as well as some other data relevant to the index structure. Ordinary tables and sequences store the HeapPageSpecialData structure in this section. Its format is specified in Table 73.4.
Figure 73.1 illustrates how these parts are laid out in a page.
Figure 73.1. Page Layout
73.6.1. Table Row Layout
HeapPageSpecialData is a 24-byte structure. The first two 8-byte fields contain the base (i.e., an offset) for short (4-byte) transaction IDs in this page. The next field is a hint that shows whether pruning the page is likely to be profitable: it tracks the oldest un-pruned XMAX on the page. The last field is a magic number identifying the type of the page (0x1010 for ordinary tables and 0x1717 for sequences).
Table 73.4. HeapPageSpecialData Layout
| Field | Type | Length | Description |
|---|---|---|---|
| pd_xid_base | TransactionId | 8 bytes | Base for short 4-byte transaction IDs in this page |
| pd_multi_base | TransactionId | 8 bytes | Base for short 4-byte multixact IDs in this page |
| pd_prune_xid | ShortTransactionId | 4 bytes | Oldest unpruned XMAX on page, or zero if none |
| pd_magic | uint32 | 4 bytes | Magic number identifying the type of this page |
All table rows are structured in the same way. There is a fixed-size header (occupying 40 bytes on most machines), followed by an optional null bitmap, an optional object ID field, and the user data. The header is detailed in Table 73.5. The actual user data (columns of the row) begins at the offset indicated by t_hoff, which must always be a multiple of the MAXALIGN distance for the platform. The null bitmap is only present if the HEAP_HASNULL bit is set in t_infomask. If it is present it begins just after the fixed header and occupies enough bytes to have one bit per data column (that is, the number of bits that equals the attribute count in t_infomask2). In this list of bits, a 1 bit indicates not-null, a 0 bit is a null. When the bitmap is not present, all columns are assumed not-null. The object ID is only present if the HEAP_HASOID_OLD bit is set in t_infomask. If present, it appears just before the t_hoff boundary. Any padding needed to make t_hoff a MAXALIGN multiple will appear between the null bitmap and the object ID. (This in turn ensures that the object ID is suitably aligned.)
Table 73.5. HeapTupleHeaderData Layout
| Field | Type | Length | Description |
|---|---|---|---|
| t_xmin | ShortTransactionId | 4 bytes | insert XID stamp |
| t_xmax | ShortTransactionId | 4 bytes | delete XID stamp |
| t_cid | CommandId | 4 bytes | insert and/or delete CID stamp (overlays with t_xvac) |
| t_xvac | ShortTransactionId | 4 bytes | XID for VACUUM operation moving a row version |
| t_ctid | ItemPointerData | 6 bytes | current TID of this or newer row version |
| t_infomask2 | uint16 | 2 bytes | number of attributes, plus various flag bits |
| t_infomask | uint16 | 2 bytes | various flag bits |
| t_hoff | uint8 | 1 byte | offset to user data |
Interpreting the actual data can only be done with information obtained from other tables, mostly pg_attribute. The key values needed to identify field locations are attlen and attalign. There is no way to directly get a particular attribute, except when there are only fixed width fields and no null values. All this trickery is wrapped up in the functions heap_getattr, fastgetattr and heap_getsysattr.
To read the data you need to examine each attribute in turn. First check whether the field is NULL according to the null bitmap. If it is, go to the next. Then make sure you have the right alignment. If the field is a fixed width field, then all the bytes are simply placed. If it's a variable length field (attlen = -1) then it's a bit more complicated. All variable-length data types share the common header structure struct varlena, which includes the total length of the stored value and some flag bits. Depending on the flags, the data can be either inline or in a TOAST table; it might be compressed, too (see Section 73.2).
[16] Actually, use of this page format is not required for either table or index access methods. The heap table access method always uses this format. All the existing index methods also use the basic format, but the data kept on index metapages usually doesn't follow the item layout rules.