PostgreSQL JSONB в 2026 году: когда реляционная база заменяет MongoDB
Введение: от hstore до JSONB
История хранения полуструктурированных данных в PostgreSQL начинается задолго до того, как NoSQL-движение стало мейнстримом. В 2006 году появился hstore — расширение для хранения пар ключ–значение в строковом формате. Оно работало, но не поддерживало вложенность и имело ограниченные возможности индексирования.
PostgreSQL 9.2 (2012) принёс тип json — текстовое хранение с базовой валидацией. Переломным моментом стал PostgreSQL 9.4 (2014), представивший jsonb — бинарное, индексируемое представление JSON. С тех пор каждый мажорный релиз добавлял новые функции: jsonb_path_query (SQL/JSON Path в 12-й версии), подписки и jsonb_set_lax в 14-й, улучшенную поддержку JSON Schema в 16-й, а в PostgreSQL 17 (2024) и грядущем 18 (2025–2026) — дальнейшую оптимизацию планировщика для JSONB-запросов.
Сегодня, в 2026 году, PostgreSQL JSONB — зрелый инструмент, закрывающий большинство сценариев, ради которых разработчики исторически тянулись к MongoDB. Разберёмся, когда это оправдано, а когда — нет.
JSONB vs JSON: ключевые отличия
В PostgreSQL существует два типа: json и jsonb. Разница принципиальная.
- json хранит данные как текст, сохраняя оригинальный порядок ключей и пробелы. Валидация происходит при вставке, но разбор — при каждом чтении. Быстрая запись, медленное чтение.
- jsonb хранит данные в бинарном формате: ключи дедуплицируются, сортируются, дубликаты удаляются. Запись чуть медленнее из-за парсинга, зато чтение и индексирование — значительно быстрее.
Правило простое: используйте json только тогда, когда вам критически важно сохранить оригинальный формат документа (например, для аудит-лога сырого запроса). В остальных случаях — всегда jsonb.
Индексирование JSONB: GIN, GiST и частичные индексы
Главное преимущество jsonb перед json — возможность эффективного индексирования. PostgreSQL предлагает несколько стратегий.
GIN-индекс: полное покрытие документа
GIN (Generalized Inverted Index) — стандартный выбор для JSONB. Он индексирует все ключи и значения документа, позволяя использовать операторы @>, ?, ?|, ?&.
-- Создание таблицы с JSONB-полем
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
data JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Полный GIN-индекс
CREATE INDEX idx_products_data_gin ON products USING GIN (data);
-- Запрос: найти все товары с тегом 'electronics'
SELECT id, data->>'name'
FROM products
WHERE data @> '{"tags": ["electronics"]}';
-- Проверка использования индекса
EXPLAIN (ANALYZE, BUFFERS)
SELECT id FROM products
WHERE data @> '{"category": "laptop"}';
GIN с jsonb_path_ops: быстрее, но уже
Оператор класса jsonb_path_ops создаёт более компактный индекс — только для оператора @>, но с меньшим размером и более высокой скоростью поиска.
CREATE INDEX idx_products_path ON products
USING GIN (data jsonb_path_ops);
-- Этот индекс используется оператором @>
SELECT * FROM products
WHERE data @> '{"specs": {"ram": 16}}';
Индекс по конкретному полю
Если вы регулярно фильтруете по одному полю, эффективнее создать B-tree индекс на выражение:
-- Индекс по полю price внутри JSONB
CREATE INDEX idx_products_price
ON products ((data->>'price')::NUMERIC);
-- Теперь этот запрос использует B-tree индекс
SELECT * FROM products
WHERE (data->>'price')::NUMERIC > 1000;
Частичный индекс
-- Индекс только для активных продуктов
CREATE INDEX idx_active_products
ON products USING GIN (data)
WHERE (data->>'status') = 'active';
Операторы и функции JSONB: практический обзор
PostgreSQL предоставляет богатый арсенал для работы с JSONB.
Операторы доступа и проверки
-- Получение значения по ключу (возвращает jsonb)
SELECT data->'specs' FROM products;
-- Получение значения как текст
SELECT data->>'name' FROM products;
-- Доступ по пути
SELECT data#>'{specs,memory}' FROM products;
SELECT data#>>'{specs,memory}' FROM products; -- как текст
-- Проверка наличия ключа
SELECT * FROM products WHERE data ? 'discount';
-- Содержит ли документ поддокумент?
SELECT * FROM products
WHERE data @> '{"brand": "Dell"}';
-- JSON Path (PostgreSQL 12+)
SELECT jsonb_path_query(data, '$.specs.ram ? (@ > 8)')
FROM products;
Модификация JSONB
-- Установка значения
UPDATE products
SET data = jsonb_set(data, '{specs,ram}', '32')
WHERE id = 1;
-- Удаление ключа
UPDATE products
SET data = data - 'old_field'
WHERE id = 1;
-- Слияние объектов (PostgreSQL 9.5+)
UPDATE products
SET data = data || '{"featured": true}'
WHERE id = 1;
-- jsonb_set_lax (PostgreSQL 14+): обработка null-пути
UPDATE products
SET data = jsonb_set_lax(data, '{discount}', 'null', true, 'use_json_null')
WHERE id = 1;
Агрегация и преобразование
-- Развернуть массив в строки
SELECT id, jsonb_array_elements(data->'tags') AS tag
FROM products;
-- Собрать строки в JSONB-массив
SELECT jsonb_agg(data->>'name') FROM products;
-- Собрать объект
SELECT jsonb_object_agg(data->>'sku', data->>'price')
FROM products;
Реальные сценарии замены MongoDB на PostgreSQL JSONB
Гибкая схема без миграций
Один из главных аргументов за MongoDB — возможность хранить документы с разной структурой. PostgreSQL JSONB решает эту задачу не хуже. Классический паттерн — гибридная схема: фиксированные поля для критически важных атрибутов и jsonb-колонка для расширяемых данных.
CREATE TABLE events (
id BIGSERIAL PRIMARY KEY,
event_type TEXT NOT NULL,
user_id BIGINT REFERENCES users(id),
occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
payload JSONB NOT NULL DEFAULT '{}'
);
-- Можно хранить события с совершенно разной структурой
INSERT INTO events (event_type, user_id, payload) VALUES
('page_view', 1, '{"url": "/home", "referrer": "google.com"}'),
('purchase', 1, '{"order_id": 42, "amount": 299.99, "currency": "USD"}'),
('signup', 2, '{"plan": "pro", "trial": true, "source": "organic"}');
Вложенные документы
MongoDB популярен в сценариях с многоуровневой вложенностью. PostgreSQL справляется с этим эффективно:
-- Найти пользователей, у которых адрес доставки в Москве
SELECT id, data->>'email'
FROM users
WHERE data @> '{"addresses": [{"city": "Moscow", "type": "shipping"}]}';
-- JSON Path для сложных условий
SELECT id
FROM orders
WHERE jsonb_path_exists(
data,
'$.items[*] ? (@.price > 100 && @.quantity > 1)'
);
Работа с массивами
-- Найти продукты с тегами 'sale' И 'new'
SELECT * FROM products
WHERE data @> '{"tags": ["sale"]}'
AND data @> '{"tags": ["new"]}';
-- Длина массива
SELECT id, jsonb_array_length(data->'images') AS image_count
FROM products
WHERE jsonb_array_length(data->'images') > 3;
Производительность: JSONB vs MongoDB в 2026 году
Объективные бенчмарки 2025–2026 годов (независимые тесты на наборах данных от 1 до 50 млн документов) показывают следующую картину.
- Точечный поиск по ключу с GIN-индексом: PostgreSQL JSONB — 0,3–0,8 мс, MongoDB — 0,2–0,6 мс. Паритет, MongoDB чуть быстрее за счёт специализированной архивектуры.
- Сложные агрегации с JOIN: PostgreSQL выигрывает на 40–60% — планировщик запросов значительно эффективнее при смешанных реляционных/JSON-запросах.
- Массовая вставка (bulk insert): MongoDB быстрее на 20–30% при отсутствии индексов; при наличии GIN-индексов разрыв сокращается до 10–15%.
- Full-scan по JSONB без индекса: сравнимые результаты, оба медленные — необходимы индексы.
- Транзакционные сценарии с JSONB + реляционные данные: PostgreSQL быстрее на 2–3x благодаря отсутствию накладных расходов на сетевое взаимодействие между сервисами.
Ключевой вывод: если ваше приложение смешивает документные и реляционные данные — PostgreSQL JSONB даёт преимущество не только в производительности, но и в операционной простоте. Один сервис вместо двух.
Интеграция с Laravel и Go
Laravel: работа с JSONB через Eloquent
Laravel поддерживает JSONB-поля нативно через механизм кастинга и операторы where.
// Миграция
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->jsonb('data')->default('{}');
$table->timestamps();
});
// Модель
class Product extends Model
{
protected $casts = [
'data' => 'array',
];
}
// Поиск по вложенному полю
$laptops = Product::whereRaw(
"data @> ?",
[json_encode(['category' => 'laptop'])]
)->get();
// Обновление отдельного ключа без перезаписи всего объекта
DB::statement(
"UPDATE products SET data = jsonb_set(data, '{specs,ram}', ?) WHERE id = ?",
['32', $product->id]
);
// GIN-индекс в миграции
DB::statement('CREATE INDEX idx_products_gin ON products USING GIN (data)');
Go: работа с JSONB через pgx
В Go библиотека pgx — де-факто стандарт для работы с PostgreSQL. JSONB-поля сканируются напрямую в структуры через интерфейс pgtype.
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/jackc/pgx/v5/pgxpool"
)
type ProductData struct {
Name string `json:"name"`
Price float64 `json:"price"`
Tags []string `json:"tags"`
Specs map[string]interface{} `json:"specs"`
}
func getProducts(pool *pgxpool.Pool) ([]ProductData, error) {
rows, err := pool.Query(
context.Background(),
`SELECT data FROM products
WHERE data @> $1
ORDER BY (data->>'price')::NUMERIC DESC
LIMIT 20`,
[]byte(`{"tags": ["sale"]}`),
)
if err != nil {
return nil, err
}
defer rows.Close()
var results []ProductData
for rows.Next() {
var raw []byte
if err := rows.Scan(&raw); err != nil {
return nil, err
}
var pd ProductData
if err := json.Unmarshal(raw, &pd); err != nil {
return nil, err
}
results = append(results, pd)
}
return results, nil
}
func updateSpec(pool *pgxpool.Pool, id int64, ram int) error {
_, err := pool.Exec(
context.Background(),
`UPDATE products
SET data = jsonb_set(data, '{specs,ram}', $1::jsonb)
WHERE id = $2`,
fmt.Sprintf("%d", ram),
id,
)
return err
}
Ограничения JSONB и когда MongoDB всё же лучше
Честный анализ требует признания ограничений PostgreSQL JSONB.
- Горизонтальное масштабирование. PostgreSQL масштабируется вертикально лучше, но горизонтальный sharding — сложнее. MongoDB Atlas или MongoDB с нативным sharding'ом выигрывает при объёмах от десятков терабайт.
- Гибкость схемы на уровне приложения. Если ваша команда исторически работает с ODM-подходом (Mongoose, Doctrine ODM), смена парадигмы требует усилий.
- Change Streams. MongoDB предоставляет нативные Change Streams для реактивных архитектур. PostgreSQL LISTEN/NOTIFY + логическая репликация закрывают 80% сценариев, но требуют дополнительной настройки.
- Очень большие JSONB-документы. Документы размером более 1 МБ в PostgreSQL хранятся через TOAST, что даёт накладные расходы. MongoDB оптимизирована для больших документов (до 16 МБ).
- Отсутствие нативного API документной базы. Если продукт требует MongoDB Wire Protocol (например, совместимость с существующими клиентами), PostgreSQL не поможет без дополнительных прокси.
Практические рекомендации по проектированию схемы с JSONB
- Гибридная схема — золотой стандарт. Выносите в отдельные колонки поля, по которым часто фильтруете или джойните:
user_id,status,created_at. Остальное — вdata JSONB. - Один GIN-индекс vs. множество выражений. Полный GIN-индекс удобен, но тяжёлый. Для high-load приложений создавайте индексы по конкретным выражениям:
(data->>'status'),(data->>'user_id')::BIGINT. - Нормализуйте «горячие» поля. Если вы обнаружили, что фильтруете по
data->>'email'в 80% запросов — выносите email в отдельную колонку с обычным B-tree индексом. - Версионирование документов. Добавьте поле
schema_version INTрядом сdata JSONB. Это позволит безопасно мигрировать структуру документов без downtime. - Проверяйте планы запросов регулярно.
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)— ваш лучший инструмент. GIN-индекс может не использоваться, если статистика устарела: запускайтеANALYZE productsпосле массовых вставок. - Используйте JSON Schema Validation (PostgreSQL 16+). Встроенная функция
jsonb_matches_schemaпозволяет валидировать документы на уровне базы без дополнительных слоёв.
-- Пример гибридной схемы
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
status TEXT NOT NULL DEFAULT 'pending',
total NUMERIC(10,2) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
metadata JSONB NOT NULL DEFAULT '{}'
);
CREATE INDEX idx_orders_user ON orders (user_id);
CREATE INDEX idx_orders_status ON orders (status);
CREATE INDEX idx_orders_meta ON orders USING GIN (metadata jsonb_path_ops);
-- Запрос эффективно использует оба индекса
SELECT id, total, metadata->>'source'
FROM orders
WHERE user_id = 42
AND status = 'completed'
AND metadata @> '{"promo": true}';
Заключение
PostgreSQL JSONB в 2026 году — это не компромисс, а полноценное решение для большинства задач, ради которых разработчики исторически выбирали MongoDB. Гибкие схемы, вложенные документы, массивы, мощное индексирование через GIN, богатый набор операторов и SQL/JSON Path делают PostgreSQL конкурентоспособным в документной нише.
Главное преимущество — единая база данных для реляционных и документных данных. Это снижает операционную сложность, упрощает транзакции и позволяет использовать мощь SQL для сложных аналитических запросов.
Вместе с тем MongoDB остаётся лучшим выбором для сценариев с нативным горизонтальным sharding'ом, Change Streams и очень большими документами. Если ваша система работает на PostgreSQL — не торопитесь вводить MongoDB «ради гибкости». Скорее всего, JSONB уже решает вашу задачу.
Интеграция через Laravel Eloquent и Go pgx — зрелая и предсказуемая. Начните с гибридной схемы, создайте правильные индексы, регулярно проверяйте планы запросов — и PostgreSQL NoSQL-возможности оправдают ожидания.
Технологии
Теги
Руслан Исмаилов
Senior Web / Backend разработчик. Senior web/backend разработчик с 9-летним опытом. Стек: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, микросервисы, CI/CD. Подробнее обо мне →