JSON и JSONB: операторы и индексация

Рейтинг: 72.2% · 10 голосов
Подробный курс по PostgreSQL: SQL по демобазе, типы данных и JSONB, индексы, транзакции и MVCC, VACUUM, EXPLAIN и планировщик, роли и привилегии, репликация, бэкап, настройка под нагрузку и под 1С. Актуально на 2026.
Ответить
Аватара пользователя
Egor_DBA
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

JSON и JSONB: операторы и индексация

Сообщение Egor_DBA »

Оглавление курса (47)
  1. Что такое PostgreSQL: история, философия, где применяется
  2. Урок 1. Что нового в PostgreSQL 15 и далее (15 -> 16 -> 17)
  3. Установка и первый запуск: Linux, Windows, Docker
  4. psql детально: метакоманды и работа в консоли
  5. Демобаза Авиаперевозки Postgres Pro: структура и развёртывание
  6. SELECT: проекция, фильтрация, сортировка
  7. Транзакции и ACID
  8. Уровни изоляции транзакций: Read Committed, Repeatable Read, Serializable
  9. Соединения таблиц (JOIN)
  10. Агрегация: GROUP BY, HAVING, GROUPING SETS
  11. Подзапросы и CTE (WITH), рекурсия
  12. Оконные функции
  13. Изменение данных: INSERT/UPDATE/DELETE, UPSERT, MERGE
  14. Числовые и булевы типы, NULL и трёхзначная логика
  15. Строки, текст, дата и время, таймзоны
  16. Массивы, диапазоны, enum, составные типы
  17. JSON и JSONB: операторы и индексация (вы здесь)
  18. DDL: CREATE TABLE, схемы, ALTER
  19. Ограничения целостности
  20. Представления и материализованные представления
  21. Последовательности, IDENTITY, генерируемые столбцы
  22. Индексы: B-tree и когда индекс не используется
  23. Типы индексов: Hash, GiST, SP-GiST, GIN, BRIN
  24. Продвинутые индексы: частичные, по выражению, покрывающие
  25. Обслуживание индексов и таблиц: bloat, REINDEX, CONCURRENTLY
  26. Полнотекстовый поиск
  27. Роли и пользователи
  28. Привилегии: GRANT/REVOKE
  29. Row-Level Security и безопасность данных
  30. Подключение приложения: pg_hba.conf, SSL, пулы соединений
  31. Серверное программирование: функции и процедуры
  32. Триггеры и события
  33. Архитектура PostgreSQL: процессы и память
  34. MVCC: версии строк и видимость
  35. VACUUM, autovacuum, freeze и wraparound
  36. WAL, контрольные точки и долговечность
  37. Блокировки и взаимоблокировки
  38. Планировщик запросов
  39. EXPLAIN и EXPLAIN ANALYZE: чтение планов
  40. Конфигурация сервера: ключевые параметры
  41. Настройка под нагрузку и под 1С
  42. Параллельные запросы и партиционирование
  43. Резервное копирование и восстановление
  44. Репликация: потоковая и hot standby
  45. Логическая репликация и кластерные решения
  46. Внешние данные, расширения и сертификация
  47. Разбор планов запросов на explain.tensor.ru: глубокое чтение EXPLAIN ANALYZE
Урок 16. JSON и JSONB: операторы и индексация

Бывает, что заранее неизвестна точная форма данных: у одного товара пять атрибутов, у другого пятнадцать, а завтра добавят ещё три. Городить под это десяток колонок или таблицу ключ-значение неудобно. Здесь и пригождается postgresql со встроенной поддержкой JSON: можно хранить полуструктурированный документ прямо в ячейке, искать по нему и даже индексировать. Фактически это NoSQL внутри обычной реляционной СУБД. В этом уроке разберём, чем json отличается от jsonb, какие есть операторы и функции, как работает язык путей jsonpath и как ускорить выборку через GIN-индекс. И, что не менее важно, когда документ лучше разложить на нормальные таблицы.

Изображение

Как это работает

В PostgreSQL есть два типа данных под документы: json и jsonb. Тип json хранит исходный текст байт в байт - с пробелами, порядком ключей и даже дублями ключей. При каждом обращении его приходится разбирать заново, поэтому он быстрый на запись, но медленный на чтение и поиск.

