sqlpostgresqljsonjsonb

The JSONB || Operator in PostgreSQL: Merging Documents and Patches

How the || operator does a shallow JSONB merge where right-hand keys win, appends to a JSON array, and applies a partial patch in one expression.

3 min læsningReferencesql · postgresql · json · jsonb · merge
Denne artikel er i øjeblikket på russisk — den engelske oversættelse er undervejs.

Оператор || на типе jsonb в PostgreSQL делает поверхностное слияние двух документов: забирает все ключи слева, накладывает поверх все ключи справа, и там, где они пересекаются, побеждает правый. Это самый короткий способ накатить частичный патч прямо в базе — проставить заказу флаг «оплачено» и способ оплаты, дописать тег в массив, добавить вычисленное поле — не выгружая весь JSON-документ в приложение, не правя его там и не отправляя обратно. Дальше разберём три режима оператора (слияние объектов, конкатенацию массивов, патч в UPDATE) и одну ловушку, на которой чаще всего спотыкаются.

Заодно держите в голове правило ещё до первого запроса: || работает строго по верхнему уровню. Поэтому перед массовым UPDATE полезно прогнать SELECT, поставив рядом исходный документ, патч и результат: если в документе есть вложенные объекты, такая проверка сразу показывает, не затёрся ли целиком соседний узел вместо аккуратной правки одного поля.

Слияние объектов: правый ключ побеждает

Когда оба операнда — JSON-объекты, на выходе получается объединение их ключей. Если ключ есть в обоих документах, его значение берётся из правого операнда — за ним всегда последнее слово.

SELECT '{"verified": false, "city": "Lima"}'::jsonb
       || '{"verified": true}'::jsonb;
-- {"city": "Lima", "verified": true}

Несколько вещей, которые стоит держать в голове:

  • Оператор ничего не мутирует на месте, а возвращает новый документ — как и любая операция над jsonb.
  • Порядок операндов решает: при пересечении ключей a || b и b || a дадут разный результат.
  • Оба операнда обязаны быть jsonb. Если столбец объявлен как json, приведите его явно через ::jsonb.
  • Ключ из правого документа со значением null ключ не удаляет, а перезаписывает его в null — разница принципиальная, к ней мы ещё вернёмся, говоря про MySQL.

Дописывание элементов в массив

Тот же оператор ведёт себя совсем иначе, когда оба операнда — массивы: тогда || их просто конкатенирует. Это удобный способ дописать элемент в конец JSON-массива, не разбирая его на части.

SELECT '["sql", "json"]'::jsonb || '["postgres"]'::jsonb;
-- ["sql", "json", "postgres"]

А если один операнд — массив, а другой скаляр или объект, последний оборачивается в одноэлементный массив и приклеивается в хвост:

SELECT '["a", "b"]'::jsonb || '"c"'::jsonb;
-- ["a", "b", "c"]

Типичная задача — гарантировать, что у профиля есть массив тегов, и завести его там, где его пока нет:

UPDATE users
SET profile = profile || '{"tags": []}'::jsonb
WHERE profile -> 'tags' IS NULL;

Частичный патч в UPDATE

Вот ради чего всё и затевалось: обновить сразу несколько полей одним выражением, не перетряхивая документ целиком. Пусть в orders лежит JSONB-столбец meta; помечаем заказ оплаченным и заодно фиксируем способ оплаты:

UPDATE orders
SET meta = meta || '{"paid": true, "method": "card"}'::jsonb
WHERE id = 1001;

Патч не обязательно писать строкой руками — его можно собрать на лету из обычных колонок через jsonb_build_object. Так не приходится склеивать JSON конкатенацией строк и ловить ошибки экранирования:

UPDATE users
SET profile = profile || jsonb_build_object(
        'country', country,
        'email', email
    )
WHERE id = 42;

А ещё приятно собирать «обогащённую» копию документа прямо в SELECT, вообще не трогая таблицу — удобно для витрин и ответов API:

SELECT id,
       profile || jsonb_build_object('active', status = 'active') AS enriched
FROM users;

Грабли: вложенные объекты заменяются целиком

А теперь обещанная свинья. Самая частая ошибка — ждать от || глубокого, рекурсивного слияния. Он же работает строго по верхнему уровню. Если ключ справа оказался объектом, он целиком вытесняет объект слева, а не сливается с ним поле за полем.

SELECT '{"address": {"city": "Lima", "zip": "15001"}}'::jsonb
       || '{"address": {"city": "Quito"}}'::jsonb;
-- {"address": {"city": "Quito"}}

Ключ zip испарился: правый address затёр левый как единое значение, со всем содержимым. Хотели поправить один город — потеряли почтовый индекс. Чтобы аккуратно изменить ровно одно вложенное поле, берите jsonb_set и указывайте путь к нему:

UPDATE users
SET profile = jsonb_set(profile, '{address,city}', '"Quito"')
WHERE id = 42;

Правило запоминается на раз: || — для плоских патчей и конкатенации, jsonb_set — для точечной правки вглубь дерева.

Как с этим у других СУБД

  • MySQL оператора || для JSON не знает вовсе — там это логическое ИЛИ. Слияние делают функцией JSON_MERGE_PATCH(a, b): она ведёт себя как рекурсивный патч и вдобавок выкидывает ключи, у которых значение null. То есть ровно наоборот по сравнению с PostgreSQL, где null остаётся жить в документе. Есть и JSON_MERGE_PRESERVE — та не заменяет совпадающие значения, а складывает их в массивы.
  • ClickHouse под частичную правку JSON не заточен: его JSON-функции рассчитаны в основном на чтение, и изменение документа обычно сводится к перезаписи всего значения.

Итог короткий. В PostgreSQL || — это быстрый поверхностный merge, где правые ключи побеждают. Держите в уме его неглубокость, не путайте поведение null с MySQL и переключайтесь на jsonb_set, как только нужно зайти внутрь документа.

Øv dig på rigtige opgaver

Løs opgaver i SQL-træneren med øjeblikkelig bedømmelse og hints.

Åbn træneren