PostgreSQL CookbookJSONB, массивы и поискДоступ к jsonb и containment
0 / 63 (0%)

Доступ к jsonb и containment

Маркетинг Brew запустил кастомизацию напитка: к заказу можно прицепить «опции» — размер, вид молока, число шотов, список добавок. Беда в том, что набор опций у каждого свой: у кого-то есть extras, у кого-то нет даже ключа milk. Это ровно тот случай, ради которого в схеме появляется jsonb: бесформенные, разреженные данные, которые не хочется раскладывать по сорока nullable-колонкам. Положить-то их легко — а вот доставать и фильтровать надо уметь.

Цель юнита — освоить четыре рабочих оператора доступа к jsonb: -> и ->> (достать значение), #>> (достать по пути) и @> (containment, «содержит»), плюс ? (есть ли ключ). Это фундамент любой работы с jsonb в приложении; глубокий jsonb_path, сборка документов и индексы — в следующих юнитах модуля.

-> против ->>: jsonb или text

Два оператора-близнеца, и путаница между ними — классическая ошибка. -> достаёт значение как jsonb: options -> 'milk' вернёт "oat" — со скобками-кавычками, потому что это всё ещё json-строка. ->> достаёт то же значение как text: options ->> 'milk' вернёт чистое oat. Когда сравниваешь, конкатенируешь или печатаешь — почти всегда нужен ->>. -> нужен, когда лезешь глубже: options -> 'meta' -> 'flags' — цепочка по вложенному объекту, на каждом шаге снова jsonb. Соответственно #> и #>> — те же «достать», но сразу по пути: options #>> '{extras,0}' спускается в массив extras и берёт нулевой элемент как text.

Containment @>: «документ содержит пару»

@> — главный оператор поиска по jsonb. options @> '{"milk":"oat"}' истинно, если в документе слева есть пара "milk":"oat" — независимо от того, что там ещё лежит. Это не равенство всего документа (его почти никогда не проверяют), а «содержит ли». Containment умеет заглядывать и внутрь массивов: options @> '{"extras":["honey"]}' найдёт заказы, где массив extras содержит honey. Один оператор покрывает и плоские поля, и вложенные структуры — и именно его ускоряет GIN-индекс (см. 06-05): на большой таблице @> без индекса — это Seq Scan, а с GIN — Bitmap Index Scan.

?: есть ли ключ

? отвечает на другой вопрос — не «какое значение», а «есть ли вообще такой ключ верхнего уровня». options ? 'extras' истинно для заказов, где extras указан, — даже если массив пуст ([]). Это важная разница с @>: пустой extras ключ имеет, но никакого значения внутри нет, поэтому @> '{"extras":[...]}' его не поймает, а ? 'extras' — поймает. Рядом живут ?| (есть хоть один из ключей) и ?& (есть все ключи) — та же логика для списка.

Что показывает наш код

Лабораторная таблица order_options_labschema.sql) с пятью заказами, у которых options намеренно разношёрстны. Четыре запроса:

sql
SELECT customer,
       coalesce((options -> 'milk')::text, '') AS milk_jsonb,   -- "oat" (jsonb)
       coalesce(options ->> 'milk', '')         AS milk_text,    -- oat   (text)
       coalesce(options #>> '{extras,0}', '')   AS first_extra   -- по пути
FROM order_options_lab ORDER BY id;                               -- AccessOps
 
SELECT customer FROM order_options_lab WHERE options @> '{"milk":"oat"}';        -- OatMilkOrders
SELECT customer FROM order_options_lab WHERE options @> '{"extras":["honey"]}';  -- HoneyInExtras
SELECT customer FROM order_options_lab WHERE options ? 'extras';                 -- HasExtrasKey

coalesce подменяет отсутствующее значение на (у Егора нет milk, у Бориса — extras) и заодно даёт sqlc тип string вместо nullable interface{}. Как и юниты 01-04/01-05, этот добавляет свой объект в схему, поэтому make db-reset накатывает его через brew.Apply (схема Brew → DDL+seed юнита → seed Brew).

Запуск

sh
docker compose up -d
make lecture L=07-jsonb-arrays-and-search/07-01-jsonb-access-and-containment T=db-reset
make lecture L=07-jsonb-arrays-and-search/07-01-jsonb-access-and-containment

Вывод:

plaintext
1) Доступ к полям options (-> даёт jsonb с кавычками, ->> — text, #>> — по пути):
КЛИЕНТ  -> 'milk'  ->> 'milk'  size  shots  #>> '{extras,0}'
Алиса   "oat"      oat         L     2      cinnamon
Борис   "cow"      cow         M     1      ∅
Карина  "oat"      oat         S     1      honey
Дина    "soy"      soy         L     3      ∅
Егор    ∅          ∅           M     2      ∅
 
2) options @> '{"milk":"oat"}' — заказы на овсяном молоке:
   Алиса (size L)
   Карина (size S)
 
3) options @> '{"extras":["honey"]}' — в массиве extras есть honey:
   Карина
 
4) options ? 'extras' — указан ключ extras (пустой массив тоже считается):
   Алиса
   Карина
   Дина

Первая таблица показывает главный контраст: -> отдал "oat" в кавычках (это jsonb), ->> — чистое oat (это text), а #>> достал нулевой элемент массива. У Егора нет ключа milk → оба оператора вернули NULL (мы подменили на ). Containment @> нашёл по плоской паре (овсяное молоко) и по элементу массива (honey), а ? поймал Дину с пустым extras, которого @> бы не увидел.

Заборчик

jsonb соблазняет сложить в одну колонку всё подряд — и тут же мстит. Граница простая:

  • Поле, по которому регулярно фильтруешь, считаешь или джойнишь, — это колонка, а не ключ внутри jsonb: её проверит CHECK и NOT NULL, на ней живёт обычный B-tree, план запроса предсказуем.
  • Containment-поиск без GIN — это Seq Scan по всей таблице (см. 06-05); индекс под @> обязателен на любой заметной таблице.
  • ?-фильтр по отсутствующему ключу легко даёт сюрпризы трёхзначной логики — её полностью разбирает 03-06.
  • Правило: jsonb — для по-настоящему бесформенного и разреженного; всё, на что опирается логика приложения, выноси в колонки.

В проде размер документа тоже не бесплатен — об этом ровно следующий юнит.

Что забрать с собой

  • -> достаёт значение как jsonb ("oat" в кавычках), ->> — как text (oat); #>/#>> делают то же по пути ('{extras,0}').
  • @> (containment) — «документ содержит пару ключ-значение», работает и для плоских полей, и внутри массивов; именно его ускоряет GIN (06-05).
  • ? — «есть ли ключ верхнего уровня» (ловит даже пустой массив); ?|/?& — для списка ключей.
  • Отсутствующий ключ — это NULL: оборачивай coalesce, чтобы получить определённый тип и поведение.

Дальше — 07-02 «Когда НЕ нужен jsonb»: у гибкости есть физическая цена (write-amplification) и семантическая (нет per-field ограничений) — посмотрим на оба счёта и решим, где jsonb уместен, а где это отложенная боль.

·Модуль 08

Этот урок ещё впереди

Курс изучается по порядку — чтобы открыть этот шаг, сначала завершите предыдущие. Так контекст накапливается без пропусков.

/ вы пытались открыть
JSONB, массивы и поиск / Доступ к jsonb и containment