Learning Platform
Урок 11.03 · 19 мин
Средний
JSONBcontainmentjsonb_path_queryjsonb_setSQL/JSON Path

В прошлом уроке мы научились читать JSONB. Сейчас — искать, спрашивать «есть ли», и менять. Это три отдельных набора операторов, и каждый закрывает свой круг задач.

Containment: @> и <@

@> и <@ — самые мощные операторы JSONB. Они проверяют, что один JSON-объект содержится в другом.

Точное правило: A @> B истинно, если каждый ключ-значение из B присутствует в A. Вложенные объекты сравниваются рекурсивно; в массивах элементы могут идти в любом порядке.

Containment: A @> B

«Содержит» — это про подмножество ключей/значений на каждом уровне. Лишние ключи в A разрешены.

A{"color":"black","ports":{"usb_c":2,"hdmi":1}}
B{"color":"black"}
A @> BTRUE
A{"color":"black","ports":{"usb_c":2,"hdmi":1}}
B{"ports":{"usb_c":2}}Вложенное сравнение тоже работает по @>
A @> BTRUE

Это идеальный инструмент для «фильтра по нескольким полям JSONB» одной строкой. И — внимание — именно @> понимает GIN-индекс. Об индексации мы поговорим в уроке 7, но запомни: WHERE attrs @> '...'::jsonb — это шаблон, под который ставят GIN.

Найди все чёрные продукты — одним containment-фильтром:

PostgreSQL

Чёрные ноутбуки с минимум 2 USB-C портами — комбинируем условия в одном JSONB-литерале:

PostgreSQL

Заметь, как это компактно. Альтернатива через attrs->> — три отдельных предиката с явным cast’ом каждого. Здесь — один.

Проверка существования ключей: ?, ?| и ?&

Три родственных оператора:

  • obj ? 'key' — содержит ли объект ключ 'key'.
  • obj ?| ARRAY['a', 'b'] — содержит ли хотя бы один ключ из массива.
  • obj ?& ARRAY['a', 'b'] — содержит ли все ключи из массива.

Это «контракт» о существовании, не о значении. Удобно для фильтров вроде «у каких клиентов вообще проставлено поле b2b».

Клиенты, у которых в profile есть хотя бы один из ключей prefers или b2b:

PostgreSQL

Клиенты, у которых проставлены оба ключа prefers И newsletter:

PostgreSQL

SQL/JSON Path: jsonb_path_query и jsonb_path_exists

Начиная с Postgres 12 появился стандарт SQL/JSON Path — мини-язык для запросов внутри JSONB, похожий на JSONPath из мира JavaScript. Это решает задачи, которые через простые операторы решаются плохо: «вытащи все значения по шаблону», «есть ли элемент массива, удовлетворяющий условию».

Базовый синтаксис: $.key.subkey, $[*] для всех элементов массива, ? (filter) для условий.

Достать значение по пути — самое простое использование:

PostgreSQL

В этом простом случае jsonb_path_query ничего нового не даёт по сравнению с attrs->'color'. Сила появляется при фильтрах:

jsonb_path_exists — проверка «существует ли в JSONB значение, удовлетворяющее условию»:

PostgreSQL

