В PostgreSQL часто хранят данные не только в обычных столбцах вроде name, price и created_at, но и внутри JSONB.
Например, в колонку можно положить настройки пользователя, дополнительные параметры заказа, список тегов или редкие атрибуты товара:
{
"theme": "dark",
"lang": "en",
"notifications": {
"email": true,
"sms": false
}
}
На первый взгляд удобно: структура гибкая, можно хранить вложенные объекты, массивы и разные наборы полей. Но сразу появляется главный вопрос: как теперь достать нужное значение из этого JSON?
В PostgreSQL для этого чаще всего используют два оператора:
Они выглядят почти одинаково, но разница между ними очень важна. Один оставляет результат в формате JSON, другой превращает его в обычный текст. Из-за этого один запрос спокойно работает, а другой может упасть с ошибкой типов или незаметно дать неправильный результат.
Разберёмся спокойно и по шагам.
Главное отличие: -> возвращает jsonb, а ->> возвращает text
Оба оператора достают значение из JSON по ключу или индексу.
Разница в том, в каком типе они возвращают результат.
Оператор -> возвращает значение как JSON. То есть результат остаётся внутри мира jsonb: с кавычками, массивами, объектами и JSON-структурой.
Оператор ->> возвращает значение как обычный текст, то есть text.
Посмотрим на простой пример. Допустим, в таблице users есть колонка prefs:
{
"theme": "dark",
"lang": "en"
}
Запрос:
SELECT
prefs -> 'theme' AS theme_jsonb,
prefs ->> 'theme' AS theme_text
FROM users;
Результат будет примерно таким:
theme_jsonb |
theme_text |
"dark" |
dark |
Разница маленькая на вид, но большая по смыслу:
prefs -> 'theme' вернул JSON-значение "dark";
prefs ->> 'theme' вернул обычный текст dark.
Можно запомнить так:
Одна стрелка -> оставляет значение в JSON. Две стрелки ->> достают текст наружу.
Когда использовать ->, а когда ->>
Самое удобное правило такое:
Используйте ->, если нужно идти глубже внутрь JSON.
Используйте ->> на последнем шаге, когда нужно получить обычное значение для вывода, фильтра, сортировки или приведения типа.
Представим JSON с вложенным объектом:
{
"address": {
"city": "Berlin",
"zip": "10115"
}
}
Чтобы достать город, нужно сначала зайти в объект address, а потом взять из него поле city:
SELECT
profile -> 'address' ->> 'city' AS city
FROM users;
Здесь всё логично:
profile -> 'address' возвращает вложенный JSON-объект.
->> 'city' достаёт из него обычный текст.
А вот так делать нельзя:
SELECT
profile ->> 'address' ->> 'city' AS city
FROM users;
Почему? Потому что profile ->> 'address' уже превратил объект address в текст. А к тексту нельзя снова применить JSON-стрелку как к объекту.
То есть ->> в середине пути — частая ошибка новичков. Его место обычно в конце цепочки.
Как доставать поля объекта
Если справа от стрелки стоит строка, PostgreSQL воспринимает её как имя поля объекта.
Допустим, в колонке payload лежит такой JSON:
{
"status": "paid",
"amount": "149.90",
"currency": "EUR"
}
Достанем статус и валюту:
SELECT
payload ->> 'status' AS status,
payload ->> 'currency' AS currency
FROM orders;
Такой запрос вернёт обычный текст:
Для вывода в отчёт чаще всего нужен именно ->>, потому что пользователю в таблице обычно нужен текст, а не JSON-скаляр с кавычками.
Как доставать элементы массива
JSON может хранить не только объекты, но и массивы.
Например:
{
"tags": ["new", "vip", "eu"]
}
Чтобы достать элемент массива, справа от стрелки указывают число — индекс элемента.
Важно: индексация JSON-массивов начинается с нуля.
То есть:
0 — первый элемент;
1 — второй элемент;
2 — третий элемент.
Пример:
SELECT
meta -> 'tags' ->> 0 AS first_tag,
meta -> 'tags' ->> 1 AS second_tag
FROM users;
Результат:
first_tag |
second_tag |
new |
vip |
Ещё PostgreSQL позволяет использовать отрицательные индексы:
-1 — последний элемент;
-2 — предпоследний элемент.
SELECT
meta -> 'tags' ->> -1 AS last_tag
FROM users;
Это удобно, когда длина массива заранее неизвестна.
Пример с заказами: достаём артикул первой позиции
Теперь посмотрим на более жизненный пример.
Допустим, в таблице orders есть колонка payload, где хранится заказ:
{
"items": [
{
"sku": "A1",
"qty": 2
},
{
"sku": "B7",
"qty": 1
}
]
}
Нужно достать артикул первой позиции заказа.
Путь такой:
- Зайти в поле
items.
- Взять первый элемент массива.
- Из этого элемента взять поле
sku.
SQL-запрос:
SELECT
id,
payload -> 'items' -> 0 ->> 'sku' AS first_sku
FROM orders
WHERE status = 'paid';
Читается почти как путь к файлу:
payload / items / first element / sku
Только в PostgreSQL этот путь записывается стрелками.
Длинные цепочки: идём внутрь через ->, достаём значение через ->>
Вложенность в JSON может быть глубокой.
Например:
{
"customer": {
"profile": {
"address": {
"city": "Berlin"
}
}
}
}
Чтобы достать город:
SELECT
payload -> 'customer' -> 'profile' -> 'address' ->> 'city' AS city
FROM orders;
Обратите внимание на закономерность:
- все промежуточные шаги идут через
->;
- последний шаг идёт через
->>.
Это хороший стиль: сначала спокойно спускаемся по JSON-структуре, а в конце достаём обычное значение.
Почему числа из JSON нужно приводить к числам
->> всегда возвращает text.
Даже если внутри JSON лежит число, после ->> PostgreSQL отдаст его как текст.
Например:
{
"age": "34"
}
Если нужно сравнивать возраст как число, нужно явно привести тип:
SELECT
(profile ->> 'age')::int AS age
FROM users
WHERE (profile ->> 'age')::int >= 18;
Здесь ::int превращает текст в целое число.
Это особенно важно в фильтрах. Без приведения PostgreSQL может сравнивать строки, а не числа.
Например:
SELECT
id
FROM orders
WHERE payload ->> 'amount' > '99';
Такой запрос выглядит правдоподобно, но он опасен. Значение из payload ->> 'amount' — это текст. Значит, сравнение тоже текстовое.
В текстовом сравнении '100' может оказаться меньше '99', потому что строки сравниваются посимвольно, а не как числа.
Правильно так:
SELECT
id
FROM orders
WHERE (payload ->> 'amount')::numeric > 99;
Теперь сумма сравнивается как число.
Пример с агрегацией: средний чек из JSON
Допустим, сумма заказа хранится в JSON:
{
"amount": "149.90",
"currency": "EUR"
}
Нужно посчитать средний чек по странам пользователей.
SELECT
u.country,
round(avg((o.payload ->> 'amount')::numeric), 2) AS avg_amount
FROM orders o
JOIN users u ON u.id = o.user_id
GROUP BY u.country
ORDER BY avg_amount DESC;
Здесь важно вот это место:
(o.payload ->> 'amount')::numeric
Сначала мы достаём значение amount как текст, потом превращаем его в numeric, и только после этого передаём в avg.
Скобки нужны, чтобы PostgreSQL сначала выполнил извлечение из JSON, а уже потом привёл результат к числу.
Что будет, если ключа нет
Если в JSON нет нужного ключа, PostgreSQL не выдаст ошибку. Он просто вернёт NULL.
Например, в JSON нет поля phone:
{
"name": "Alex",
"email": "alex@example.com"
}
Запрос:
SELECT
profile ->> 'phone' AS phone
FROM users;
Вернёт NULL.
С одной стороны, это удобно: запрос не падает из-за отсутствующего поля. С другой — опасно. Если вы ошиблись в названии ключа, PostgreSQL тоже вернёт NULL, и ошибку можно долго не заметить.
Например:
SELECT
profile ->> 'emial' AS email
FROM users;
Если вы случайно написали emial вместо email, запрос выполнится, но вернёт пустые значения.
Поэтому при работе с JSON особенно важно внимательно проверять имена ключей.
Частые ловушки с -> и ->>
Ошибка 1. Сравнивать jsonb с текстом
Вот так делать не стоит:
SELECT
id
FROM users
WHERE prefs -> 'theme' = 'dark';
Слева результат типа jsonb, справа текстовая строка. Это разные типы.
Правильно использовать ->>:
SELECT
id
FROM users
WHERE prefs ->> 'theme' = 'dark';
Есть и другой вариант — сравнивать JSON с JSON:
SELECT
id
FROM users
WHERE prefs -> 'theme' = '"dark"'::jsonb;
Но для обычных фильтров по строковым значениям проще и понятнее использовать ->>.
Ошибка 2. Использовать ->> слишком рано
Неправильно:
SELECT
profile ->> 'address' ->> 'city' AS city
FROM users;
После profile ->> 'address' результат уже стал текстом. Дальше идти по нему стрелками как по JSON-объекту нельзя.
Правильно:
SELECT
profile -> 'address' ->> 'city' AS city
FROM users;
Ошибка 3. Забыть привести тип
Неправильно:
SELECT
id
FROM orders
WHERE payload ->> 'amount' > '100';
Правильно:
SELECT
id
FROM orders
WHERE (payload ->> 'amount')::numeric > 100;
Если значение должно быть числом, датой или булевым значением, не оставляйте его текстом.
Ошибка 4. Доставать объект через ->> и ждать отдельное поле
Если применить ->> к объекту или массиву, PostgreSQL вернёт весь объект или массив как текст.
Например:
SELECT
profile ->> 'address' AS address
FROM users;
Если address — это объект, результат будет строкой примерно такого вида:
{"city": "Berlin", "zip": "10115"}
Это не ошибка, но обычно это не то, что нужно. Если нужен город, нужно идти глубже:
SELECT
profile -> 'address' ->> 'city' AS city
FROM users;
Мини-шпаргалка
| Задача |
Что использовать |
| Достать вложенный объект |
-> |
| Достать вложенный массив |
-> |
| Продолжить путь дальше |
-> |
| Получить строку для вывода |
->> |
Получить значение для WHERE |
чаще всего ->> |
| Получить число из JSON |
->> плюс приведение типа |
| Получить элемент массива |
-> 0, ->> 0, -> -1, ->> -1 |
Главная мысль:
-> оставляет вас внутри JSON. ->> достаёт значение наружу как текст.
Чем PostgreSQL отличается от других баз
Синтаксис со стрелками удобный, но он не является универсальным для всех баз данных.
В PostgreSQL можно писать цепочки:
SELECT
profile -> 'address' ->> 'city' AS city
FROM users;
В MySQL чаще используют JSON-путь одной строкой:
SELECT
JSON_EXTRACT(profile, '$.address.city') AS city
FROM users;
Также в MySQL есть операторы для работы с JSON, но путь всё равно записывается иначе:
SELECT
profile ->> '$.address.city' AS city
FROM users;
В ClickHouse обычно используют специальные функции, которые сразу возвращают нужный тип:
SELECT
JSONExtractString(profile, 'city') AS city
FROM users;
Идея везде похожая: нужно достать значение из JSON по пути. Но синтаксис и типы результата отличаются.
В PostgreSQL важно хорошо понимать именно пару -> и ->>, потому что на ней строится большая часть повседневной работы с JSONB.
Главное
JSONB удобен, когда данные гибкие: настройки, метаданные, дополнительные параметры, вложенные списки и объекты. Но доставать значения из него нужно аккуратно.
Запомните простое правило:
-> возвращает jsonb;
->> возвращает text;
-> используем для промежуточных шагов;
->> используем в конце, когда нужно обычное значение;
- числа, даты и булевые значения после
->> нужно явно приводить к нужному типу;
- отсутствующий ключ не вызывает ошибку, а возвращает
NULL.
Когда это правило укладывается в голове, JSON-запросы в PostgreSQL перестают выглядеть как магия. Вы просто идёте по структуре слева направо: объект, поле, массив, элемент, вложенное поле — и в конце аккуратно достаёте нужное значение наружу.
В PostgreSQL часто хранят данные не только в обычных столбцах вроде
name,priceиcreated_at, но и внутриJSONB.Например, в колонку можно положить настройки пользователя, дополнительные параметры заказа, список тегов или редкие атрибуты товара:
{ "theme": "dark", "lang": "en", "notifications": { "email": true, "sms": false } }На первый взгляд удобно: структура гибкая, можно хранить вложенные объекты, массивы и разные наборы полей. Но сразу появляется главный вопрос: как теперь достать нужное значение из этого JSON?
В PostgreSQL для этого чаще всего используют два оператора:
->->>Они выглядят почти одинаково, но разница между ними очень важна. Один оставляет результат в формате JSON, другой превращает его в обычный текст. Из-за этого один запрос спокойно работает, а другой может упасть с ошибкой типов или незаметно дать неправильный результат.
Разберёмся спокойно и по шагам.
Главное отличие:
->возвращаетjsonb, а->>возвращаетtextОба оператора достают значение из JSON по ключу или индексу.
Разница в том, в каком типе они возвращают результат.
Оператор
->возвращает значение как JSON. То есть результат остаётся внутри мираjsonb: с кавычками, массивами, объектами и JSON-структурой.Оператор
->>возвращает значение как обычный текст, то естьtext.Посмотрим на простой пример. Допустим, в таблице
usersесть колонкаprefs:Запрос:
SELECT prefs -> 'theme' AS theme_jsonb, prefs ->> 'theme' AS theme_text FROM users;Результат будет примерно таким:
theme_jsonbtheme_text"dark"darkРазница маленькая на вид, но большая по смыслу:
prefs -> 'theme'вернул JSON-значение"dark";prefs ->> 'theme'вернул обычный текстdark.Можно запомнить так:
Одна стрелка
->оставляет значение в JSON. Две стрелки->>достают текст наружу.Когда использовать
->, а когда->>Самое удобное правило такое:
Используйте
->, если нужно идти глубже внутрь JSON.Используйте
->>на последнем шаге, когда нужно получить обычное значение для вывода, фильтра, сортировки или приведения типа.Представим JSON с вложенным объектом:
Чтобы достать город, нужно сначала зайти в объект
address, а потом взять из него полеcity:SELECT profile -> 'address' ->> 'city' AS city FROM users;Здесь всё логично:
profile -> 'address'возвращает вложенный JSON-объект.->> 'city'достаёт из него обычный текст.А вот так делать нельзя:
SELECT profile ->> 'address' ->> 'city' AS city FROM users;Почему? Потому что
profile ->> 'address'уже превратил объектaddressв текст. А к тексту нельзя снова применить JSON-стрелку как к объекту.То есть
->>в середине пути — частая ошибка новичков. Его место обычно в конце цепочки.Как доставать поля объекта
Если справа от стрелки стоит строка, PostgreSQL воспринимает её как имя поля объекта.
Допустим, в колонке
payloadлежит такой JSON:Достанем статус и валюту:
SELECT payload ->> 'status' AS status, payload ->> 'currency' AS currency FROM orders;Такой запрос вернёт обычный текст:
statuscurrencypaidEURДля вывода в отчёт чаще всего нужен именно
->>, потому что пользователю в таблице обычно нужен текст, а не JSON-скаляр с кавычками.Как доставать элементы массива
JSON может хранить не только объекты, но и массивы.
Например:
Чтобы достать элемент массива, справа от стрелки указывают число — индекс элемента.
Важно: индексация JSON-массивов начинается с нуля.
То есть:
0— первый элемент;1— второй элемент;2— третий элемент.Пример:
SELECT meta -> 'tags' ->> 0 AS first_tag, meta -> 'tags' ->> 1 AS second_tag FROM users;Результат:
first_tagsecond_tagnewvipЕщё PostgreSQL позволяет использовать отрицательные индексы:
-1— последний элемент;-2— предпоследний элемент.SELECT meta -> 'tags' ->> -1 AS last_tag FROM users;Это удобно, когда длина массива заранее неизвестна.
Пример с заказами: достаём артикул первой позиции
Теперь посмотрим на более жизненный пример.
Допустим, в таблице
ordersесть колонкаpayload, где хранится заказ:{ "items": [ { "sku": "A1", "qty": 2 }, { "sku": "B7", "qty": 1 } ] }Нужно достать артикул первой позиции заказа.
Путь такой:
items.sku.SQL-запрос:
SELECT id, payload -> 'items' -> 0 ->> 'sku' AS first_sku FROM orders WHERE status = 'paid';Читается почти как путь к файлу:
Только в PostgreSQL этот путь записывается стрелками.
Длинные цепочки: идём внутрь через
->, достаём значение через->>Вложенность в JSON может быть глубокой.
Например:
Чтобы достать город:
SELECT payload -> 'customer' -> 'profile' -> 'address' ->> 'city' AS city FROM orders;Обратите внимание на закономерность:
->;->>.Это хороший стиль: сначала спокойно спускаемся по JSON-структуре, а в конце достаём обычное значение.
Почему числа из JSON нужно приводить к числам
->>всегда возвращаетtext.Даже если внутри JSON лежит число, после
->>PostgreSQL отдаст его как текст.Например:
Если нужно сравнивать возраст как число, нужно явно привести тип:
SELECT (profile ->> 'age')::int AS age FROM users WHERE (profile ->> 'age')::int >= 18;Здесь
::intпревращает текст в целое число.Это особенно важно в фильтрах. Без приведения PostgreSQL может сравнивать строки, а не числа.
Например:
SELECT id FROM orders WHERE payload ->> 'amount' > '99';Такой запрос выглядит правдоподобно, но он опасен. Значение из
payload ->> 'amount'— это текст. Значит, сравнение тоже текстовое.В текстовом сравнении
'100'может оказаться меньше'99', потому что строки сравниваются посимвольно, а не как числа.Правильно так:
SELECT id FROM orders WHERE (payload ->> 'amount')::numeric > 99;Теперь сумма сравнивается как число.
Пример с агрегацией: средний чек из JSON
Допустим, сумма заказа хранится в JSON:
Нужно посчитать средний чек по странам пользователей.
SELECT u.country, round(avg((o.payload ->> 'amount')::numeric), 2) AS avg_amount FROM orders o JOIN users u ON u.id = o.user_id GROUP BY u.country ORDER BY avg_amount DESC;Здесь важно вот это место:
(o.payload ->> 'amount')::numericСначала мы достаём значение
amountкак текст, потом превращаем его вnumeric, и только после этого передаём вavg.Скобки нужны, чтобы PostgreSQL сначала выполнил извлечение из JSON, а уже потом привёл результат к числу.
Что будет, если ключа нет
Если в JSON нет нужного ключа, PostgreSQL не выдаст ошибку. Он просто вернёт
NULL.Например, в JSON нет поля
phone:Запрос:
SELECT profile ->> 'phone' AS phone FROM users;Вернёт
NULL.С одной стороны, это удобно: запрос не падает из-за отсутствующего поля. С другой — опасно. Если вы ошиблись в названии ключа, PostgreSQL тоже вернёт
NULL, и ошибку можно долго не заметить.Например:
SELECT profile ->> 'emial' AS email FROM users;Если вы случайно написали
emialвместоemail, запрос выполнится, но вернёт пустые значения.Поэтому при работе с JSON особенно важно внимательно проверять имена ключей.
Частые ловушки с
->и->>Ошибка 1. Сравнивать
jsonbс текстомВот так делать не стоит:
SELECT id FROM users WHERE prefs -> 'theme' = 'dark';Слева результат типа
jsonb, справа текстовая строка. Это разные типы.Правильно использовать
->>:SELECT id FROM users WHERE prefs ->> 'theme' = 'dark';Есть и другой вариант — сравнивать JSON с JSON:
SELECT id FROM users WHERE prefs -> 'theme' = '"dark"'::jsonb;Но для обычных фильтров по строковым значениям проще и понятнее использовать
->>.Ошибка 2. Использовать
->>слишком раноНеправильно:
SELECT profile ->> 'address' ->> 'city' AS city FROM users;После
profile ->> 'address'результат уже стал текстом. Дальше идти по нему стрелками как по JSON-объекту нельзя.Правильно:
SELECT profile -> 'address' ->> 'city' AS city FROM users;Ошибка 3. Забыть привести тип
Неправильно:
SELECT id FROM orders WHERE payload ->> 'amount' > '100';Правильно:
SELECT id FROM orders WHERE (payload ->> 'amount')::numeric > 100;Если значение должно быть числом, датой или булевым значением, не оставляйте его текстом.
Ошибка 4. Доставать объект через
->>и ждать отдельное полеЕсли применить
->>к объекту или массиву, PostgreSQL вернёт весь объект или массив как текст.Например:
SELECT profile ->> 'address' AS address FROM users;Если
address— это объект, результат будет строкой примерно такого вида:{"city": "Berlin", "zip": "10115"}Это не ошибка, но обычно это не то, что нужно. Если нужен город, нужно идти глубже:
SELECT profile -> 'address' ->> 'city' AS city FROM users;Мини-шпаргалка
->->->->>WHERE->>->>плюс приведение типа-> 0,->> 0,-> -1,->> -1Главная мысль:
->оставляет вас внутри JSON.->>достаёт значение наружу как текст.Чем PostgreSQL отличается от других баз
Синтаксис со стрелками удобный, но он не является универсальным для всех баз данных.
В PostgreSQL можно писать цепочки:
SELECT profile -> 'address' ->> 'city' AS city FROM users;В MySQL чаще используют JSON-путь одной строкой:
SELECT JSON_EXTRACT(profile, '$.address.city') AS city FROM users;Также в MySQL есть операторы для работы с JSON, но путь всё равно записывается иначе:
SELECT profile ->> '$.address.city' AS city FROM users;В ClickHouse обычно используют специальные функции, которые сразу возвращают нужный тип:
SELECT JSONExtractString(profile, 'city') AS city FROM users;Идея везде похожая: нужно достать значение из JSON по пути. Но синтаксис и типы результата отличаются.
В PostgreSQL важно хорошо понимать именно пару
->и->>, потому что на ней строится большая часть повседневной работы сJSONB.Главное
JSONBудобен, когда данные гибкие: настройки, метаданные, дополнительные параметры, вложенные списки и объекты. Но доставать значения из него нужно аккуратно.Запомните простое правило:
->возвращаетjsonb;->>возвращаетtext;->используем для промежуточных шагов;->>используем в конце, когда нужно обычное значение;->>нужно явно приводить к нужному типу;NULL.Когда это правило укладывается в голове, JSON-запросы в PostgreSQL перестают выглядеть как магия. Вы просто идёте по структуре слева направо: объект, поле, массив, элемент, вложенное поле — и в конце аккуратно достаёте нужное значение наружу.