PostgreSQL с поддержкой JSONB — это бездонная магическая сумка разработчика: можно хранить полуструктурированные данные и при этом эффективно их фильтровать. В статье разберём реальные примеры запросов с WHERE по JSONB-полям и соберём тестовый стенд для экспериментов.
🤔 JSON vs JSONB, что это за B
Поле JSONB позволяет хранить полуструктурированные данные в формате JSON. В отличие от обычного JSON, тип JSONB хранится в бинарном виде и поддерживает индексацию. Это даёт возможность эффективно фильтровать и комбинировать данные с реляционной моделью.
🧱 Подготовка стенда
Прежде чем прыгать в запросы, создадим тестовую среду. Вот Docker-композ для PostgreSQL + скрипт генерации данных:
# docker-compose.yml
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: test
ports:
- "5432:5432"
volumes:
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
-- init.sql
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
attributes JSONB NOT NULL
);
-- Генератор тестовых данных
INSERT INTO products (name, attributes)
SELECT
'Product ' || i,
jsonb_build_object(
'price', (random() * 100)::numeric(10,2),
'tags', CASE WHEN random() > 0.5
THEN jsonb_build_array('sale', 'new')
ELSE jsonb_build_array('popular') END,
'specs', jsonb_build_object(
'weight', (random() * 10)::numeric(5,2),
'color', CASE floor(random()*4)
WHEN 0 THEN 'red'
WHEN 1 THEN 'blue'
ELSE 'black' END
)
)
FROM generate_series(1, 10000) AS i;
Описание стенда
Что хранится в attributes
Поле attributes содержит JSONB-объект с такими ключами:
| Ключ | Тип данных | Пример значения |
|---|---|---|
price |
число (decimal) | 49.75 |
tags |
массив строк | ["sale", "new"] или ["popular"] |
specs |
вложенный объект JSONB | { "weight": 3.42, "color": "red" } |
Пример одной строки
{
"price": 67.23,
"tags": ["sale", "new"],
"specs": {
"weight": 7.85,
"color": "black"
}
}
🚀 Запускаем
docker-compose up -d
psql -d postgres -h 127.0.0.1 -U postgres -W
пароль test
или через контейнер: docker ps получаем название, у меня jsonb-where-postgres-postgres-1
docker exec -it jsonb-where-postgres-postgres-1 bash
psql -U postgres
SELECT COUNT(*) FROM products;
count
-------
10000
Теперь у нас есть 10.000 товаров с разными атрибутами в JSONB.
🔍 Базовые операции WHERE с JSONB
1. Поиск по значению ключа
-- Товары красного цвета
SELECT name, attributes->'specs'->>'color' AS color
FROM products
WHERE attributes->'specs'->>'color' = 'red';
2. Фильтр по числовому значению
-- Товары дешевле 50
SELECT name, attributes->>'price' AS price
FROM products
WHERE (attributes->>'price')::numeric < 50;
3. Проверка наличия ключа
-- Товары с указанным весом
SELECT name
FROM products
WHERE attributes->'specs' ? 'weight';
🧩 Продвинутые сценарии
Поиск по массиву тегов
-- Товары с тегом 'sale'
SELECT name, attributes->'tags' AS tags
FROM products
WHERE attributes->'tags' @> '["sale"]'::jsonb;
Комбинированные условия
-- Красные товары со скидкой тяжелее 5 кг
SELECT name
FROM products
WHERE attributes->'specs'->>'color' = 'red'
AND (attributes->'specs'->>'weight')::numeric > 5
AND attributes->'tags' @> '["sale"]'::jsonb;
Использование GIN-индексов
Для ускорения поиска по JSONB:
CREATE INDEX idx_products_attributes ON products USING GIN (attributes jsonb_path_ops);
И все? в JSONB так легко с индексацией? Ага, сейчас 😅 Это самый простой пример, а там уйма своих приколов и боли! 🤯
🚀 Производительность: EXPLAIN на практике
Проверим разницу с индексом и без:
EXPLAIN ANALYZE
SELECT * FROM products
WHERE attributes @> '{"tags": ["sale"]}'::jsonb;
До индекса:
Seq Scan on products (cost=0.00..334.00 rows=4949 width=136) (actual time=0.014..7.555 rows=4969 loops=1)
После создания GIN-индекса:
Bitmap Heap Scan on products (cost=54.36..325.22 rows=4949 width=136) (actual time=0.519..3.522 rows=4969 loops=1)
Для экспериментов можно удалить индекс
DROP INDEX idx_products_attributes;
⚠️ Мы создали 10 000 строк, чтобы планировщик запросов не «ленился» использовать индекс, и вы не ленитесь делать стенд приближенный к продакшену.
💥 Антипаттерны
- Игнорирование типов
-- Плохо: неявное приведение типов WHERE attributes->>'price' < '50'; -- Хорошо: явное приведение WHERE (attributes->>'price')::numeric < 50; - Избыточные операции
Вместо цепочки->'a'->'b'->>'c'используйте#>>'{a,b,c}':-- Оптимально: WHERE attributes#>>'{specs,color}' = 'red'; - Отсутствие индексов
Поиск по JSONB без индексов на больших таблицах — путь к медленным запросам.
🤓Вывод
- JSONB в PostgreSQL — мощный инструмент, но требующий аккуратного обращения
- Используйте операторы
@>,?,->>в зависимости от сценария - Не забывайте про индексы для часто фильтруемых полей
- Тестируйте планы запросов — EXPLAIN ваш лучший друг