MidТеория8 min

SQL vs NoSQL: глубокий разбор

ACID vs BASE, реляционная модель vs document/KV/wide-column/graph, когда и что использовать, Postgres JSONB размывает границу

Что это

SQL-БД (реляционные): PostgreSQL, MySQL, Oracle, SQL Server, CockroachDB, Spanner. Модель: таблицы + строгая схема + SQL + ACID-транзакции + JOIN.

NoSQL -- зонтичный термин. Реально это четыре разных семейства:

Семейство Примеры Модель
Document MongoDB, Couchbase, Firestore Вложенные JSON-документы
Key-Value Redis, DynamoDB, Riak Хэш-мапа
Wide-column Cassandra, ScyllaDB, HBase Sparse таблицы, partition key + clustering
Graph Neo4j, JanusGraph, Neptune Узлы и рёбра с атрибутами

Главный ментальный сдвиг: "SQL vs NoSQL" -- не один выбор, а пять разных выборов, каждый со своими компромиссами.

ACID vs BASE

Свойство ACID (SQL) BASE (многие NoSQL)
A Atomicity: всё или ничего Basically Available
C Consistency: invariants Soft state
I Isolation: параллельные транзакции Eventual consistency
D Durability: после commit данные сохранены (Durability обычно есть)

ACID -- контракт "после COMMIT всё правильно". BASE -- "данные в итоге сойдутся, а пока работай с тем что есть". Выбор ACID vs BASE -- это по сути CAP: CP vs AP. См. 06.distributed-systems/2.cap-theorem.

Важно: BASE не означает "беспорядок". Eventual consistency имеет модели (read-your-writes, monotonic reads, causal), и хорошо спроектированные BASE-системы соблюдают их.

Когда SQL лучше

Преимущества реляционной модели

  • JOIN и произвольные запросы. Не знаешь заранее, какие отчёты понадобятся -- SQL позволяет делать любые. NoSQL обычно оптимизирован под конкретный access pattern.
  • Целостность через foreign keys. БД сама не даст создать заказ без существующего пользователя.
  • ACID-транзакции через несколько таблиц. Списать с одного счёта, зачислить на другой -- атомарно.
  • Декларативные индексы и query planner. Оптимизатор сам выбирает план.
  • Зрелость ops. Бэкапы, репликация, миграции, монитор -- отлаженные 40 лет.
  • Constraints: UNIQUE, CHECK, NOT NULL -- схема-сторож.

Типичные use-cases SQL

  • Финансы и учёт (ledger, bills, invoices).
  • E-commerce: заказы, корзины, инвентарь.
  • CRM, ERP.
  • Любая система записей (source of truth).
  • Отчётность и BI поверх OLTP.

Postgres как "супер-SQL"

PostgreSQL сейчас -- not "просто SQL". Он покрывает много NoSQL кейсов:

  • JSONB с GIN-индексами -- document store внутри реляционки.
  • Arrays, hstore -- полуструктурированные данные.
  • PostGIS -- геоданные.
  • pg_trgm, tsvector -- full-text search на уровне MVP.
  • pgvector -- векторный поиск для RAG/ML.
  • LISTEN/NOTIFY -- pub/sub.
  • Partitioning -- горизонтальное разбиение.

Часто выбор "берём Mongo, потому что нужны гибкие документы" -- это просто незнание JSONB.

Когда NoSQL лучше

Document (MongoDB и друзья)

  • Гибкая схема, аггрегат как единица. Продуктовый каталог с разными атрибутами у товаров.
  • Embedded data без JOIN. Заказ + позиции одним документом -- быстрое чтение.
  • Быстрая эволюция. Меняй поля без ALTER TABLE.
  • Write-heavy для независимых документов.

Минусы: JOIN -- боль, транзакции на несколько документов появились поздно и дороги, агрегации сложнее SQL.

Key-Value (Redis, DynamoDB)

  • Простейший access pattern: get/set по ключу.
  • Кеш, сессии, rate limit, счётчики.
  • Очень высокий QPS: Redis -- сотни тысяч ops/sec на узле, DynamoDB -- миллионы.
  • Low latency: миллисекунда и меньше.

Минусы: невозможно искать по значению, нет сложных запросов, нет JOIN.

Wide-column (Cassandra, ScyllaDB)

  • Масштаб в PB.
  • Write-heavy: логи, метрики, события, time-series.
  • Tunable consistency (LOCAL_QUORUM, ONE и т.д.).
  • Linear scalability: добавь узел -- получи линейный рост.

Минусы: думать надо партициями и clustering keys заранее; JOIN нет; транзакций нет; плохие запросы = drop кластера.

Graph (Neo4j)

  • Сильно связанные данные: соцграф, рекомендации, fraud detection, knowledge graph.
  • N-hop запросы дешевле, чем рекурсивные CTE в SQL.

Минусы: мало инженеров, экосистема скромнее, дорогое железо.

Таблица: семейства vs критерии

Критерий SQL Document Key-Value Wide-column Graph
Схема Строгая Гибкая Нет Гибкая Гибкая
JOIN Да Нет / слабо Нет Нет Нативно (n-hop)
Транзакции ACID multi-row Multi-doc (поздно) Обычно single-key Lightweight / single-row Зависит
Query language SQL Query API / Aggregation GET/PUT CQL Cypher / Gremlin
Consistency default Strong Tunable / eventual Strong (single key) Tunable Strong
Масштаб записи До десятков TB Средний (шардинг) Огромный Огромный Средний
Latency p50 1-10 ms 1-10 ms < 1 ms 1-5 ms 5-50 ms
Use cases OLTP, finance Каталоги, CMS Кеш, сессии Логи, телеметрия Соцграф, реки