Тип jsonb (буква b - от binary) при вставке разбирает документ один раз и складывает в компактном двоичном виде. Пробелы теряются, ключи сортируются, дубликаты схлопываются (остаётся последний). Запись чуть дороже, зато любой доступ к полю и сравнение работают без повторного парсинга. На практике в 2026 году почти всегда берут именно jsonb - он индексируется и поддерживает оператор вхождения. Тип json оставляют, только когда нужно сохранить документ ровно как пришёл.

Доступ к содержимому идёт через операторы. Стрелка -> достаёт элемент как json/jsonb (по ключу для объекта или по индексу для массива), а ->> возвращает то же самое, но как text. Разница принципиальная: -> можно продолжать цепочкой дальше вглубь, а ->> даёт готовую строку для сравнения и приведения типов. Оператор #> идёт по пути-массиву сразу на несколько уровней, #>> - то же, но результат текстом.

Для поиска ключевой оператор - вхождение @>. Он проверяет, содержится ли правый документ в левом как поддокумент. Оператор ? спрашивает, есть ли в объекте такой ключ верхнего уровня; ?| и ?& - есть ли любой или все ключи из списка. Именно эти операторы умеет ускорять GIN-индекс, поэтому они и важны.

SQL и примеры

Соберём документ из строк демобазы Авиаперевозки. Поле contact_data в таблице tickets как раз имеет тип jsonb:

Код: Выделить всё

SELECT ticket_no,
       passenger_name,
       contact_data ->> 'phone' AS phone,
       contact_data ->> 'email' AS email
FROM tickets
WHERE contact_data ? 'email'
LIMIT 5;
Здесь ->> вытаскивает значения как текст, а условие ? 'email' оставляет только билеты, где ключ email вообще присутствует.

Конструкторы собирают JSON прямо в запросе. jsonb_build_object лепит объект из пар ключ-значение, а jsonb_agg сворачивает группу строк в массив:

Код: Выделить всё

SELECT f.flight_no,
       jsonb_build_object(
         'departure', f.departure_airport,
         'arrival',   f.arrival_airport,
         'seats',     jsonb_agg(s.seat_no ORDER BY s.seat_no)
       ) AS flight_doc
FROM flights f
JOIN seats s ON s.aircraft_code = f.aircraft_code
WHERE f.flight_no = 'PG0001'
GROUP BY f.flight_no, f.departure_airport, f.arrival_airport;
Получаем один документ на рейс с вложенным массивом мест - удобно отдавать в API без лишних JOIN на стороне приложения.

Теперь jsonpath - мини-язык запросов внутри документа (появился в PG12, расширялся дальше). Функция jsonb_path_query вынимает значения по выражению, а оператор @@ проверяет предикат:

Код: Выделить всё

SELECT ticket_no
FROM tickets
WHERE contact_data @@ '$.phone starts with "+7"';
Выражение $.phone starts with "+7" истинно для билетов с российским номером. Знак $ - это корень документа.

Главное про скорость - индекс. Без него @> и ? читают всю таблицу. GIN-индекс по jsonb эти операторы ускоряет:

Код: Выделить всё

CREATE INDEX idx_tickets_contact ON tickets USING gin (contact_data);

SELECT ticket_no
FROM tickets
WHERE contact_data @> '{"phone": "+70127117011"}';
По умолчанию GIN индексирует и ключи, и значения и поддерживает операторы @> ? ?| ?&. Если поиск только по вхождению @>, возьмите класс операторов jsonb_path_ops - индекс выйдет компактнее и быстрее, но без поддержки ?:

Код: Выделить всё

CREATE INDEX idx_tickets_contact_path
  ON tickets USING gin (contact_data jsonb_path_ops);
А если фильтр всегда по одному и тому же полю, дешевле обычный B-tree по выражению:

Код: Выделить всё

CREATE INDEX idx_tickets_email
  ON tickets ((contact_data ->> 'email'));
Проверяйте план через explain analyze - он покажет, подхватился ли GIN (Bitmap Index Scan) или ушёл в Seq Scan.

