Простой пример использования pageinspect

18 июля 2022

Недавно мы разобрались, как PostgreSQL хранит данные на диске. Но как убедиться, что СУБД именно так и работает? Вдруг мы что-то упустили или недопоняли? Можно прочитать данные с диска с помощью утилиты hexdump и посмотреть, что там реально записано. Но это трудоемко, ведь все битики придется декодировать вручную. К счастью, с PostgreSQL идет расширение pageinspect, которое может декодировать битики за нас.

Pageinspect имеет много возможностей. Все они описаны в документации. Помимо прочего, расширение умеет декодировать индексы Hash, B-Tree, GiST, и другие. В рамках этого поста мы не будем уходить в такие дебри и рассмотрим только базовый функционал.

Включаем pageinspect:

CREATE EXTENSION pageinspect;

Для эксперимента воспользуемся той же таблицей, что и в прошлый раз:

CREATE TABLE phonebook(
  "id" SERIAL PRIMARY KEY NOT NULL,
  "name" NAME NOT NULL,
  "phone" INT NOT NULL);

INSERT INTO phonebook ("name", "phone")
VALUES ('Alice', 123), ('Bob', 456), ('Charlie', 789);

Заметьте, что таблица имеет первичный ключ. По нему автоматически будет создан B-Tree индекс.

Чтобы прочитать нулевую страницу таблицы phonebook и декодировать ее заголовок, выполним следующий запрос:

=# SELECT * FROM page_header(get_raw_page('phonebook', 0));

-[ RECORD 1 ]---------
lsn       | 0/12631C00
checksum  | 0
flags     | 0
lower     | 36
upper     | 7904
special   | 8192
pagesize  | 8192
version   | 4
prune_xid | 0

Здесь get_raw_page() считывает страницу в виде бинаря, а page_header() декодирует ее заголовок и возвращает, как record.

Также мы можем заглянуть в кортежи, их заголовки и содержимое ItemIdData[]:

=# SELECT lp, lp_off, lp_flags, lp_len, t_ctid, t_infomask2
   FROM heap_page_items(get_raw_page('phonebook', 0));

 lp | lp_off | lp_flags | lp_len | t_ctid | t_infomask2
----+--------+----------+--------+--------+-------------
  1 |   8096 |        1 |     96 | (0,1)  |           3
  2 |   8000 |        1 |     96 | (0,2)  |           3
  3 |   7904 |        1 |     96 | (0,3)  |           3

Здесь lp* соответствуют элементам массива ItemIdData[]. Столбец lp представляет собой индекс массива, а остальные — декодированные поля соответствующего элемента. Вспомним, что хранится в ItemIdData:

typedef struct ItemIdData
{
    unsigned  lp_off:15,  /* смещение кортежа */
              lp_flags:2, /* состояние элемента */
              lp_len:15;  /* размер кортежа */
} ItemIdData;

Pageinspect не отображает флаги в текстовом виде, но их можно подсмотреть в исходном коде:

#define LP_UNUSED       0
#define LP_NORMAL       1
#define LP_REDIRECT     2
#define LP_DEAD         3

Видим, что у нас здесь три нормальных (lp_flags = 1) кортежа. Наконец, t_ctid и t_infomask2 — это одноименные поля из структуры HeapTupleHeaderData. Напомню, что t_ctid представляет собой ItemPointer на более новую версию кортежа. Более новых версий пока нет, поэтому все кортежи ссылаются сами на себя. Поле t_infomask2 может хранить следующую информацию:

/* 11 bits for number of attributes */
#define HEAP_NATTS_MASK         0x07FF  

#define HEAP_KEYS_UPDATED       0x2000
#define HEAP_HOT_UPDATED        0x4000
#define HEAP_ONLY_TUPLE         0x8000

Пока что оно просто говорит, что все кортежи имеют по три атрибута. Никаких других флагов не проставлено.

Что будет, если обновить одну из строк? Например:

=# UPDATE phonebook SET name = 'Alex' WHERE name = 'Alice';
UPDATE 1

=# SELECT lp, lp_off, lp_flags, lp_len, t_ctid, to_hex(t_infomask2)
   FROM heap_page_items(get_raw_page('phonebook', 0));

 lp | lp_off | lp_flags | lp_len | t_ctid | to_hex
----+--------+----------+--------+--------+--------
  1 |   8096 |        1 |     96 | (0,4)  | 4003
  2 |   8000 |        1 |     96 | (0,2)  | 3
  3 |   7904 |        1 |     96 | (0,3)  | 3
  4 |   7808 |        1 |     96 | (0,4)  | 8003

Поле t_ctid первого кортежа теперь указывает на четвертый кортеж. Кроме того, у старого кортежа был проставлен флаг HEAP_HOT_UPDATED, а у нового — HEAP_ONLY_TUPLE. Другими словами, мы видим HOT-цепочку. Следовательно, индекс по первичному ключу не был перестроен.

Если теперь выполнить VACCUM:

=# VACUUM phonebook;
VACUUM

=# SELECT lp, lp_off, lp_flags, lp_len, t_ctid, to_hex(t_infomask2)
   FROM heap_page_items(get_raw_page('phonebook', 0));
 lp | lp_off | lp_flags | lp_len | t_ctid | to_hex
----+--------+----------+--------+--------+--------
  1 |      4 |        2 |      0 |        |
  2 |   8096 |        1 |     96 | (0,2)  | 3
  3 |   8000 |        1 |     96 | (0,3)  | 3
  4 |   7904 |        1 |     96 | (0,4)  | 8003

… то СУБД поймет, что старый кортеж больше никому не виден, а значит, может быть удален. При этом в поле lp_flags соответствующего ItemIdData проставляется LP_REDIRECT, а поле lp_off указывает на более новую версию кортежа. Таким образом, хоть кортеж и был удален, HOT-цепочка не рвется.

При желании можно придумать еще множество захватывающих экспериментов. Придумать и провести которые я предлагаю вам самостоятельно. Моей целью было лишь показать, что pageinspect прост и приятен в использовании. Он может быть использован для отладки кода, диагностики проблем на проде, а также служить средством изучения внутренностей СУБД. Звучит как что-то, чем полезно уметь пользоваться.

Метки: , .