Реальные стеки (смесь)

Никто в проде не ограничивается одной БД. Типичный стек:

PostgreSQL    — source of truth, заказы, пользователи, деньги
Redis         — сессии, кеш, rate limit, очереди
Elasticsearch — full-text поиск, логи
ClickHouse    — аналитика OLAP
S3            — большие blob
Kafka         — event bus

Это называется polyglot persistence. Вопрос: как синхронизировать? -- через CDC (Debezium), outbox pattern, ETL.

Pattern-driven decision

Правильный вопрос не "SQL или NoSQL", а "какие у меня access patterns":

Для каждого запроса опиши:
  - ключ доступа
  - размер результата
  - частота
  - latency budget
  - consistency требования

Потом подбери БД, которая естественно обслуживает эти паттерны.

Пример:

  • "Получить пользователя по id" -- любой движок.
  • "Получить все заказы пользователя" -- SQL (FK + index) или embedded doc в Mongo.
  • "Лента событий за 7 дней с 500M событий" -- Cassandra / ClickHouse.
  • "Рекомендации друзей друзей" -- Graph.
  • "Кто купил X и Y" -- SQL или OLAP.

Sizing и scaling

Аспект SQL NoSQL
Вертикальный scale До лимитов железа (~64 vCPU, TB RAM) Нормально, но не акцент
Горизонтальный scale read Read replicas -- просто Нативно
Горизонтальный scale write Sharding -- сложно, ручной Нативно (consistent hashing)
Re-sharding Болезненно Обычно автоматически
Cross-shard transactions Распределённая БД (Spanner, CockroachDB) Обычно невозможно

Обычный путь:

  1. Одна SQL-БД → до 10k QPS / 1 TB.
  2. Read replicas → до 50k read QPS.
  3. Partitioning / sharding + Redis кеш → до 100k QPS.
  4. Переезд hot-path в NoSQL (feeds, metrics) → миллионы QPS.

Большинство продуктов никогда не выходят из пункта 1-2.

Код: одна модель, два подхода

<?php

declare(strict_types=1);

/**
 * Relational model: users, orders, order_items — three normalized tables.
 * Pros: constraints enforce integrity; rich queries; multi-row ACID.
 * Cons: reading the whole order = 3 JOINs.
 */
final class OrderRepositorySql
{
    public function __construct(private readonly \PDO $db) {}

    public function create(int $userId, array $items): int
    {
        $this->db->beginTransaction();
        try {
            $this->db->prepare(
                'INSERT INTO orders(user_id, status, created_at) VALUES (:u, :s, NOW())'
            )->execute(['u' => $userId, 's' => 'new']);
            $orderId = (int) $this->db->lastInsertId();

            $stmt = $this->db->prepare(
                'INSERT INTO order_items(order_id, sku, qty, price) VALUES (:o, :s, :q, :p)'
            );
            foreach ($items as $item) {
                $stmt->execute([
                    'o' => $orderId,
                    's' => $item['sku'],
                    'q' => $item['qty'],
                    'p' => $item['price'],
                ]);
            }
            $this->db->commit();
            return $orderId;
        } catch (\Throwable $e) {
            $this->db->rollBack();
            throw $e;
        }
    }

    public function getFull(int $orderId): array
    {
        $sql = 'SELECT o.id, o.status, u.email,
                       i.sku, i.qty, i.price
                FROM orders o
                JOIN users u ON u.id = o.user_id
                JOIN order_items i ON i.order_id = o.id
                WHERE o.id = :id';
        $stmt = $this->db->prepare($sql);
        $stmt->execute(['id' => $orderId]);
        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
    }
}
Обратите внимание: в Mongo-версии email хранится копией в ордере. Это денормализация -- плата за быстрое чтение. В SQL-версии email один, но каждое чтение ордера делает JOIN.

Матрица выбора

Требование Выбор
Финансы, инвойсы SQL
Каталог с разнотипными атрибутами SQL + JSONB / Document
Кеш, сессии Key-Value
Логи, метрики, события Wide-column / timeseries DB
Соцграф, рекомендации Graph (или SQL с CTE на малых объёмах)
Поиск полнотекстовый Elasticsearch / Postgres tsvector
OLAP отчёты ClickHouse / Redshift / BigQuery
PB-масштаб write Cassandra / Scylla
MVP, не знаешь нагрузки PostgreSQL

Правило: начинайте с PostgreSQL, добавляйте специальную БД только когда профилировано узкое место. Преждевременный выбор NoSQL -- классическая ошибка.

Выводы

  • SQL vs NoSQL -- это не один выбор, а пять разных: реляционная, документ, KV, wide-column, graph.
  • SQL: JOIN, ACID, зрелость, констрейнты; минус -- труднее масштабировать на запись.
  • Postgres с JSONB, pgvector, partitioning закрывает 80% NoSQL-кейсов.
  • NoSQL берут под конкретный паттерн доступа и масштаб, а не "на всякий случай".
  • Polyglot persistence -- норма: SQL + Redis + Elasticsearch + Kafka в одном проекте.
  • Начинать с PostgreSQL, мигрировать часть хот-пути в специализированные БД при необходимости.