Базы данных

PostgreSQL JSONB в 2026 году: когда реляционная база заменяет MongoDB

Ruslan Ismailov Опубликовано 12 мин чтения
P

Введение: от 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

  1. Гибридная схема — золотой стандарт. Выносите в отдельные колонки поля, по которым часто фильтруете или джойните: user_id, status, created_at. Остальное — в data JSONB.
  2. Один GIN-индекс vs. множество выражений. Полный GIN-индекс удобен, но тяжёлый. Для high-load приложений создавайте индексы по конкретным выражениям: (data->>'status'), (data->>'user_id')::BIGINT.
  3. Нормализуйте «горячие» поля. Если вы обнаружили, что фильтруете по data->>'email' в 80% запросов — выносите email в отдельную колонку с обычным B-tree индексом.
  4. Версионирование документов. Добавьте поле schema_version INT рядом с data JSONB. Это позволит безопасно мигрировать структуру документов без downtime.
  5. Проверяйте планы запросов регулярно. EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) — ваш лучший инструмент. GIN-индекс может не использоваться, если статистика устарела: запускайте ANALYZE products после массовых вставок.
  6. Используйте 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. Подробнее обо мне →