Tento článek je momentálně v ruštině — anglický překlad se připravuje.
В PostgreSQL тип jsonb часто используют для гибких структурированных данных.
Например, в таблице users может быть колонка payload:
{
"plan": "pro",
"active": true,
"tags": ["beta", "eu"],
"settings": {
"theme": "dark",
"language": "ru"
}
}
Иногда нужно не просто достать одно поле через ->>.
Иногда нужно спросить у JSON-документа:
Содержишь ли ты вот такой фрагмент?
Например:
{"plan": "pro"}
или:
{"settings": {"theme": "dark"}}
Для такой проверки в PostgreSQL есть оператор:
@>
Он называется containment operator.
По-русски его удобно понимать как:
содержит
Запрос:
payload @> '{"plan":"pro"}'
означает:
payload содержит JSON-фрагмент {"plan":"pro"}?
Если да — условие вернёт true.
Что делает оператор @>
Оператор @> проверяет, содержит ли левый jsonb правый jsonb-фрагмент.
Пример:
SELECT
id,
email
FROM users
WHERE payload @> '{"plan":"pro"}';
Этот запрос найдёт пользователей, у которых в payload есть:
{
"plan": "pro"
}
При этом в самом payload могут быть и другие поля.
Например, такой документ подходит:
{
"plan": "pro",
"active": true,
"tags": ["beta", "eu"],
"settings": {
"theme": "dark"
}
}
Почему подходит?
Потому что он содержит нужный фрагмент:
{"plan": "pro"}
Оператор @> не требует, чтобы документы были равны целиком. Он проверяет именно наличие фрагмента.
@> — это не равенство
Важно не путать @> с обычным равенством.
Равенство:
payload = '{"plan":"pro"}'::jsonb
означает:
payload полностью равен этому JSON?
А containment:
payload @> '{"plan":"pro"}'
означает:
payload содержит этот JSON-фрагмент?
Пример:
{
"plan": "pro",
"active": true
}
Для такого документа:
payload = '{"plan":"pro"}'::jsonb
вернёт false, потому что в документе есть ещё active.
А:
payload @> '{"plan":"pro"}'
вернёт true, потому что нужный фрагмент внутри есть.
Можно запомнить так:
= проверяет полное равенство
@> проверяет содержание фрагмента
Базовый пример
Допустим, есть таблица users:
id | email | payload
---+---------------+------------------------------------------------
1 | anna@mail.com | {"plan":"pro","active":true}
2 | bob@mail.com | {"plan":"free","active":true}
3 | max@mail.com | {"plan":"pro","active":false}
Найдём всех пользователей с тарифом pro:
SELECT
id,
email
FROM users
WHERE payload @> '{"plan":"pro"}';
Результат:
id | email
---+---------------
1 | anna@mail.com
3 | max@mail.com
Пользователь 2 не попал, потому что у него:
{"plan": "free"}
Несколько условий внутри одного JSON-фрагмента
В правом фрагменте можно указать несколько полей.
Например, найти пользователей, у которых:
plan = "pro";
active = true.
SELECT
id,
email
FROM users
WHERE payload @> '{"plan":"pro","active":true}';
Такой фрагмент:
{"plan":"pro","active":true}
означает:
в payload должны быть оба поля с такими значениями
Подойдёт документ:
{
"plan": "pro",
"active": true,
"tags": ["beta"]
}
Не подойдёт:
{
"plan": "pro",
"active": false
}
И не подойдёт:
{
"plan": "free",
"active": true
}
То есть поля внутри правого JSON-фрагмента работают как несколько условий одновременно.
Типы должны совпадать строго
jsonb различает типы.
Например, это boolean:
{"active": true}
А это строка:
{"active": "true"}
Для PostgreSQL это разные значения.
Запрос:
SELECT id
FROM users
WHERE payload @> '{"active":true}';
найдёт документы, где active — именно JSON boolean true.
Он не найдёт документы, где active хранится строкой:
{"active": "true"}
То же самое с числами.
Это число:
{"priority": 5}
А это строка:
{"priority": "5"}
Запрос:
WHERE payload @> '{"priority":5}'
не совпадает с:
{"priority": "5"}
Главное правило:
В jsonb @> важны не только ключи и значения, но и JSON-типы.
Это одна из причин, почему важные поля лучше хранить в обычных колонках правильного типа, а не только в JSON.
Вложенные объекты
Оператор @> хорошо работает с вложенными объектами.
Допустим, в заказе есть адрес доставки:
{
"shipping": {
"country": "DE",
"city": "Berlin",
"zip": "10115"
}
}
Чтобы найти заказы с доставкой в Германию, можно написать:
SELECT
id,
amount
FROM orders
WHERE payload @> '{"shipping":{"country":"DE"}}';
Этот запрос не требует, чтобы внутри shipping было только поле country.
Документ подходит, даже если там есть ещё city, zip и другие поля:
{
"shipping": {
"country": "DE",
"city": "Berlin",
"zip": "10115"
}
}
Потому что он содержит фрагмент:
{
"shipping": {
"country": "DE"
}
}
Важное уточнение про «глубину»
Иногда говорят, что @> ищет фрагмент «на любой глубине». Это можно понять неправильно.
@> не делает свободный поиск ключа по всему документу.
Например, если документ такой:
{
"shipping": {
"country": "DE"
}
}
то условие:
payload @> '{"country":"DE"}'
не сработает.
Почему?
Потому что на верхнем уровне нет ключа country.
Правильно указать путь структурой фрагмента:
payload @> '{"shipping":{"country":"DE"}}'
То есть @> умеет проверять вложенные структуры, но вы должны описать нужную вложенность в правом JSON-фрагменте.
Массивы: порядок обычно не важен
@> также работает с массивами.
Допустим, у пользователя есть теги:
{
"tags": ["beta", "eu", "paid"]
}
Найти пользователей с тегом beta:
SELECT
id,
email
FROM users
WHERE payload @> '{"tags":["beta"]}';
Найти пользователей, у которых есть оба тега beta и eu:
SELECT
id,
email
FROM users
WHERE payload @> '{"tags":["beta","eu"]}';
Порядок элементов в массиве не важен.
Такой документ подходит:
{
"tags": ["eu", "paid", "beta"]
}
Потому что в нём есть и beta, и eu.
Можно запомнить так:
Для JSONB-массивов @> проверяет наличие элементов, а не их позицию.
Примеры с массивами
Проверим идею на простых JSONB-массивах.
SELECT
'[1,2,3]'::jsonb @> '[2,1]'::jsonb AS contains;
Результат:
contains
--------
true
Порядок справа [2,1] не совпадает с порядком слева [1,2,3], но containment всё равно истинный.
А вот так:
SELECT
'[1,2]'::jsonb @> '[1,2,3]'::jsonb AS contains;
Результат:
contains
--------
false
Потому что слева нет элемента 3.
Ещё одна особенность: дубликаты в JSONB-массивах для containment обычно не играют роль как отдельные обязательные копии.
Например, если слева есть 1, это не значит, что нужно хранить две копии 1, чтобы пройти проверку фрагмента с двумя единицами. Поэтому не стоит использовать @> для задач, где важно количество одинаковых элементов в массиве.
Массив объектов
Очень полезный кейс — массив объектов.
Допустим, в заказе есть товары:
{
"items": [
{"sku": "A-100", "qty": 2},
{"sku": "B-200", "qty": 1}
]
}
Найти заказы, где среди товаров есть SKU A-100:
SELECT
id
FROM orders
WHERE payload @> '{"items":[{"sku":"A-100"}]}';
Почему это работает?
Потому что объект:
{"sku":"A-100","qty":2}
содержит фрагмент:
{"sku":"A-100"}
А массив items содержит объект, который подходит под этот фрагмент.
Это очень удобно: не нужно разворачивать массив через jsonb_array_elements, если достаточно проверить наличие подходящего объекта.
Массив объектов: несколько условий
Можно указать несколько полей внутри объекта.
Например, найти заказы, где есть товар A-100 с количеством 2:
SELECT
id
FROM orders
WHERE payload @> '{"items":[{"sku":"A-100","qty":2}]}';
Такой документ подойдёт:
{
"items": [
{"sku": "A-100", "qty": 2, "price": 99.90}
]
}
Потому что объект внутри массива содержит нужные поля:
{"sku":"A-100","qty":2}
Но такой документ не подойдёт:
{
"items": [
{"sku": "A-100", "qty": 1}
]
}
Потому что qty не совпадает.
Пустой объект и пустой массив
С пустыми фрагментами нужно быть аккуратным.
Пустой объект:
{}
содержится почти в любом JSON-объекте.
Например:
SELECT
'{"plan":"pro"}'::jsonb @> '{}'::jsonb AS contains_empty_object;
Результат:
contains_empty_object
---------------------
true
Поэтому условие:
payload @> '{}'
обычно бесполезно: оно будет истинно почти для всех объектов.
Похожая осторожность нужна с пустыми массивами.
Фрагмент:
{"tags":[]}
проверяет наличие совместимой структуры, но не означает «массив tags непустой».
Если нужно проверить, что ключ есть, используйте оператор ?.
Если нужно проверить длину массива, используйте функции вроде jsonb_array_length.
Например:
SELECT id
FROM users
WHERE payload ? 'tags'
AND jsonb_typeof(payload -> 'tags') = 'array'
AND jsonb_array_length(payload -> 'tags') > 0;
Зеркальный оператор <@
У @> есть зеркальный оператор:
<@
Выражение:
a <@ b
означает:
a содержится в b
То есть:
a @> b
и:
b <@ a
по смыслу зеркальны.
Пример:
SELECT
'{"plan":"pro"}'::jsonb <@ '{"plan":"pro","active":true}'::jsonb AS result;
Результат:
result
------
true
На практике чаще используют @>, потому что его легче читать:
payload @> '{"plan":"pro"}'
То есть:
payload содержит такой фрагмент
@> против ->>
Теперь сравним два подхода.
Через ->>:
SELECT id
FROM users
WHERE payload ->> 'plan' = 'pro';
Через @>:
SELECT id
FROM users
WHERE payload @> '{"plan":"pro"}';
Оба запроса могут найти пользователей с тарифом pro.
Но смысл немного разный.
->> говорит:
достань поле plan как текст и сравни со строкой 'pro'
@> говорит:
проверь, содержит ли JSON-фрагмент {"plan":"pro"}
Для простого строкового поля оба варианта понятны.
Но для вложенных объектов и массивов @> часто читается лучше.
Например:
WHERE payload @> '{"shipping":{"country":"DE"}}'
или:
WHERE payload @> '{"tags":["beta","eu"]}'
Это компактнее, чем писать несколько извлечений через -> и ->>.
Когда лучше использовать @>
@> хорошо подходит, когда условие естественно описывается JSON-фрагментом.
Например:
payload @> '{"plan":"pro"}'
payload @> '{"active":true}'
payload @> '{"shipping":{"country":"DE"}}'
payload @> '{"tags":["beta","eu"]}'
payload @> '{"items":[{"sku":"A-100"}]}'
Особенно хорошо @> подходит, когда:
- нужно проверить несколько полей сразу;
- структура вложенная;
- нужно проверить наличие элементов массива;
- нужно использовать один GIN-индекс на JSONB-колонку;
- набор ключей гибкий и заранее не фиксирован полностью.
Когда @> не подходит
@> проверяет containment, но не умеет делать диапазонные сравнения.
Например, так нельзя выразить:
total > 100
created_at < 2026-01-01
priority >= 5
Фрагмент:
{"total": 100}
означает именно:
total равен 100
А не:
total больше 100
Для диапазонов нужен другой подход:
SELECT id
FROM orders
WHERE (payload ->> 'total')::numeric > 100;
Или лучше — отдельная колонка:
orders.total numeric
Если поле часто используется в фильтрах по диапазону, сортировках и агрегатах, хранить его только внутри JSONB обычно не лучшая идея.
GIN-индекс для JSONB
Главное преимущество @> в PostgreSQL — он может хорошо работать с GIN-индексом.
Без индекса запрос:
SELECT id
FROM users
WHERE payload @> '{"plan":"pro"}';
может читать всю таблицу.
На большой таблице это дорого.
Можно создать GIN-индекс:
CREATE INDEX users_payload_gin_idx
ON users
USING gin (payload);
После этого PostgreSQL может использовать индекс для containment-запросов:
SELECT id
FROM users
WHERE payload @> '{"plan":"pro"}';
Чтобы проверить, используется ли индекс, смотрите план:
EXPLAIN ANALYZE
SELECT id
FROM users
WHERE payload @> '{"plan":"pro"}';
Если видите что-то вроде:
Bitmap Index Scan
по вашему GIN-индексу, значит индекс подхватился.
jsonb_ops и jsonb_path_ops
Для GIN-индекса по jsonb есть разные классы операторов.
Самый обычный вариант:
CREATE INDEX users_payload_gin_idx
ON users
USING gin (payload);
Это индекс с операторным классом по умолчанию, обычно jsonb_ops.
Он поддерживает разные JSONB-операторы, включая containment @> и операторы существования ключей вроде:
?
?|
?&
Например:
payload ? 'plan'
проверяет наличие ключа plan.
Есть другой вариант:
CREATE INDEX users_payload_path_gin_idx
ON users
USING gin (payload jsonb_path_ops);
jsonb_path_ops обычно меньше и быстрее именно для запросов containment через @>.
Но он не подходит для операторов существования ключей вроде ?.
То есть:
payload @> '{"plan":"pro"}'
может хорошо использовать jsonb_path_ops.
А:
payload ? 'plan'
обычно требует индекс с jsonb_ops, если вы хотите ускорять именно такой оператор.
Коротко:
jsonb_ops -- шире по поддержке операторов, индекс обычно больше
jsonb_path_ops -- компактнее для @>, но уже по возможностям
Какой GIN-индекс выбрать
Если у вас разные типы JSONB-запросов:
payload @> '{"plan":"pro"}'
payload ? 'tags'
payload ?| array['plan', 'active']
то чаще выбирают обычный индекс:
CREATE INDEX users_payload_gin_idx
ON users
USING gin (payload);
Если у вас почти все частые запросы именно containment:
payload @> '{"plan":"pro"}'
payload @> '{"settings":{"theme":"dark"}}'
payload @> '{"tags":["beta"]}'
можно рассмотреть:
CREATE INDEX users_payload_path_gin_idx
ON users
USING gin (payload jsonb_path_ops);
Но выбор лучше подтверждать на реальных запросах через:
EXPLAIN ANALYZE
Не стоит создавать индексы «на всякий случай». GIN-индексы могут быть большими и замедлять вставки и обновления.
GIN-индекс — не всегда лучший вариант
GIN-индекс по всей JSONB-колонке удобен, когда вы ищете по разным ключам и фрагментам.
Но если вы постоянно фильтруете по одному конкретному полю, иногда лучше выражающий B-tree индекс.
Например, частый запрос:
SELECT id
FROM users
WHERE payload ->> 'plan' = 'pro';
Индекс:
CREATE INDEX users_payload_plan_idx
ON users ((payload ->> 'plan'));
Такой индекс будет проще и точнее для конкретного поля plan.
Для числового поля:
CREATE INDEX orders_payload_total_idx
ON orders (((payload ->> 'total')::numeric));
Но с числовыми cast-индексами нужно быть осторожным: если в JSON есть грязные значения, приведение типа может падать.
Общее правило:
Много разных containment-запросов по JSONB — смотрите в сторону GIN.
Один стабильный ключ — часто лучше выражающий индекс.
Важное бизнес-поле — лучше отдельная колонка.
@> и параметры запроса
В примерах часто пишут JSON-фрагмент строкой:
WHERE payload @> '{"plan":"pro"}'
PostgreSQL обычно понимает тип по контексту оператора.
Но в приложениях лучше явно передавать параметр как jsonb.
Например:
WHERE payload @> $1::jsonb
И передавать значение:
{"plan":"pro"}
Это безопаснее и удобнее, чем склеивать JSON-строку вручную.
Особенно если значения приходят от пользователя.
Не собирайте такие условия строковой конкатенацией:
'{"plan":"' || user_input || '"}'
Лучше используйте параметры.
@> и проверка ключа через ?
Иногда нужно просто проверить, что ключ существует.
Для этого лучше использовать не @>, а оператор ?.
Например:
SELECT id
FROM users
WHERE payload ? 'plan';
Это означает:
на верхнем уровне JSONB есть ключ plan?
А если нужно проверить и наличие ключа, и значение:
SELECT id
FROM users
WHERE payload ? 'plan'
AND payload @> '{"plan":"pro"}';
Хотя второе условие уже подразумевает наличие plan со значением pro, отдельная проверка ключа бывает полезна, когда вы хотите различать случаи:
- ключа нет;
- ключ есть, но значение другое;
- ключ есть, но значение
null.
Для вложенных ключей сначала перейдите к вложенному объекту:
SELECT id
FROM users
WHERE (payload -> 'settings') ? 'theme';
@> и JSON null
С JSON null нужно быть внимательным.
Документ:
{"coupon": null}
содержит фрагмент:
{"coupon": null}
То есть:
SELECT
'{"coupon":null}'::jsonb @> '{"coupon":null}'::jsonb AS result;
вернёт:
result
------
true
Но документ без ключа:
{}
не содержит этот фрагмент:
SELECT
'{}'::jsonb @> '{"coupon":null}'::jsonb AS result;
вернёт:
result
------
false
Это отличие от ->>, где и отсутствующий ключ, и JSON null часто дают SQL NULL.
Через @> можно явно проверить наличие ключа со значением JSON null.
Практические примеры
Найти пользователей с тарифом pro
SELECT
id,
email
FROM users
WHERE payload @> '{"plan":"pro"}';
Найти активных pro-пользователей
SELECT
id,
email
FROM users
WHERE payload @> '{"plan":"pro","active":true}';
Найти пользователей с тегами beta и eu
SELECT
id,
email
FROM users
WHERE payload @> '{"tags":["beta","eu"]}';
Найти заказы с доставкой в Германию
SELECT
id,
amount
FROM orders
WHERE payload @> '{"shipping":{"country":"DE"}}';
Найти заказы с конкретным SKU
SELECT
id
FROM orders
WHERE payload @> '{"items":[{"sku":"A-100"}]}';
Найти заказы с SKU и количеством
SELECT
id
FROM orders
WHERE payload @> '{"items":[{"sku":"A-100","qty":2}]}';
Проверить наличие ключа
SELECT
id
FROM users
WHERE payload ? 'plan';
Проверить JSON null через containment
SELECT
id
FROM orders
WHERE payload @> '{"coupon":null}';
Создать обычный GIN-индекс
CREATE INDEX users_payload_gin_idx
ON users
USING gin (payload);
Создать GIN-индекс для @>
CREATE INDEX users_payload_path_gin_idx
ON users
USING gin (payload jsonb_path_ops);
Выражающий индекс по одному ключу
CREATE INDEX users_payload_plan_idx
ON users ((payload ->> 'plan'));
Проверить план
EXPLAIN ANALYZE
SELECT
id
FROM users
WHERE payload @> '{"plan":"pro"}';
MySQL: JSON_CONTAINS
В MySQL нет PostgreSQL-оператора @>.
Похожая идея реализуется через функцию:
JSON_CONTAINS()
Пример:
SELECT
id
FROM users
WHERE JSON_CONTAINS(payload, '{"plan":"pro"}');
Для вложенного объекта:
SELECT
id
FROM orders
WHERE JSON_CONTAINS(payload, '{"shipping":{"country":"DE"}}');
Для извлечения конкретного поля в MySQL часто используют:
payload ->> '$.plan'
или:
JSON_UNQUOTE(JSON_EXTRACT(payload, '$.plan'))
Идея похожая, но синтаксис отличается от PostgreSQL.
В ClickHouse обычно нет прямого аналога PostgreSQL jsonb @> как универсального containment-оператора.
Чаще JSON разбирают через функции.
Например:
SELECT
id
FROM users
WHERE JSONExtractString(payload, 'plan') = 'pro';
Для числа:
SELECT
id
FROM orders
WHERE JSONExtractFloat(payload, 'total') > 100;
Если нужно работать с массивами или вложенными структурами, подход зависит от формата данных, версии ClickHouse и выбранных JSON-функций.
При переносе запросов из PostgreSQL в ClickHouse containment-логику обычно приходится переписывать явно.
Частые ошибки
Перепутали @> и ->>
@> проверяет JSON-фрагмент:
payload @> '{"plan":"pro"}'
->> извлекает поле как текст:
payload ->> 'plan' = 'pro'
Оба варианта могут работать для простого поля, но это разные операции.
Ожидали поиск ключа на любой глубине
Не сработает:
payload @> '{"country":"DE"}'
если JSON такой:
{"shipping":{"country":"DE"}}
Правильно:
payload @> '{"shipping":{"country":"DE"}}'
Не учли строгие JSON-типы
Не одно и то же:
{"active": true}
и:
{"active": "true"}
Запрос:
payload @> '{"active":true}'
не найдёт строку со строковым "true".
Использовали @> для диапазона
Так нельзя выразить:
total > 100
@> проверяет наличие конкретного фрагмента, а не больше/меньше.
Для диапазона:
(payload ->> 'total')::numeric > 100
или отдельная колонка.
Создали GIN-индекс без реальной необходимости
GIN-индекс полезен, но не бесплатен.
Он занимает место и может замедлять запись.
Перед добавлением индекса проверьте:
- как часто выполняется запрос;
- сколько строк в таблице;
- какой план без индекса;
- какой план после индекса;
- насколько часто обновляется JSONB-колонка.
Что важно запомнить
@> проверяет, содержит ли JSONB-документ заданный JSONB-фрагмент.
Пример:
SELECT
id
FROM users
WHERE payload @> '{"plan":"pro"}';
Главные правила:
- левый JSONB должен содержать правый JSONB-фрагмент;
- это не равенство всего документа;
- документ может иметь дополнительные ключи;
- типы должны совпадать строго;
- вложенные объекты нужно описывать вложенным фрагментом;
@> не ищет ключ автоматически на любой глубине;
- для массивов порядок элементов обычно не важен;
- массив объектов можно проверять по частичному объекту;
@> хорошо сочетается с GIN-индексом;
jsonb_ops шире по операторам;
jsonb_path_ops компактнее для containment-запросов;
- для диапазонов и сортировок
@> не подходит;
- для одного частого поля часто лучше выражающий индекс или обычная колонка.
Короткий вывод
JSONB @> — это оператор для проверки, содержит ли JSONB-документ нужный фрагмент.
Например:
payload @> '{"plan":"pro"}'
означает:
payload содержит поле plan со значением pro
Для вложенной структуры:
payload @> '{"shipping":{"country":"DE"}}'
Для массива тегов:
payload @> '{"tags":["beta","eu"]}'
Главная мысль:
@> используем, когда условие удобно описать JSON-фрагментом.
Если таких запросов много, добавляйте GIN-индекс:
CREATE INDEX users_payload_gin_idx
ON users
USING gin (payload);
Но если вы постоянно фильтруете одно и то же поле, особенно числовое или датовое, лучше подумать о выражающем индексе или отдельной колонке правильного типа.
jsonb даёт гибкость, @> даёт удобный containment-поиск, а GIN-индекс помогает сделать такие запросы быстрыми на больших таблицах.
В PostgreSQL тип
jsonbчасто используют для гибких структурированных данных.Например, в таблице
usersможет быть колонкаpayload:{ "plan": "pro", "active": true, "tags": ["beta", "eu"], "settings": { "theme": "dark", "language": "ru" } }Иногда нужно не просто достать одно поле через
->>.Иногда нужно спросить у JSON-документа:
Например:
{"plan": "pro"}или:
{"settings": {"theme": "dark"}}Для такой проверки в PostgreSQL есть оператор:
@>Он называется containment operator.
По-русски его удобно понимать как:
Запрос:
payload @> '{"plan":"pro"}'означает:
Если да — условие вернёт
true.Что делает оператор @>
Оператор
@>проверяет, содержит ли левыйjsonbправыйjsonb-фрагмент.Пример:
SELECT id, email FROM users WHERE payload @> '{"plan":"pro"}';Этот запрос найдёт пользователей, у которых в
payloadесть:{ "plan": "pro" }При этом в самом
payloadмогут быть и другие поля.Например, такой документ подходит:
{ "plan": "pro", "active": true, "tags": ["beta", "eu"], "settings": { "theme": "dark" } }Почему подходит?
Потому что он содержит нужный фрагмент:
{"plan": "pro"}Оператор
@>не требует, чтобы документы были равны целиком. Он проверяет именно наличие фрагмента.@> — это не равенство
Важно не путать
@>с обычным равенством.Равенство:
payload = '{"plan":"pro"}'::jsonbозначает:
А containment:
payload @> '{"plan":"pro"}'означает:
Пример:
{ "plan": "pro", "active": true }Для такого документа:
payload = '{"plan":"pro"}'::jsonbвернёт
false, потому что в документе есть ещёactive.А:
payload @> '{"plan":"pro"}'вернёт
true, потому что нужный фрагмент внутри есть.Можно запомнить так:
Базовый пример
Допустим, есть таблица
users:Найдём всех пользователей с тарифом
pro:SELECT id, email FROM users WHERE payload @> '{"plan":"pro"}';Результат:
Пользователь
2не попал, потому что у него:{"plan": "free"}Несколько условий внутри одного JSON-фрагмента
В правом фрагменте можно указать несколько полей.
Например, найти пользователей, у которых:
plan = "pro";active = true.SELECT id, email FROM users WHERE payload @> '{"plan":"pro","active":true}';Такой фрагмент:
{"plan":"pro","active":true}означает:
Подойдёт документ:
{ "plan": "pro", "active": true, "tags": ["beta"] }Не подойдёт:
{ "plan": "pro", "active": false }И не подойдёт:
{ "plan": "free", "active": true }То есть поля внутри правого JSON-фрагмента работают как несколько условий одновременно.
Типы должны совпадать строго
jsonbразличает типы.Например, это boolean:
{"active": true}А это строка:
{"active": "true"}Для PostgreSQL это разные значения.
Запрос:
SELECT id FROM users WHERE payload @> '{"active":true}';найдёт документы, где
active— именно JSON booleantrue.Он не найдёт документы, где
activeхранится строкой:{"active": "true"}То же самое с числами.
Это число:
{"priority": 5}А это строка:
{"priority": "5"}Запрос:
WHERE payload @> '{"priority":5}'не совпадает с:
{"priority": "5"}Главное правило:
Это одна из причин, почему важные поля лучше хранить в обычных колонках правильного типа, а не только в JSON.
Вложенные объекты
Оператор
@>хорошо работает с вложенными объектами.Допустим, в заказе есть адрес доставки:
{ "shipping": { "country": "DE", "city": "Berlin", "zip": "10115" } }Чтобы найти заказы с доставкой в Германию, можно написать:
SELECT id, amount FROM orders WHERE payload @> '{"shipping":{"country":"DE"}}';Этот запрос не требует, чтобы внутри
shippingбыло только полеcountry.Документ подходит, даже если там есть ещё
city,zipи другие поля:{ "shipping": { "country": "DE", "city": "Berlin", "zip": "10115" } }Потому что он содержит фрагмент:
{ "shipping": { "country": "DE" } }Важное уточнение про «глубину»
Иногда говорят, что
@>ищет фрагмент «на любой глубине». Это можно понять неправильно.@>не делает свободный поиск ключа по всему документу.Например, если документ такой:
{ "shipping": { "country": "DE" } }то условие:
payload @> '{"country":"DE"}'не сработает.
Почему?
Потому что на верхнем уровне нет ключа
country.Правильно указать путь структурой фрагмента:
payload @> '{"shipping":{"country":"DE"}}'То есть
@>умеет проверять вложенные структуры, но вы должны описать нужную вложенность в правом JSON-фрагменте.Массивы: порядок обычно не важен
@>также работает с массивами.Допустим, у пользователя есть теги:
{ "tags": ["beta", "eu", "paid"] }Найти пользователей с тегом
beta:SELECT id, email FROM users WHERE payload @> '{"tags":["beta"]}';Найти пользователей, у которых есть оба тега
betaиeu:SELECT id, email FROM users WHERE payload @> '{"tags":["beta","eu"]}';Порядок элементов в массиве не важен.
Такой документ подходит:
{ "tags": ["eu", "paid", "beta"] }Потому что в нём есть и
beta, иeu.Можно запомнить так:
Примеры с массивами
Проверим идею на простых JSONB-массивах.
SELECT '[1,2,3]'::jsonb @> '[2,1]'::jsonb AS contains;Результат:
Порядок справа
[2,1]не совпадает с порядком слева[1,2,3], но containment всё равно истинный.А вот так:
SELECT '[1,2]'::jsonb @> '[1,2,3]'::jsonb AS contains;Результат:
Потому что слева нет элемента
3.Ещё одна особенность: дубликаты в JSONB-массивах для containment обычно не играют роль как отдельные обязательные копии.
Например, если слева есть
1, это не значит, что нужно хранить две копии1, чтобы пройти проверку фрагмента с двумя единицами. Поэтому не стоит использовать@>для задач, где важно количество одинаковых элементов в массиве.Массив объектов
Очень полезный кейс — массив объектов.
Допустим, в заказе есть товары:
{ "items": [ {"sku": "A-100", "qty": 2}, {"sku": "B-200", "qty": 1} ] }Найти заказы, где среди товаров есть SKU
A-100:SELECT id FROM orders WHERE payload @> '{"items":[{"sku":"A-100"}]}';Почему это работает?
Потому что объект:
{"sku":"A-100","qty":2}содержит фрагмент:
{"sku":"A-100"}А массив
itemsсодержит объект, который подходит под этот фрагмент.Это очень удобно: не нужно разворачивать массив через
jsonb_array_elements, если достаточно проверить наличие подходящего объекта.Массив объектов: несколько условий
Можно указать несколько полей внутри объекта.
Например, найти заказы, где есть товар
A-100с количеством2:SELECT id FROM orders WHERE payload @> '{"items":[{"sku":"A-100","qty":2}]}';Такой документ подойдёт:
{ "items": [ {"sku": "A-100", "qty": 2, "price": 99.90} ] }Потому что объект внутри массива содержит нужные поля:
{"sku":"A-100","qty":2}Но такой документ не подойдёт:
{ "items": [ {"sku": "A-100", "qty": 1} ] }Потому что
qtyне совпадает.Пустой объект и пустой массив
С пустыми фрагментами нужно быть аккуратным.
Пустой объект:
{}содержится почти в любом JSON-объекте.
Например:
SELECT '{"plan":"pro"}'::jsonb @> '{}'::jsonb AS contains_empty_object;Результат:
Поэтому условие:
payload @> '{}'обычно бесполезно: оно будет истинно почти для всех объектов.
Похожая осторожность нужна с пустыми массивами.
Фрагмент:
{"tags":[]}проверяет наличие совместимой структуры, но не означает «массив tags непустой».
Если нужно проверить, что ключ есть, используйте оператор
?.Если нужно проверить длину массива, используйте функции вроде
jsonb_array_length.Например:
SELECT id FROM users WHERE payload ? 'tags' AND jsonb_typeof(payload -> 'tags') = 'array' AND jsonb_array_length(payload -> 'tags') > 0;Зеркальный оператор <@
У
@>есть зеркальный оператор:<@Выражение:
a <@ bозначает:
То есть:
a @> bи:
b <@ aпо смыслу зеркальны.
Пример:
SELECT '{"plan":"pro"}'::jsonb <@ '{"plan":"pro","active":true}'::jsonb AS result;Результат:
На практике чаще используют
@>, потому что его легче читать:payload @> '{"plan":"pro"}'То есть:
@> против ->>
Теперь сравним два подхода.
Через
->>:SELECT id FROM users WHERE payload ->> 'plan' = 'pro';Через
@>:SELECT id FROM users WHERE payload @> '{"plan":"pro"}';Оба запроса могут найти пользователей с тарифом
pro.Но смысл немного разный.
->>говорит:@>говорит:Для простого строкового поля оба варианта понятны.
Но для вложенных объектов и массивов
@>часто читается лучше.Например:
WHERE payload @> '{"shipping":{"country":"DE"}}'или:
WHERE payload @> '{"tags":["beta","eu"]}'Это компактнее, чем писать несколько извлечений через
->и->>.Когда лучше использовать @>
@>хорошо подходит, когда условие естественно описывается JSON-фрагментом.Например:
payload @> '{"plan":"pro"}'payload @> '{"active":true}'payload @> '{"shipping":{"country":"DE"}}'payload @> '{"tags":["beta","eu"]}'payload @> '{"items":[{"sku":"A-100"}]}'Особенно хорошо
@>подходит, когда:Когда @> не подходит
@>проверяет containment, но не умеет делать диапазонные сравнения.Например, так нельзя выразить:
Фрагмент:
{"total": 100}означает именно:
А не:
Для диапазонов нужен другой подход:
SELECT id FROM orders WHERE (payload ->> 'total')::numeric > 100;Или лучше — отдельная колонка:
Если поле часто используется в фильтрах по диапазону, сортировках и агрегатах, хранить его только внутри JSONB обычно не лучшая идея.
GIN-индекс для JSONB
Главное преимущество
@>в PostgreSQL — он может хорошо работать с GIN-индексом.Без индекса запрос:
SELECT id FROM users WHERE payload @> '{"plan":"pro"}';может читать всю таблицу.
На большой таблице это дорого.
Можно создать GIN-индекс:
CREATE INDEX users_payload_gin_idx ON users USING gin (payload);После этого PostgreSQL может использовать индекс для containment-запросов:
SELECT id FROM users WHERE payload @> '{"plan":"pro"}';Чтобы проверить, используется ли индекс, смотрите план:
EXPLAIN ANALYZE SELECT id FROM users WHERE payload @> '{"plan":"pro"}';Если видите что-то вроде:
по вашему GIN-индексу, значит индекс подхватился.
jsonb_ops и jsonb_path_ops
Для GIN-индекса по
jsonbесть разные классы операторов.Самый обычный вариант:
CREATE INDEX users_payload_gin_idx ON users USING gin (payload);Это индекс с операторным классом по умолчанию, обычно
jsonb_ops.Он поддерживает разные JSONB-операторы, включая containment
@>и операторы существования ключей вроде:? ?| ?&Например:
payload ? 'plan'проверяет наличие ключа
plan.Есть другой вариант:
CREATE INDEX users_payload_path_gin_idx ON users USING gin (payload jsonb_path_ops);jsonb_path_opsобычно меньше и быстрее именно для запросов containment через@>.Но он не подходит для операторов существования ключей вроде
?.То есть:
payload @> '{"plan":"pro"}'может хорошо использовать
jsonb_path_ops.А:
payload ? 'plan'обычно требует индекс с
jsonb_ops, если вы хотите ускорять именно такой оператор.Коротко:
Какой GIN-индекс выбрать
Если у вас разные типы JSONB-запросов:
payload @> '{"plan":"pro"}' payload ? 'tags' payload ?| array['plan', 'active']то чаще выбирают обычный индекс:
CREATE INDEX users_payload_gin_idx ON users USING gin (payload);Если у вас почти все частые запросы именно containment:
payload @> '{"plan":"pro"}' payload @> '{"settings":{"theme":"dark"}}' payload @> '{"tags":["beta"]}'можно рассмотреть:
CREATE INDEX users_payload_path_gin_idx ON users USING gin (payload jsonb_path_ops);Но выбор лучше подтверждать на реальных запросах через:
Не стоит создавать индексы «на всякий случай». GIN-индексы могут быть большими и замедлять вставки и обновления.
GIN-индекс — не всегда лучший вариант
GIN-индекс по всей JSONB-колонке удобен, когда вы ищете по разным ключам и фрагментам.
Но если вы постоянно фильтруете по одному конкретному полю, иногда лучше выражающий B-tree индекс.
Например, частый запрос:
SELECT id FROM users WHERE payload ->> 'plan' = 'pro';Индекс:
CREATE INDEX users_payload_plan_idx ON users ((payload ->> 'plan'));Такой индекс будет проще и точнее для конкретного поля
plan.Для числового поля:
CREATE INDEX orders_payload_total_idx ON orders (((payload ->> 'total')::numeric));Но с числовыми cast-индексами нужно быть осторожным: если в JSON есть грязные значения, приведение типа может падать.
Общее правило:
@> и параметры запроса
В примерах часто пишут JSON-фрагмент строкой:
WHERE payload @> '{"plan":"pro"}'PostgreSQL обычно понимает тип по контексту оператора.
Но в приложениях лучше явно передавать параметр как
jsonb.Например:
WHERE payload @> $1::jsonbИ передавать значение:
{"plan":"pro"}Это безопаснее и удобнее, чем склеивать JSON-строку вручную.
Особенно если значения приходят от пользователя.
Не собирайте такие условия строковой конкатенацией:
Лучше используйте параметры.
@> и проверка ключа через ?
Иногда нужно просто проверить, что ключ существует.
Для этого лучше использовать не
@>, а оператор?.Например:
SELECT id FROM users WHERE payload ? 'plan';Это означает:
А если нужно проверить и наличие ключа, и значение:
SELECT id FROM users WHERE payload ? 'plan' AND payload @> '{"plan":"pro"}';Хотя второе условие уже подразумевает наличие
planсо значениемpro, отдельная проверка ключа бывает полезна, когда вы хотите различать случаи:null.Для вложенных ключей сначала перейдите к вложенному объекту:
SELECT id FROM users WHERE (payload -> 'settings') ? 'theme';@> и JSON null
С JSON
nullнужно быть внимательным.Документ:
{"coupon": null}содержит фрагмент:
{"coupon": null}То есть:
SELECT '{"coupon":null}'::jsonb @> '{"coupon":null}'::jsonb AS result;вернёт:
Но документ без ключа:
{}не содержит этот фрагмент:
SELECT '{}'::jsonb @> '{"coupon":null}'::jsonb AS result;вернёт:
Это отличие от
->>, где и отсутствующий ключ, и JSONnullчасто дают SQLNULL.Через
@>можно явно проверить наличие ключа со значением JSONnull.Практические примеры
Найти пользователей с тарифом pro
SELECT id, email FROM users WHERE payload @> '{"plan":"pro"}';Найти активных pro-пользователей
SELECT id, email FROM users WHERE payload @> '{"plan":"pro","active":true}';Найти пользователей с тегами beta и eu
SELECT id, email FROM users WHERE payload @> '{"tags":["beta","eu"]}';Найти заказы с доставкой в Германию
SELECT id, amount FROM orders WHERE payload @> '{"shipping":{"country":"DE"}}';Найти заказы с конкретным SKU
SELECT id FROM orders WHERE payload @> '{"items":[{"sku":"A-100"}]}';Найти заказы с SKU и количеством
SELECT id FROM orders WHERE payload @> '{"items":[{"sku":"A-100","qty":2}]}';Проверить наличие ключа
SELECT id FROM users WHERE payload ? 'plan';Проверить JSON null через containment
SELECT id FROM orders WHERE payload @> '{"coupon":null}';Создать обычный GIN-индекс
CREATE INDEX users_payload_gin_idx ON users USING gin (payload);Создать GIN-индекс для @>
CREATE INDEX users_payload_path_gin_idx ON users USING gin (payload jsonb_path_ops);Выражающий индекс по одному ключу
CREATE INDEX users_payload_plan_idx ON users ((payload ->> 'plan'));Проверить план
EXPLAIN ANALYZE SELECT id FROM users WHERE payload @> '{"plan":"pro"}';MySQL: JSON_CONTAINS
В MySQL нет PostgreSQL-оператора
@>.Похожая идея реализуется через функцию:
Пример:
SELECT id FROM users WHERE JSON_CONTAINS(payload, '{"plan":"pro"}');Для вложенного объекта:
SELECT id FROM orders WHERE JSON_CONTAINS(payload, '{"shipping":{"country":"DE"}}');Для извлечения конкретного поля в MySQL часто используют:
payload ->> '$.plan'или:
JSON_UNQUOTE(JSON_EXTRACT(payload, '$.plan'))Идея похожая, но синтаксис отличается от PostgreSQL.
ClickHouse: чаще JSONExtract-функции
В ClickHouse обычно нет прямого аналога PostgreSQL
jsonb @>как универсального containment-оператора.Чаще JSON разбирают через функции.
Например:
SELECT id FROM users WHERE JSONExtractString(payload, 'plan') = 'pro';Для числа:
SELECT id FROM orders WHERE JSONExtractFloat(payload, 'total') > 100;Если нужно работать с массивами или вложенными структурами, подход зависит от формата данных, версии ClickHouse и выбранных JSON-функций.
При переносе запросов из PostgreSQL в ClickHouse containment-логику обычно приходится переписывать явно.
Частые ошибки
Перепутали @> и ->>
@>проверяет JSON-фрагмент:payload @> '{"plan":"pro"}'->>извлекает поле как текст:payload ->> 'plan' = 'pro'Оба варианта могут работать для простого поля, но это разные операции.
Ожидали поиск ключа на любой глубине
Не сработает:
payload @> '{"country":"DE"}'если JSON такой:
{"shipping":{"country":"DE"}}Правильно:
payload @> '{"shipping":{"country":"DE"}}'Не учли строгие JSON-типы
Не одно и то же:
{"active": true}и:
{"active": "true"}Запрос:
payload @> '{"active":true}'не найдёт строку со строковым
"true".Использовали @> для диапазона
Так нельзя выразить:
@>проверяет наличие конкретного фрагмента, а не больше/меньше.Для диапазона:
(payload ->> 'total')::numeric > 100или отдельная колонка.
Создали GIN-индекс без реальной необходимости
GIN-индекс полезен, но не бесплатен.
Он занимает место и может замедлять запись.
Перед добавлением индекса проверьте:
Что важно запомнить
@>проверяет, содержит ли JSONB-документ заданный JSONB-фрагмент.Пример:
SELECT id FROM users WHERE payload @> '{"plan":"pro"}';Главные правила:
@>не ищет ключ автоматически на любой глубине;@>хорошо сочетается с GIN-индексом;jsonb_opsшире по операторам;jsonb_path_opsкомпактнее для containment-запросов;@>не подходит;Короткий вывод
JSONB @>— это оператор для проверки, содержит ли JSONB-документ нужный фрагмент.Например:
payload @> '{"plan":"pro"}'означает:
Для вложенной структуры:
payload @> '{"shipping":{"country":"DE"}}'Для массива тегов:
payload @> '{"tags":["beta","eu"]}'Главная мысль:
Если таких запросов много, добавляйте GIN-индекс:
CREATE INDEX users_payload_gin_idx ON users USING gin (payload);Но если вы постоянно фильтруете одно и то же поле, особенно числовое или датовое, лучше подумать о выражающем индексе или отдельной колонке правильного типа.
jsonbдаёт гибкость,@>даёт удобный containment-поиск, а GIN-индекс помогает сделать такие запросы быстрыми на больших таблицах.