В прошлом уроке мы научились читать JSONB. Сейчас — искать, спрашивать «есть ли», и менять. Это три отдельных набора операторов, и каждый закрывает свой круг задач.
Containment: @> и <@
@> и <@ — самые мощные операторы JSONB. Они проверяют, что один JSON-объект содержится в другом.
Точное правило: A @> B истинно, если каждый ключ-значение из B присутствует в A. Вложенные объекты сравниваются рекурсивно; в массивах элементы могут идти в любом порядке.
«Содержит» — это про подмножество ключей/значений на каждом уровне. Лишние ключи в A разрешены.
Это идеальный инструмент для «фильтра по нескольким полям JSONB» одной строкой. И — внимание — именно @> понимает GIN-индекс. Об индексации мы поговорим в уроке 7, но запомни: WHERE attrs @> '...'::jsonb — это шаблон, под который ставят GIN.
Найди все чёрные продукты — одним containment-фильтром:
Чёрные ноутбуки с минимум 2 USB-C портами — комбинируем условия в одном JSONB-литерале:
Заметь, как это компактно. Альтернатива через attrs->> — три отдельных предиката с явным cast’ом каждого. Здесь — один.
Проверка существования ключей: ?, ?| и ?&
Три родственных оператора:
obj ? 'key'— содержит ли объект ключ'key'.obj ?| ARRAY['a', 'b']— содержит ли хотя бы один ключ из массива.obj ?& ARRAY['a', 'b']— содержит ли все ключи из массива.
Это «контракт» о существовании, не о значении. Удобно для фильтров вроде «у каких клиентов вообще проставлено поле b2b».
Клиенты, у которых в profile есть хотя бы один из ключей prefers или b2b:
Клиенты, у которых проставлены оба ключа prefers И newsletter:
SQL/JSON Path: jsonb_path_query и jsonb_path_exists
Начиная с Postgres 12 появился стандарт SQL/JSON Path — мини-язык для запросов внутри JSONB, похожий на JSONPath из мира JavaScript. Это решает задачи, которые через простые операторы решаются плохо: «вытащи все значения по шаблону», «есть ли элемент массива, удовлетворяющий условию».
Базовый синтаксис: $.key.subkey, $[*] для всех элементов массива, ? (filter) для условий.
Достать значение по пути — самое простое использование:
В этом простом случае jsonb_path_query ничего нового не даёт по сравнению с attrs->'color'. Сила появляется при фильтрах:
jsonb_path_exists — проверка «существует ли в JSONB значение, удовлетворяющее условию»:
Здесь @ — это «текущее значение» (то, на что показывает путь), а ?(...) — фильтр. Эта запись эквивалентна (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:
Третий аргумент — это JSONB-литерал, поэтому строки в нём с кавычками ('"push"', не 'push'). Это частая ловушка.
Четвёртый аргумент — create_missing. Если true (по умолчанию), ключ создастся, если его нет. Если false, отсутствующий ключ оставит JSONB без изменений.
Конкатенация: оператор ||
|| объединяет два JSONB-объекта. При совпадающих ключах побеждает правый операнд. Это удобный shortcut для «обновить несколько полей сразу»:
Добавим клиенту 2 новое поле и одновременно изменим существующее:
Обрати внимание: prefers был 'sms', стал 'email' — правый победил. А loyalty_level появился впервые.
Для массивов внутри JSONB || тоже работает, но как конкатенация массивов, не как «слияние объектов». Это разные семантики, что иногда вводит в заблуждение.
Удаление: оператор -
- (минус) удаляет ключ из объекта или элемент из массива (по индексу):
Уберём ключ b2b из профиля клиента 3:
Для удаления по пути есть #- с массивом ключей: profile #- '{ports, hdmi}'. Работает как jsonb_set, но в обратную сторону.
jsonb_array_elements: развернуть массив внутри JSONB
Когда JSONB содержит массив объектов (типичный случай — events, items, attachments), его часто нужно «развернуть» в строки и работать с каждым элементом. Для этого есть jsonb_array_elements:
Сделаем JSONB с массивом и развернём его:
jsonb_array_elements возвращает каждый элемент как JSONB (если нужен текст — jsonb_array_elements_text). Дальше — обычные операторы доступа. Это то же самое, что unnest для TEXT[], но для JSONB-массивов.
Сценарий — обработка событий, при которой структура события вариативна, а нужно посчитать что-то агрегатное:
Посчитаем общий объём заказа из JSONB-payload'а:
В реальных схемах эта же идиома применяется к webhook-payload’ам, бизнес-событиям, импортированным таблицам неизвестной структуры. JSONB-array + jsonb_array_elements — это как иметь временную таблицу без её создания.
Что нельзя делать дёшево
В JSONB нет «изменения на месте». Любая правка — это перезапись всего значения и (часто) MVCC-копия всей строки. На большом JSONB с частыми UPDATE это становится
Поэтому правило: JSONB лучше всего, когда обновлений мало. Если поле меняется часто (например, счётчик), оно должно быть отдельной колонкой.
Чек-лист
@>— 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 подходит для «много читают, редко меняют».