sqlpostgresqljsonbjson

JSONB Arrow Operators: -> and ->> in PostgreSQL

How -> and ->> work: read object fields and array elements, drill into nested structures by chaining, and cast the result to the right type.

4 Min. LesezeitReferencesql · postgresql · jsonb · json · mysql · clickhouse
Dieser Artikel ist derzeit auf Russisch — die englische Übersetzung ist in Arbeit.

Положили полуструктурированные данные в колонку JSONB — настройки пользователя, метаданные заказа, разнородные атрибуты, которые не хочется разносить по отдельным столбцам, — и теперь к ним нужно как-то подобраться. Любое чтение JSON в PostgreSQL начинается с двух операторов-стрелок: -> и ->>. Это рабочая лошадка JSON-запросов: первый ныряет глубже по структуре и держит вас внутри JSON, второй выныривает наружу с готовым значением. Разница между ними кажется косметической — всего один лишний символ, — но именно она решает, скомпилируется ваш запрос или упадёт с ошибкой типов. Разберём по порядку: чем они отличаются, как доставать поля объекта и элементы массива и как складывать всё это в длинную цепочку, которая читается так же легко, как путь к файлу.

-> возвращает jsonb, ->> возвращает text

Оба оператора достают значение по ключу, но возвращают его в разных типах, и в этом вся соль. -> отдаёт jsonb — объект, массив или скаляр в JSON-форме, со всеми кавычками и структурой, как есть. ->> отдаёт обычный text, уже распакованный из JSON-обёртки. Мнемоника простая: лишняя стрелка снимает кавычки. Запомните это различие — на нём держится всё остальное, от фильтров до агрегатов.

-- prefs is a jsonb column on users: {"theme": "dark", "lang": "en"}
SELECT
  prefs -> 'theme'  AS as_jsonb,   -- "dark"  (with quotes, type jsonb)
  prefs ->> 'theme' AS as_text     -- dark    (no quotes, type text)
FROM users;

Отсюда простое правило, по которому стрелку выбираешь почти не задумываясь:

  • -> — когда вы идёте глубже и следом будет ещё один -> или ->>.
  • ->> — на последнем шаге пути, когда нужно скалярное значение для вывода, фильтра или сравнения.

Ключ задаётся строкой — это имя поля объекта. Отсутствующий ключ не роняет запрос, а молча возвращает NULL. Эту тихую снисходительность стоит держать в голове: ниже она ещё аукнется.

Поля объекта и элементы массива

PostgreSQL выбирает, что именно делать, по типу правого операнда — один и тот же оператор обслуживает и объекты, и массивы. Строка справа — значит, ищем поле в объекте по имени. Целое число справа — индексируем массив, причём нумерация идёт с нуля, как в большинстве языков, а не с единицы, как привыкли любители обычного SQL-массива. Отрицательный индекс удобно отсчитывается с конца: -1 — последний элемент, -2 — предпоследний, и не нужно заранее знать длину.

-- meta = {"tags": ["new", "vip", "eu"], "score": 7}
SELECT
  meta -> 'tags' -> 0     AS first_tag_json,   -- "new" as jsonb
  meta -> 'tags' ->> 0    AS first_tag_text,   -- new  as text
  meta -> 'tags' ->> -1   AS last_tag          -- eu   (last element)
FROM users;

Ровно тот же приём работает поверх ваших собственных таблиц — никакой особой магии для «игрушечных» примеров тут нет. Пусть в orders лежит payload с массивом позиций заказа, и нам нужен артикул первой:

-- payload = {"items": [{"sku": "A1", "qty": 2}, {"sku": "B7", "qty": 1}]}
SELECT
  id,
  payload -> 'items' -> 0 ->> 'sku' AS first_sku
FROM orders
WHERE status = 'paid';

Цепочки и приведение типов

Длинный путь к вложенному значению — это просто несколько стрелок подряд, читаемых слева направо. Спускаетесь через ->, оставаясь в jsonb на каждом промежуточном шаге, а на самом последнем переключаетесь на ->>, чтобы вынуть text. Поставить ->> в середине цепочки нельзя — он отдаст текст, а к тексту следующую стрелку уже не прицепишь. И поскольку ->> всегда возвращает именно текст, для чисел, дат и булевых значений результат приходится приводить явно через ::.

-- profile = {"address": {"city": "Berlin", "zip": "10115"}, "age": "34"}
SELECT
  profile -> 'address' ->> 'city'        AS city,
  (profile ->> 'age')::int               AS age_int
FROM users
WHERE (profile ->> 'age')::int >= 18;

Чтобы это не выглядело учебным упражнением, вот живой запрос с агрегацией: вытаскиваем сумму из JSON-метаданных заказа и считаем средний чек по странам. Обратите внимание на скобки вокруг ->> перед ::numeric — без них приоритет операторов сыграет против вас.

-- payload = {"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;

Подводные камни

  • Сравнение без приведения — почти всегда баг. Выражение payload ->> 'amount' > '99' сравнивает строки лексикографически, и в этом мире '9' оказывается больше '100', потому что сравниваются символы, а не числа. Приводите явно: (payload ->> 'amount')::numeric > 99.
  • -> на скаляре отдаёт jsonb, поэтому prefs -> 'theme' = 'dark' падает с ошибкой типов — слева jsonb, справа text. Для сравнения берите ->> либо пишите prefs -> 'theme' = '"dark"'::jsonb (с двойными кавычками внутри — это валидный JSON-скаляр).
  • Несуществующий ключ или индекс за границами массива возвращает NULL молча. Опечатка в имени поля не вызовет ни ошибки, ни предупреждения — просто тихо испортит результат, и вы будете долго гадать, куда делись данные.
  • ->> на объекте или массиве вернёт его JSON-текст целиком, а не отдельное значение. Текст берите только со скаляров, иначе получите строку вида {"city": ...} вместо самого города.

Стоит помнить и о том, что синтаксис стрелок — не межплатформенный стандарт. В MySQL такого синтаксиса стрелок-цепочек нет: там пользуются JSON_EXTRACT(doc, '$.address.city') или сахаром doc->>'$.address.city' (оператор ->> тоже даёт текст, но путь записывается через JSONPath одной строкой, а не двумя стрелками подряд). В ClickHouse доступа по стрелкам нет вовсе — там работают типизированные функции вроде JSONExtractString(col, 'city') и JSONExtractInt(col, 'age'), сразу возвращающие нужный тип, без отдельного приведения через ::. Идея «достать значение из JSON по пути» универсальна, а вот лаконичные стрелки -> / ->> — фирменная черта PostgreSQL, ради которой его и любят за работу с JSON. Освоите различие между jsonb и text и привычку приводить тип на выходе — и остальной арсенал JSON-функций ляжет поверх него уже без сюрпризов.

Übe an echten Aufgaben

Löse Aufgaben im SQL-Trainer mit sofortiger Bewertung und Hinweisen.

Trainer öffnen