Раздел 33 · PostgreSQL с нуля

JSONB: хранение и доступ

junior~30 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Прошлый урок закрыл кластер про индексы. Начинается новый: типы, которых нет в большинстве других СУБД. Первым возвращаемся к JSONB. В уроке про JSONB мы положили книгам магазина записку details и научились доставать из неё значения стрелками. Теперь посмотрим, как записка лежит на диске, как править её по кусочку и как разворачивать её в обычные строки.

Примеры работают на учебной базе магазина. Записку получили пять книг:

ALTER TABLE books ADD COLUMN IF NOT EXISTS details jsonb;

UPDATE books SET details = '{"publisher": "АСТ", "tags": ["классика", "мистика"], "cover": {"type": "твёрдая", "color": "чёрная"}}' WHERE id = 1;
UPDATE books SET details = '{"publisher": "АСТ", "tags": ["фантастика", "классика"], "cover": {"type": "мягкая"}}' WHERE id = 3;
UPDATE books SET details = '{"publisher": "АСТ", "tags": ["фантастика"], "cover": {"type": "мягкая"}}' WHERE id = 4;
UPDATE books SET details = '{"publisher": "Питер", "translator": "Е. Матвеев", "tags": ["алгоритмы", "python"], "cover": {"type": "мягкая"}}' WHERE id = 7;
UPDATE books SET details = '{"publisher": "Русская редакция", "translator": "В. Вдовиченко", "tags": ["программирование", "классика"], "cover": {"type": "твёрдая"}}' WHERE id = 8;

Письмо и картотека

Друг прислал тебе письмо с рецептом. Чтобы найти, сколько класть соли, ты перечитываешь письмо с начала, каждый раз. Это тип json: PostgreSQL хранит текст как пришёл и разбирает его заново при каждом обращении.

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

Отсюда все особенности jsonb, которые видны снаружи.

Ключи выстраиваются в свой порядок. Не в том, в каком ты их написал, и даже не по алфавиту:

SELECT '{"publisher": "АСТ", "tags": ["классика"], "cover": {"type": "твёрдая"}, "id": 1}'::jsonb AS doc;
                                        doc
-----------------------------------------------------------------------------------
 {"id": 1, "tags": ["классика"], "cover": {"type": "твёрдая"}, "publisher": "АСТ"}
(1 row)

Сначала короткие ключи, потом длинные: так серверу быстрее искать ключ в оглавлении. Повторный ключ схлопывается до последнего значения, это мы видели в 21-м уроке. Вывод: порядок ключей в jsonb не значит ничего, и код, который на него полагается, сломается.

Пробелы пропадают, а числа остаются точными. Числа в jsonb хранятся как numeric из урока про числа, поэтому нули после запятой сохраняются, а экспонента превращается в обычное число:

SELECT '{"price":   690.50, "rating": 4.70, "big": 1e3}'::jsonb AS doc;
                      doc
------------------------------------------------
 {"big": 1000, "price": 690.50, "rating": 4.70}
(1 row)

Двоичный не значит маленький. Функция pg_column_size показывает, сколько байт занимает значение:

SELECT pg_column_size('{"publisher": "АСТ", "tags": ["классика", "мистика"]}'::json)  AS json_bytes,
       pg_column_size('{"publisher": "АСТ", "tags": ["классика", "мистика"]}'::jsonb) AS jsonb_bytes;
 json_bytes | jsonb_bytes
------------+-------------
         75 |          88
(1 row)

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

Большой документ и TOAST

Строка таблицы живёт на странице в 8 КБ, про страницы рассказано в уроке теории. Документ на десятки килобайт туда не влезет. Для таких значений у PostgreSQL есть TOAST: длинное значение сжимается, а если и после этого не помещается, уезжает в отдельную служебную таблицу. В строке остаётся ссылка.

CREATE TABLE notes (id integer PRIMARY KEY, doc jsonb);
INSERT INTO notes VALUES (1, jsonb_build_object('text', repeat('Рукописи не горят. ', 2000)));

SELECT octet_length(doc::text) AS as_text, pg_column_size(doc) AS on_disk FROM notes;
 as_text | on_disk
---------+---------
   68012 |     843
(1 row)

68 КБ текста легли в 843 байта, потому что одна фраза повторяется две тысячи раз и отлично сжимается. Обычный документ сожмётся скромнее, но правило то же. И у этого правила есть цена: документ целиком читается, распаковывается и записывается заново, даже если ты поменял в нём один ключ.

Проверим на «Мастере и Маргарите». Адрес строки ctid из урока про индексы и VACUUM меняется после правки одного ключа:

SELECT ctid FROM books WHERE id = 1;
UPDATE books SET details = jsonb_set(details, '{cover,color}', '"красная"') WHERE id = 1;
SELECT ctid, details -> 'cover' AS cover FROM books WHERE id = 1;
  ctid
--------
 (0,10)
(1 row)

  ctid  |                  cover
--------+-----------------------------------------
 (0,15) | {"type": "твёрдая", "color": "красная"}
(1 row)

Новая версия всей строки, вместе со всей запиской. Отсюда совет: держи документы небольшими и не складывай в одну записку то, что часто меняется по отдельности, например счётчик просмотров.

урок из курса

Урок из курса «PostgreSQL с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.

Оплата пока не подключена: премиум и курсы открываются промокодом.