sqlpostgresqljsonbjson

`->` и `->>` в PostgreSQL: как доставать данные из `JSONB`

Как операторы -> и ->> читают поля JSONB, достают элементы массивов, строят цепочки и требуют явного приведения типов.

7 мин чтенияСправочникsql · postgresql · jsonb · json · mysql · clickhouse

В 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;

Здесь всё логично:

  1. profile -> 'address' возвращает вложенный JSON-объект.
  2. ->> '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;

Такой запрос вернёт обычный текст:

status currency
paid EUR

Для вывода в отчёт чаще всего нужен именно ->>, потому что пользователю в таблице обычно нужен текст, а не 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
    }
  ]
}

Нужно достать артикул первой позиции заказа.

Путь такой:

  1. Зайти в поле items.
  2. Взять первый элемент массива.
  3. Из этого элемента взять поле 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 перестают выглядеть как магия. Вы просто идёте по структуре слева направо: объект, поле, массив, элемент, вложенное поле — и в конце аккуратно достаёте нужное значение наружу.

Закрепи на практике

Решай задачи в SQL-тренажёре с мгновенной проверкой и подсказками.

Открыть тренажёр