
Привет, Хабр! Это Антон Дятлов, инженер по защите информации в Selectel. В современных базах данных JSON — де-факто стандарт для хранения полуструктурированных сведений. PostgreSQL предлагает два специализированных типа для работы с ним: json
и jsonb
. Первый хранит точную текстовую копию документа со всеми пробелами и порядком ключей. Второй применяет оптимизированный бинарный формат.
При вставке информации в поле типа jsonb
система немедленно разбирает (парсит) текст, проверяет его корректность, удаляет дубликаты ключей и сохраняет результат в виде высокоэффективной древовидной структуры.
И вот мы сталкиваемся с фундаментальным компромиссом — между скоростью записи и чтения.
Вставка в jsonb
происходит медленнее, чем в json
— ведь требуются дополнительные вычисления. Зато расплачиваться временем приходится лишь однажды.
Обратная сторона: каждая последующая операция чтения или поиска по полю jsonb
выполняется значительно оперативнее, так как не нужно каждый раз разбирать и перепроверять, что написано в текстовой строке. Более того, jsonb
поддерживает индексацию, а это решающий фактор производительности на больших объемах информации.
Несмотря на все достоинства jsonb
, стандартные операторы PostgreSQL — @>
, ?
, ?|
, ?&
— создают так называемый «разрыв выразительности». Они прекрасно подходят для простых задач — например, проверки наличия ключа верхнего уровня или вхождения одного JSON-документа в другой.
Однако как только требуется сформулировать замысловатое условие — скажем, найти товары, у которых в массиве характеристик есть «синий» цвет и цена больше 1 000, — выражение превращается в громоздкую нечитаемую конструкцию. Разработчикам приходится прибегать к неэффективным обходным путям. В ход идут вложенные подзапросы, «разворачиванию» массивов с помощью функции jsonb_array_elements
с последующей фильтрацией, а то и прямой перебор. Такой код трудно схватывать на лету, поддерживать и, что хуже всего, он медленно работает.
Именно для того, чтобы закрыть этот разрыв, и было создано расширение jsquery. Оно предоставляет полноценный язык для формулирования выборок, который возвращает возможность писать декларативные, понятные и благодаря индексам очень быстрые инструкции для работы с jsonb
-полями.
Что такое jsquery
jsquery — расширение для PostgreSQL для поиска по данным jsonb. Появляются два ключевых компонента: собственный тип данных jsquery
и новый оператор сопоставления @@
.
В отличие от набора отдельных функций, jsquery — целостный декларативный язык. Его главная цель — предоставить удобный и эффективный синтаксис для работы с вложенными объектами и массивами. Кроме того, он предоставляет расширенный набор операторов сравнения и проверок.
Наиболее важный аспект jsquery — его глубокая интеграция в ядро PostgreSQL. Когда вы составляете команду, строка, скажем items.#.price > 100
, сначала преобразуется в специальное значение типа jsquery
. Далее СУБД парсит получившуюся последовательность символов, проверяет синтаксис и, наконец, сохраняет ее в оптимизированном внутреннем представлении. Затем указанный объект сравнивается с полем jsonb
через оператор @@
.
Такая архитектура — собственный тип данных и выделенный оператор — критически важна для быстродействия. Планировщик PostgreSQL «знает» об инструкции @@
и понимает, что ее выполнение можно ускорить с помощью GIN-индекса. В этом кардинальное отличие jsquery от пользовательских функций (UDF, User Defined Functions). Для планировщика те часто выглядят как «черный ящик», что вынуждает того прибегать к полному сканированию таблицы.
С точки зрения своего синтаксиса, jsquery вдохновлен селекторами CSS и языком запросов MongoDB, что делает его интуитивно понятным для многих разработчиков. Запросы выглядят декларативно — вы описываете, что хотите найти, а не как это сделать по шагам. В результате код становится чище, короче и проще для восприятия.
Ключевые преимущества jsquery можно свести к следующим пунктам.
Простой и мощный синтаксис, который позволяет легко описывать пути к глубоко вложенным элементам JSON-структуры с помощью точек и специальных символов.
Расширенные операторы предоставляют богатый набор для сравнения (
=
,>
,<
и др..), а также проверки типов (IS NUMERIC
,IS OBJECT
и т. д.).Поддержка GIN-индексов полностью интегрируется с GIN-индексами PostgreSQL, что гарантирует высокую производительность запросов даже при работе с таблицами, содержащими миллионы JSON-документов.
Декларативность и читаемость позволяет формулировать сложные и гибкие условия фильтрации в виде единого, логически завершенного выражения, избавляя от необходимости создавать громоздкие и трудноподдерживаемые SQL-конструкции.