Здесь @ — это «текущее значение» (то, на что показывает путь), а ?(...) — фильтр. Эта запись эквивалентна (attrs#>'{ports,usb_c}')::int >= 2, но компактнее и работает даже когда ключа может не быть (без NULL-проверок).

jsonb_path_query возвращает строки (потенциально несколько на одну запись — если путь матчит несколько мест). jsonb_path_exists возвращает только boolean — есть/нет совпадения. Для WHERE обычно нужен второй.

Изменение JSONB: jsonb_set

JSONB можно менять — но только пересоздавая значение целиком. На уровне SQL это выглядит как функция, которая возвращает новый JSONB, который затем кладётся в UPDATE.

jsonb_set(target, path, new_value, create_missing := true):

Изменим один ключ внутри profile у клиента 1:

PostgreSQL

Третий аргумент — это JSONB-литерал, поэтому строки в нём с кавычками ('"push"', не 'push'). Это частая ловушка.

Четвёртый аргумент — create_missing. Если true (по умолчанию), ключ создастся, если его нет. Если false, отсутствующий ключ оставит JSONB без изменений.

Конкатенация: оператор ||

|| объединяет два JSONB-объекта. При совпадающих ключах побеждает правый операнд. Это удобный shortcut для «обновить несколько полей сразу»:

Добавим клиенту 2 новое поле и одновременно изменим существующее:

PostgreSQL

Обрати внимание: prefers был 'sms', стал 'email' — правый победил. А loyalty_level появился впервые.

Для массивов внутри JSONB || тоже работает, но как конкатенация массивов, не как «слияние объектов». Это разные семантики, что иногда вводит в заблуждение.

Удаление: оператор -

- (минус) удаляет ключ из объекта или элемент из массива (по индексу):

Уберём ключ b2b из профиля клиента 3:

PostgreSQL

Для удаления по пути есть #- с массивом ключей: profile #- '{ports, hdmi}'. Работает как jsonb_set, но в обратную сторону.

jsonb_array_elements: развернуть массив внутри JSONB

Когда JSONB содержит массив объектов (типичный случай — events, items, attachments), его часто нужно «развернуть» в строки и работать с каждым элементом. Для этого есть jsonb_array_elements:

Сделаем JSONB с массивом и развернём его:

PostgreSQL

jsonb_array_elements возвращает каждый элемент как JSONB (если нужен текст — jsonb_array_elements_text). Дальше — обычные операторы доступа. Это то же самое, что unnest для TEXT[], но для JSONB-массивов.

Сценарий — обработка событий, при которой структура события вариативна, а нужно посчитать что-то агрегатное:

Посчитаем общий объём заказа из JSONB-payload'а:

PostgreSQL

В реальных схемах эта же идиома применяется к webhook-payload’ам, бизнес-событиям, импортированным таблицам неизвестной структуры. JSONB-array + jsonb_array_elements — это как иметь временную таблицу без её создания.

Что нельзя делать дёшево

В JSONB нет «изменения на месте». Любая правка — это перезапись всего значения и (часто) MVCC-копия всей строки. На большом JSONB с частыми UPDATE это становится

источником write amplification
.

Поэтому правило: JSONB лучше всего, когда обновлений мало. Если поле меняется часто (например, счётчик), оно должно быть отдельной колонкой.

Проверка знанийKnowledge check
Нужно найти все продукты, у которых среди портов есть хотя бы один HDMI с количеством >= 1. attrs выглядит как {"ports":{"hdmi":1,"usb_c":2}}. Какой инструмент подойдёт лучше всего и почему?
ОтветAnswer
Лучше всего подойдёт jsonb_path_exists(attrs, '$.ports.hdmi ? (@ >= 1)'). Преимущества: (1) не требует CASE/COALESCE на отсутствие ключа — если ключа нет, выражение даст false; (2) можно сразу выразить условие «значение >= 1» внутри JSON Path без cast'а; (3) индексируется через GIN на jsonb_path_ops при правильной настройке. Альтернатива WHERE (attrs#>>'{ports,hdmi}')::int >= 1 тоже работает, но падает с ошибкой, если значение оказалось не числом, и требует явного cast. Альтернатива attrs @> '{"ports":{"hdmi":1}}' проверит только точное равенство 1, не >= 1.
JSON type (GA 25.3), Variant и Dynamic в ClickHouse

Чек-лист

  • @> — containment, основной инструмент фильтрации по JSONB. Работает с GIN-индексом.
  • ?, ?|, ?& — проверка существования ключа / любого / всех.
  • jsonb_path_query (возвращает значения) и jsonb_path_exists (возвращает boolean) реализуют SQL/JSON Path. Внутри пути @ — текущее значение, ? (...) — фильтр.
  • jsonb_set(target, path, value, create_missing) — заменить один ключ. Значение — JSONB-литерал.
  • || — merge двух JSONB. Правый побеждает при конфликте ключей.
  • - — удалить ключ; #- — удалить по пути.
  • В JSONB нет дешёвого «изменения на месте» — каждое UPDATE = перезапись всего значения. JSONB подходит для «много читают, редко меняют».

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Какое из этих утверждений про оператор @> верно?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7