Частые грабли
  • Путают -> и ->>. Сравнение contact_data -> 'phone' = '+7...' не сработает: слева jsonb, справа text. Для сравнения нужен ->>.
  • Берут json вместо jsonb и потом удивляются, почему нельзя построить GIN-индекс и не работает @>. Индексация и вхождение - это про jsonb.
  • Ждут, что GIN ускорит ->> или сортировку по полю. GIN заточен под @> ? ?| ?&, для извлечения одного поля нужен B-tree по выражению.
  • Кладут в jsonb то, что строго структурировано и активно джойнится. Документ нельзя связать внешним ключом, а апдейт одного поля переписывает всю ячейку целиком.
  • Сравнивают числа как текст: ->> возвращает строку, поэтому '9' окажется больше '100'. Приводите явно через (... ->> 'n')::int.
  • Забывают, что jsonb теряет порядок ключей и дубликаты. Если важен исходный байтовый вид документа (например, подпись), нужен тип json.
  • Делают GIN-индекс по всему большому документу, когда ищут по одному полю - индекс распухает. Часто выгоднее индекс по выражению.
Мини-лаба

1. Создайте таблицу: CREATE TABLE products (id int generated always as identity primary key, attrs jsonb);
2. Вставьте 5-6 строк с разным набором ключей, например {"color":"red","size":42,"tags":["sale","new"]} и {"color":"blue","weight":1.2}.
3. Выберите все товары, где attrs @> '{"color":"red"}'. Затем те, где есть ключ weight через attrs ? 'weight'.
4. Достаньте первый тег: attrs #>> '{tags,0}'. Сравните с attrs -> 'tags' -> 0 и поймите разницу типов.
5. Запустите EXPLAIN ANALYZE на запросе с @> до индекса - увидите Seq Scan.
6. Создайте CREATE INDEX ... USING gin (attrs) и повторите EXPLAIN ANALYZE: должен появиться Bitmap Index Scan.
7. Через jsonb_path_query найдите товары с числовым size больше 40: запрос '$ ? (@.size > 40)'.

Контрольные вопросы
  • Чем jsonb отличается от json при хранении и при поиске, и почти всегда выбирают именно jsonb?
  • В чём разница между -> и ->>, и почему она важна при сравнении значений?
  • Какие операторы ускоряет GIN-индекс по jsonb и какой это даёт тип сканирования в плане?
  • Когда стоит взять класс jsonb_path_ops, а когда обычный GIN или B-tree по выражению?
  • Что делают jsonb_build_object и jsonb_agg, и зачем собирать документ прямо в SQL?
  • По каким признакам понять, что данные пора нормализовать в таблицы, а не держать в одном jsonb-поле?
👍3 ❤️4 🔥3 😄 🤔2
Аватара пользователя
lash22
Сообщения: 1
Зарегистрирован: 20 май 2026, 10:51

Re: JSON и JSONB: операторы и индексация

Сообщение lash22 »

А если поле jsonb апдейтить часто - оно реально каждый раз переписывает всю ячейку? У меня таблица с настройками юзера, дёргаю один ключ по сто раз в день, теперь думаю выносить в отдельную колонку.
👍 ❤️ 🔥 😄 🤔
Аватара пользователя
pg2003
Сообщения: 1
Зарегистрирован: 13 май 2026, 04:43

Re: JSON и JSONB: операторы и индексация

Сообщение pg2003 »

Поймался на ->> с числами: сортировал size как текст и 100 уходило перед 9. Добавил ::int в приведение и норм, спасибо что предупредили.
👍1 ❤️ 🔥1 😄 🤔1
Ответить
← Предыдущая глава
Массивы, диапазоны, enum, составные типы
Следующая глава →
DDL: CREATE TABLE, схемы, ALTER

Все главы курса «PostgreSQL: от первого запроса до продакшена»

Поделиться темой: ✈ Telegram VK
Похожие запросы: частичный и покрывающий индекс в postgresql когда нуженjsonb в postgresql операторы и индексацияпартиционирование таблиц postgresql по датематериализованное представление postgresql как обновлятьpostgresql какой индекс выбрать btree gin gist brin

Вернуться в «PostgreSQL: от первого запроса до продакшена»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 2 гостя