Облачный сервер с криперами и порталом в Незер. Добывайте ресурсы, стройте объекты, исследуйте мир Selectel в Minecraft и получайте призы.
Установка и базовая настройка
Системные требования
jsquery — отдельное расширение, поэтому его нужно установить и подключить. Минимальная версия PostgreSQL — 9.4, именно в ней впервые появился jsonb
.
Для сборки из исходного кода или установки через некоторые пакетные менеджеры могут потребоваться заголовочные файлы для разработки PostgreSQL, которые названы в стиле postgresql-server-dev-XX
.
Установка
Проще всего получить jsquery из репозиториев, где он часто доступен в виде готового пакета. Например, для дистрибутивов на базе Debian, таких как Ubuntu или SelectOS, установка выполняется одной командой.
sudo apt install postgresql-17-jsquery
В примере выше число 17 может потребоваться заменить на актуальную версию PostgreSQL.
Активация в базе данных
После того как пакет расширения установлен на сервере, его необходимо активировать в каждой базе данных, где он будет использоваться. Делается это с помощью одной SQL-команды, выполненной от имени суперпользователя или владельца БД:
CREATE EXTENSION jsquery;
Команда выше создаст в текущей базе БД тип данных jsquery
, оператор сопоставления @@
, а также все необходимые функции и операторские классы для поддержки индексации.
Проверка
Чтобы убедиться, что расширение установлено и работает корректно, достаточно выполнить простой тестовый запрос. Он не обращается к каким-либо данным, а лишь подтверждает, что PostgreSQL распознает тип jsquery
и оператор @@
.
SELECT '{}'::jsonb @@ '{}'::jsquery;
Если команда выполняется без ошибок и возвращает булево значение — true
или false
, — значит, установка прошла успешно.
Синтаксис и команды
Чтобы эффективно применять jsquery, необходимо освоить его синтаксис, который сочетает в себе и простоту, и мощные возможности для навигации и фильтрации.
Навигация по документу
Основа любого запроса — это путь к нужному значению, который в jsquery строится интуитивно.
Доступ к вложенному ключу осуществляется через точку. Например, путь
profile.contact.email
обратится к ключуemail
, который находится внутри объектаcontact
, вложенного вprofile
.Корень документа обозначается символом
$
. Этот символ позволяет применить оператор ко всему JSON-документу как к единому значению.Длина объекта или массива получается с помощью символа
@#
в конце пути. Например,tags@# > 3
найдет документы, где массивtags
содержит более трех элементов.
Литералы и операторы
Строковые литералы в запросе должны заключаться в двойные кавычки — например,
status = "active"
.Числовые литералы указываются без кавычек —
age > 30
.
Массивы и объекты
Самая мощная часть jsquery — плейсхолдеры (wildcards), которые избавляют от необходимости писать процедурный код для обхода информационных структур.
#
— любой элемент массива. Условиеitems.#.price > 100
проверяет, есть ли в массивеitems
хотя бы один объект, у которого полеprice
больше 100. Этот плейсхолдер напрямую заменяет громоздкую конструкцию сjsonb_array_elements
.#N
— N-й элемент массива (индексация с нуля). Скажем,tags.#0 = "featured"
проверит только первый элемент массиваtags
.%
— любой ключ объекта. Выражениеversions.% = "1.0"
найдет документы, где у объектаversions
есть любое поле со значением 1.0.— произвольная вложенность. Этот плейсхолдер позволяет искать ключ на любом уровне документа. Условие
.color = "red"
найдет ключcolor
со значением «red» вне зависимости от того, насколько глубоко тот спрятан. Так удается заменить запутанные рекурсивные запросы.$
— весь JSON-документ как единое значение.@#
— длина массива или количество ключей в объекте. Используется в конце пути:items@# > 5
— вернетtrue
, если в массивеitems
больше пяти элементов.
Фильтрация значений
Есть обширный набор операций для проверки значений.
Команды сравнения —
=
,!=
,>
,<
,>=
,<=.1
— работают как с числами, так и со строками (лексикографическое сопоставление).Команды проверки типов —
IS NUMERIC
,IS STRING
,IS OBJECT
,IS ARRAY
,IS BOOLEAN
,IS NULL
— позволяют убедиться в корректности типа данных прямо в запросе. Например,status IS STRING
гарантирует, что значение ключаstatus
— строка. Заметьте, такие команды принято писать заглавными буквами.
Поиск подстроки
Для поиска фрагментов текста в значениях jsquery предлагает две инструкции, работающие на основе регулярных выражений. Важно правильно понимать их назначение:
~
— поиск с учетом регистра,~*
— поиск без учета регистра.
Например, если в поле description
может содержаться слово «Express» в разном регистре, следует задействовать оператор ~*
:
WHERE info @@ 'description ~* "express"';
Этот запрос найдет строки «Express», «express» и «EXPRESS». Если же нужен точный поиск, применяется команда ~
.
Проверка наличия ключа
Чтобы проверить, что ключ просто существует в документе, независимо от его значения, применяется специальная конструкция с плейсхолдером : foo =
.
Комбинирование условий
Отдельные выражения можно объединять с помощью логических команд AND
, OR
и NOT
. Для управления порядком выполнения используются скобки, как и в обычном SQL.
Пример комплексного запроса — в одном из примеров ниже.
Приведем несколько распространенных запросов, закрепим полученные знания по синтаксису, а также порадуемся гибкости и выразительности jsquery на практике.
Найдем все документы, у которых поле status имеет строковое значение «active».
SELECT * FROM documents WHERE data @@ 'status = "active"';
Найдем всех пользователей, которые живут в Москве. Предположим, что информация о городе хранится по пути person.address.city. Обратите внимание, как jsquery позволяет элегантно «проваливаться» вглубь JSON-структуры с помощью точечной нотации.
SELECT * FROM users WHERE profile_data @@ 'person.address.city = "Moscow"';
Найдем пользователей с именем «Иван», которые старше 30 лет.
SELECT * FROM users WHERE data @@ '(name = "Иван" AND age > 30)';
Найдем пользователей с именем «Иван» старше 30 лет или тех, у кого есть тег «vip»:
SELECT * FROM users
WHERE data @@ '((name = "Иван" AND age > 30) OR tags.# = "vip")';
Выберем товары, у которых в массиве тегов tags
есть значение «электроника». Обратите внимание: плейсхолдер #
используется для итерации по всем элементам массива без необходимости его «разворачивания».
SELECT * FROM products WHERE info @@ 'tags.# = "электроника"';
Найдем заказы, в которых хотя бы один товар (items
) стоит дороже тысячи.
SELECT * FROM orders WHERE data @@ 'items.#.price > 1000';
Выберем все документы, где поле status
является строкой, независимо от его значения.
SELECT * FROM docs WHERE info @@ 'status IS STRING';
Найдем все документы, где ключ foo
просто существует, независимо от его значения.
SELECT * FROM documents WHERE data @@ 'foo = *';
Индексация jsquery — ключ к высокому быстродействию
Декларативный синтаксис — это лишь половина успеха. Настоящая сила jsquery раскрывается при работе с GIN-индексами. Без индекса любой запрос к jsonb
-полю на большой таблице приведет к полному последовательному сканированию (Seq Scan) — PostgreSQL будет вынужден прочитать каждую строку, чтобы проверить условие. На миллионах записей такая процедура недопустимо медлительна.
Продемонстрируем это на практике. Создадим тестовую таблицу и вставим 200 000 строк со случайными ценами на вещи:
CREATE TABLE products (
id serial PRIMARY KEY,
data jsonb
);
INSERT INTO products (data)
SELECT jsonb_build_object(
'items', jsonb_agg(jsonb_build_object('price', (random() * 2000)::int))
)
FROM generate_series(1, 200000)
GROUP BY generate_series;
Запрос без индекса: базовый уровень эффективности
Выполним запрос на поиск товаров, у которых есть хотя бы один предмет дороже 1500, и посмотрим на план выполнения с помощью EXPLAIN ANALYZE
. Обратите внимание на корректный синтаксис jsquery:
EXPLAIN ANALYZE
SELECT count(*)
FROM products
WHERE data @@ 'items.#.price > 1500';
Результат покажет, что планировщик выбрал Seq Scan, а время выполнения будет измеряться сотнями миллисекунд или даже секундами на реальной системе.
Aggregate (cost=12943.33..12943.34 rows=1 width=8) (actual time=245.873..245.874 rows=1 loops=1)
-> Parallel Seq Scan on products (cost=0.00..12940.83 rows=1000 width=0) (actual time=136.438..245.615 rows=95163 loops=3)
Filter: (data @@ 'items.#.price > 1500'::jsquery)
Rows Removed by Filter: 38171
Planning Time: 0.116 ms
Execution Time: 245.924 ms
Создание GIN-индекса
Теперь создадим GIN-индекс на столбце data:
CREATE INDEX idx_products_data_gin ON products USING GIN (data);
ANALYZE products;
Запрос с индексом — скачок продуктивности
Выполним тот же самый запрос еще раз:
EXPLAIN ANALYZE
SELECT count(*)
FROM products
WHERE data @@ 'items.#.price > 1500';
План выполнения кардинально изменится. Вместо медленного Seq Scan мы увидим комбинацию Bitmap Index Scan и Bitmap Heap Scan.
Aggregate (cost=148.83..148.84 rows=1 width=8) (actual time=34.483..34.484 rows=1 loops=1)
-> Bitmap Heap Scan on products (cost=124.75..146.33 rows=1000 width=0) (actual time=16.351..34.223 rows=95163 loops=1)
Recheck Cond: (data @@ 'items.#.price > 1500'::jsquery)
Heap Blocks: exact=4483
-> Bitmap Index Scan on idx_products_data_gin (cost=0.00..124.50 rows=1000 width=0) (actual time=14.931..14.931 rows=95163 loops=1)
Index Cond: (data @@ 'items.#.price > 1500'::jsquery)
Planning Time: 0.157 ms
Execution Time: 34.532 ms
Двухэтапный процесс работает чрезвычайно эффективно. Сначала Bitmap Index Scan быстро просматривает GIN-индекс. Далее, чтобы найти все документы, удовлетворяющие условию, он создает в памяти битовую карту страниц таблицы, где находятся эти строки. Затем Bitmap Heap Scan точечно считывает с диска только нужные страницы.
Такой метод на порядки быстрее полного перебора таблицы, что и демонстрирует многократное уменьшение времени отклика.
Сравнение jsquery с альтернативами
Чтобы понять место jsquery в экосистеме PostgreSQL, полезно сравнить его со встроенными механизмами и стандартом SQL/JSON (jsonpath).
Критерий |
Встроенные операторы: |
jsquery: |
SQL/JSON: jsonpath, |
Синтаксис |
Функциональный, операторный. Требует вложенности функций для комплексных условий |
Декларативный, похож на CSS/XPath. Единое выражение для сложной логики |
Декларативный, стандартный. Синтаксис близок к JavaScript — напр. |
Возможности |
Проверка существования, вхождения, извлечение значений верхнего уровня. Ограниченный набор действий |
Широкий набор: сравнение, типы, подстроки, логика, плейсхолдеры для массивов и вложенности |
Очень гибкий: арифметика, функции для работы со строками, условные выражения внутри пути |
Индексация (GIN) |
Поддерживается, но только для ограниченного набора операторов: |
Полная и эффективная поддержка. Единый оператор |
Поддерживается через |
Сложность |
Легко для простых задач. Быстро становится громоздким и трудночитаемым для комплексных фильтров |
Требует изучения нового синтаксиса, но после этого написание сложных фильтров становится проще и быстрее |
Требует изучения стандарта SQL/JSON. Мощность может привести к сложности |
Доступность |
Встроены в ядро PostgreSQL с появления |
Расширение. Требует установки. Доступно для PostgreSQL 9.4+ |
Стандарт SQL:2016. Встроен в ядро PostgreSQL начиная с версии 12 |
Итог |
Идеально для простых, быстрых проверок и извлечения данных |
Лучший выбор для комплексных, производительных фильтров, особенно в версиях PostgreSQL до 12 |
Мощный стандарт для сложных манипуляций и фильтрации в PostgreSQL 12+ |
Встроенные инструменты остаются отличным решением для простых задач. Для замысловатых условий отбора jsquery предлагает более чистую и мощную грамматику. К примеру, jsonpath, появившийся в PostgreSQL 12 — мощная альтернатива, соответствующая стандарту SQL. Однако jsquery сильнее — в простоте применения индексов и доступности на более ранних версиях PostgreSQL.
Сценарии применения и лучшие практики
jsquery наиболее полезен в проектах, которые интенсивно работают с jsonb
и требуют частой и замысловатой фильтрации сведений.
Когда выбирать jsquery
Аналитика по логам и событиям. Когда нужно фильтровать JSON-логи по множеству вложенных параметров.
Отбор позиций в каталогах товаров. Идеально для интернет-магазинов, где у товаров есть сложные наборы атрибутов в JSON.
Работа с любыми полуструктурированными данными. Хранение пользовательских профилей, настроек, документов, где структура может варьироваться.
Проекты на PostgreSQL до версии 12. В версиях 9.4−11 jsquery — безальтернативный выбор для производительных комплексных выборок из
jsonb
.
Лучшие практики
Индекс GIN — не опция, а требование. Всегда создавайте GIN-индекс на jsonb
-столбцах, которые активно фильтруются. Без него быстродействие будет неприемлемым.
Доверяй, но проверяй — (EXPLAIN ANALYZE
). Всегда анализируйте план выполнения запроса, чтобы убедиться, что PostgreSQL использует индекс (Bitmap Scan), а не полный перебор (Seq Scan). Иногда структура команды может помешать оптимизатору — тогда ее придется переписать.
Выбирайте правильный инструмент для задачи. Не прибегайте к jsquery там, где достаточно нативного оператора. Для простого извлечения значения data -> 'name'
будет быстрее и проще. Задействуйте jsquery в чем он силен — для комплексных условий фильтрации.
Тестируйте на реальных данных. Эффективность jsquery и jsonpath (для PostgreSQL 12+) может зависеть от структуры данных и запросов. Проводите измерения на своем окружении, чтобы сделать осознанный выбор.
Заключение
jsquery — это зрелое, мощное и проверенное временем расширение, которое значительно обогащает возможности PostgreSQL при работе с jsonb
.
Несмотря на появление стандарта SQL/JSON и jsonpath, jsquery не теряет своей актуальности. Его ценность — в уникальном сочетании простой декларативной синтаксической структуры, мощных плейсхолдерах и, что самое главное, предсказуемо высокой продуктивности благодаря тесной интеграции с GIN-индексами.
Для проектов, работающих на версиях PostgreSQL до 12-й, он остается незаменимым решением. Однако и в более новых версиях он часто выигрывает за счет легкости и прозрачности. Ведь jsquery — не просто расширение, а механизм, который возвращает разработчику контроль над комплексными выборками из jsonb
, делая их чище, быстрее и проще в поддержке.
Если в БД хранятся значительные объемы неструктурированных сведений в jsonb
, то jsquery определенно заслуживает внимания.
erogov
Вот только даже 12-я версия уже не поддерживается. Зачем сейчас что-то, кроме SQL/JSON — большая загадка.
kortovea
В greenplum 6й версии стоит пг 9.6. Не знаю, можно ли на него накатить этот плагин, скорее всего можно