Dan l-artiklu bħalissa huwa bir-Russu — it-traduzzjoni bl-Ingliż għaddejja.
Оператор || на типе jsonb в PostgreSQL делает поверхностное слияние двух документов: забирает все ключи слева, накладывает поверх все ключи справа, и там, где они пересекаются, побеждает правый. Это самый короткий способ накатить частичный патч прямо в базе — проставить заказу флаг «оплачено» и способ оплаты, дописать тег в массив, добавить вычисленное поле — не выгружая весь JSON-документ в приложение, не правя его там и не отправляя обратно. Дальше разберём три режима оператора (слияние объектов, конкатенацию массивов, патч в UPDATE) и одну ловушку, на которой чаще всего спотыкаются.
Заодно держите в голове правило ещё до первого запроса: || работает строго по верхнему уровню. Поэтому перед массовым UPDATE полезно прогнать SELECT, поставив рядом исходный документ, патч и результат: если в документе есть вложенные объекты, такая проверка сразу показывает, не затёрся ли целиком соседний узел вместо аккуратной правки одного поля.
Слияние объектов: правый ключ побеждает
Когда оба операнда — JSON-объекты, на выходе получается объединение их ключей. Если ключ есть в обоих документах, его значение берётся из правого операнда — за ним всегда последнее слово.
SELECT '{"verified": false, "city": "Lima"}'::jsonb
|| '{"verified": true}'::jsonb;
Несколько вещей, которые стоит держать в голове:
- Оператор ничего не мутирует на месте, а возвращает новый документ — как и любая операция над
jsonb.
- Порядок операндов решает: при пересечении ключей
a || b и b || a дадут разный результат.
- Оба операнда обязаны быть
jsonb. Если столбец объявлен как json, приведите его явно через ::jsonb.
- Ключ из правого документа со значением
null ключ не удаляет, а перезаписывает его в null — разница принципиальная, к ней мы ещё вернёмся, говоря про MySQL.
Дописывание элементов в массив
Тот же оператор ведёт себя совсем иначе, когда оба операнда — массивы: тогда || их просто конкатенирует. Это удобный способ дописать элемент в конец JSON-массива, не разбирая его на части.
SELECT '["sql", "json"]'::jsonb || '["postgres"]'::jsonb;
А если один операнд — массив, а другой скаляр или объект, последний оборачивается в одноэлементный массив и приклеивается в хвост:
SELECT '["a", "b"]'::jsonb || '"c"'::jsonb;
Типичная задача — гарантировать, что у профиля есть массив тегов, и завести его там, где его пока нет:
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;
Ключ 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, как только нужно зайти внутрь документа.
Оператор
||на типе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— для точечной правки вглубь дерева.Как с этим у других СУБД
||для JSON не знает вовсе — там это логическое ИЛИ. Слияние делают функциейJSON_MERGE_PATCH(a, b): она ведёт себя как рекурсивный патч и вдобавок выкидывает ключи, у которых значениеnull. То есть ровно наоборот по сравнению с PostgreSQL, гдеnullостаётся жить в документе. Есть иJSON_MERGE_PRESERVE— та не заменяет совпадающие значения, а складывает их в массивы.Итог короткий. В PostgreSQL
||— это быстрый поверхностный merge, где правые ключи побеждают. Держите в уме его неглубокость, не путайте поведениеnullс MySQL и переключайтесь наjsonb_set, как только нужно зайти внутрь документа.