Базы данных Базы данных: карта трека и как выбирать хранилище под задачу
0%

Базы данных: карта трека и как выбирать хранилище под задачу

Базы данных: карта трека и как выбирать хранилище под задачу

Это вход в трек из двадцати статей. Она не пересказывает остальные, а даёт то, без чего они превращаются в каталог брендов: систему координат. После неё вы сможете про любое хранилище ответить на пять вопросов — какую модель данных оно навязывает, под какой паттерн доступа заточен его движок, какие гарантии даёт при сбое, чего стоит его эксплуатация и в какой момент оно перестанет справляться.

Главный тезис трека звучит неудобно: выбор базы данных — это на 80% выбор паттерна доступа и только на 20% выбор продукта. Инженеры спорят «Postgres или Mongo», а спорить надо о том, читаете ли вы данные по первичному ключу поштучно, диапазонами, полнотекстово или агрегируете миллиарды строк по трём колонкам. Ответ на этот вопрос детерминирует движок; название на логотипе — уже почти косметика.

1. Цена ошибки, или почему это не «просто выберем позже»

Выбор хранилища — одно из немногих архитектурных решений, которое сложно откатить. Фреймворк меняют за спринт, язык — за квартал, базу — за год, и то не всегда. Причин три.

Данные тяжёлые. Перенести 20 ТБ — это не pg_dump | psql. Это двойная запись, сверка, обратная совместимость читателей, окно переключения и план отката, который тоже надо чем-то тестировать. Разбору этого посвящена https://courses.digitable.life/post/databases/19-choosing-and-migrating/.

Модель протекает в код. Если вы выбрали документную БД и денормализовали заказ вместе с адресом доставки, то доменная логика, API-контракты и кэши построены вокруг этой формы. Смена хранилища тянет за собой смену модели, а смена модели — переписывание бизнес-логики.

Гарантии протекают в продукт. Система, построенная поверх eventual consistency, содержит компенсирующие механизмы: идемпотентные обработчики, версии, разрешение конфликтов, «данные обновятся в течение минуты» в UI. Переезд на строгую консистентность эти механизмы не удалит — они уже вросли.

Отсюда практический вывод: решение надо обосновывать письменно и заранее, в формате ADR, где явно записаны паттерн доступа, ожидаемый объём, требуемые гарантии и условия, при которых решение подлежит пересмотру. Шаблон — в разделе 12.

2. Пять осей, на которых живёт любое хранилище

Прежде чем смотреть на продукты, зафиксируем оси. Их ровно пять, и любая честная сравнительная таблица — это проекция на эти оси.

Ось 1. Модель данных. Что база считает «единицей»: кортеж в отношении, документ, пара ключ-значение, строка в широкой колоночной таблице, вершина и ребро, вектор. Модель определяет, какие вопросы задавать легко, а какие — через боль. Реляционная модель (https://courses.digitable.life/post/databases/01-relational-model/) уникальна тем, что она не оптимизирована под конкретный вопрос: вы храните факты нормализованно, а вопрос формулируете в момент чтения. Все остальные модели торгуют эту гибкость на скорость конкретного паттерна.

Ось 2. Паттерн доступа. Точечное чтение по ключу, диапазонный скан, полнотекстовый поиск, агрегация по колонкам, обход графа, поиск ближайших соседей. И отдельно — соотношение чтений и записей, и размер горячего множества относительно RAM.

Ось 3. Гарантии. Атомарность и изоляция (https://courses.digitable.life/post/databases/07-transactions-and-isolation/), консистентность при репликации, durability при потере узла, поведение при сетевом разделении (https://courses.digitable.life/post/databases/09-nosql-landscape/).

Ось 4. Масштабирование. Вертикально до какого предела, горизонтально — по чтению, по записи, автоматически или руками, что происходит при решардинге (https://courses.digitable.life/post/databases/08-replication-and-sharding/).

Ось 5. Эксплуатация и стоимость. Кто дежурит ночью, сколько стоит лицензия и железо, насколько предсказуемы апгрейды, есть ли на рынке люди с этим опытом. Эта ось убивает больше проектов, чем первые четыре вместе.

3. Физика под капотом: почему пределы именно такие

Маркетинг обещает «масштабируется бесконечно». Физика говорит иначе, и понимание физики — это то, что отличает осознанный выбор от выбора по популярности.

3.1 Лестница задержек

Все решения в устройстве БД вырастают из одного факта: разница в скорости между уровнями памяти — порядки величины. Порядки (по мотивам Latency Numbers Every Programmer Should Know Джеффа Дина, актуализированные под современное железо):

Операция Порядок задержки Во сколько раз медленнее L1
Обращение к L1-кэшу ~1 нс 1x
Обращение к RAM ~100 нс ~100x
Случайное чтение с NVMe SSD ~20–100 мкс ~10⁴–10⁵x
Случайное чтение с HDD ~5–10 мс ~10⁷x
Round-trip внутри дата-центра ~0.5 мс ~5·10⁵x
Round-trip между регионами ~50–150 мс ~10⁸x

Отсюда три следствия, объясняющие почти всё в этом треке.

Следствие A: индекс — это способ не читать диск. B-tree высотой 4 при страницах 8 КБ адресует сотни миллионов строк за 4 обращения, и первые 2–3 уровня почти всегда в кэше. Без индекса — full scan, то есть линейное чтение всего объёма (https://courses.digitable.life/post/databases/06-indexes-and-query-plans/).

Следствие B: синхронная репликация между регионами стоит минимум один RTT на коммит. 100 мс между Европой и США — физика скорости света в оптоволокне, её не «оптимизируют». Именно поэтому геораспределённые системы со строгой консистентностью (Spanner, CockroachDB) имеют принципиально другой профиль латентности записи, чем одно-региональные — https://courses.digitable.life/post/databases/18-newsql-and-distributed/.

Следствие C: если рабочее множество влезает в RAM, почти любая база быстрая. Различия между движками начинают проявляться ровно там, где данные перестают помещаться в память. Бенчмарк на 10 ГБ данных при 64 ГБ RAM не измеряет ничего, кроме качества сетевого стека.

3.2 Строки против колонок

Строчное и колоночное хранение: одни и те же данные, разные вопросы

Это первая большая развилка. Строчное хранение кладёт кортеж целиком в одну страницу — идеально, когда вы работаете с сущностью целиком (SELECT * FROM orders WHERE id = 1042, затем UPDATE). Колоночное раскладывает каждый атрибут в отдельный файл — идеально, когда вы читаете 3 колонки из 40 по 200 миллионам строк.

Разница не в процентах, а в разах. Аналитический запрос на строчном хранилище поднимает с диска весь кортеж, включая 39 ненужных колонок; на колоночном — один файл, где однородные значения жмутся в 5–20 раз (Delta + LZ4 для чисел, словарное кодирование для низкокардинальных строк). Отсюда типичный разрыв в 10–100x на агрегациях — https://courses.digitable.life/post/databases/13-clickhouse-and-olap/.

Обратная сторона: точечный UPDATE в колоночном хранилище означает переписывание блоков во всех колонках, поэтому ClickHouse и подобные либо не поддерживают полноценный UPDATE, либо реализуют его как асинхронную мутацию. Колоночная база — не «быстрый Postgres», это база для другого класса вопросов.

3.3 B-tree против LSM

Сравнение B-tree и LSM-tree: обновление на месте против дозаписи

Вторая развилка — как движок кладёт данные на диск. B-tree обновляет страницу на месте: чтение предсказуемо, запись случайная и усиленная (одна изменённая строка = записанная страница целиком плюс WAL). LSM-tree только дозаписывает: запись уходит в memtable в памяти и сбрасывается пачками, но чтение может потребовать проверки нескольких уровней SSTable, а фоновый compaction создаёт всплески I/O и latency.

Практическое правило: write-heavy нагрузка с точечными чтениями по ключу → LSM; read-heavy с диапазонами и честными апдейтами → B-tree. Каноническое изложение — глава 3 «Designing Data-Intensive Applications» Мартина Клеппмана (книга), лучший однотомник по теме на сегодня.

4. Честное сравнение классов

Теперь таблицы. Читайте колонку «Когда НЕ брать» первой — она информативнее остальных.

4.1 Модель, гарантии, масштабирование

Класс Модель данных Транзакции Консистентность по умолчанию Горизонтальное масштабирование
Реляционные (Postgres, MySQL) отношения, схема на записи полноценные ACID, multi-statement строгая на лидере; реплики — асинхронные чтение — реплики; запись — только шардинг руками
NewSQL (CockroachDB, Spanner, TiDB) отношения, SQL-совместимость распределённый ACID, serializable строгая (linearizable) автоматический решардинг, запись масштабируется
Документные (MongoDB) JSON-документы, схема на чтении ACID внутри документа; multi-doc — с 4.0, дороже настраиваемая (readConcern/writeConcern) встроенный шардинг по ключу
Key-value (Redis) ключ → структура данных атомарность команды, MULTI, Lua-скрипт одно-узловая; кластер — без кросс-слот транзакций слоты в кластере, ребалансировка
Wide-column (Cassandra, Scylla) таблица с partition key + clustering лёгкие транзакции только через Paxos, дорого tunable (ONE/QUORUM/ALL) линейное, безмастерное, лучшее в классе
Аналитические (ClickHouse) колонки, широкие таблицы практически нет; вставки атомарны по блоку eventual между репликами шардирование по ключу, distributed-таблицы
Поисковые (Elasticsearch) инвертированный индекс + документы нет near-real-time (refresh ~1 с) шардирование индексов
Графовые (Neo4j) вершины и рёбра как first-class ACID (в Neo4j — да) строгая в кластере слабое место класса
Векторные (Qdrant, Milvus) вектор + метаданные обычно нет eventual шардирование коллекций
Объектные (S3, MinIO) объект целиком по ключу атомарная замена объекта; read-after-write strong read-after-write (S3 с 2020) практически неограниченное

4.2 Эксплуатация и стоимость

Класс Порог входа Типичная боль в проде Модель стоимости Когда НЕ брать
PostgreSQL низкий vacuum и bloat, wraparound, долгие транзакции, connection storm без пулера железо + люди; лицензия 0 $ запись выше возможностей одного узла; чистая аналитика на терабайтах
MySQL / MariaDB низкий репликационный лаг, DDL на больших таблицах, различия форков железо + люди; лицензия 0 $ (GPL) нужны сложные типы, оконные функции старых версий, богатые расширения
MS SQL / Oracle средний лицензионный аудит, привязка к вендору, стоимость ядра лицензия по ядрам — десятки тысяч $ на сервер стартап без бюджета; нагрузка, где ядер надо много, а фич — мало
SQLite минимальный один писатель, отсутствие сетевого доступа 0 $ многопользовательская запись через сеть
MongoDB низкий неудачный shard key, распухшие документы, рост индексов Atlas дорого масштабируется; on-prem SSPL нужны сложные джойны и строгая схема; финансовые инварианты
Redis низкий OOM и вытеснение, потеря данных при падении, big keys, hot slot RAM — самая дорогая память на гигабайт primary storage для данных, которые нельзя потерять
Cassandra / Scylla высокий tombstones, ремонт (repair), перекос партиций, JVM-паузы много узлов, дорогая эксплуатация меньше нескольких ТБ; нужны ad-hoc запросы и джойны
ClickHouse средний мутации, слияния, кардинальность ключа партиционирования дёшево на терабайт, дорого на «много мелких запросов» OLTP, точечные апдейты, высокая конкуренция коротких запросов
Elasticsearch средний сплит-брейн в старых версиях, mapping explosion, heap лицензия ELv2/SSPL; RAM-ёмкий как источник истины; как обычная БД
Neo4j средний масштаб записи, стоимость Enterprise Enterprise-лицензия графы, которые прекрасно живут в рекурсивном CTE Postgres
Векторные низкий пересборка индекса, память под HNSW, качество recall RAM-ёмкие < 1 млн векторов — хватит pgvector

4.3 Ориентиры производительности

Числа ниже — порядки величины на типичном узле (современный x86, NVMe, данные не влезают в RAM целиком), а не обещания. Всегда меряйте на своих данных; но если ваши ожидания расходятся с этой таблицей на порядок — вы что-то недопоняли о своей нагрузке.

Операция PostgreSQL Redis Cassandra ClickHouse
Точечное чтение по PK, p50 0.1–1 мс 0.05–0.2 мс 0.5–2 мс плохо приспособлен
Точечная запись, p50 0.5–2 мс (fsync) 0.05–0.2 мс 0.5–2 мс вставка только пачками
Пропускная способность записи, узел 5–50 тыс/с 100+ тыс/с 20–100 тыс/с 100 тыс – 1 млн строк/с (батчами)
Агрегация по 100 млн строк минуты нет нет 0.1–2 с
Полнотекстовый поиск приемлемо (GIN) нет нет ограниченно

Отдельно — про пропускную способность записи в Postgres. Ограничитель почти всегда fsync при коммите WAL. Отсюда классический приём: батчить записи в одну транзакцию. Разница между 1000 отдельных INSERT и одним INSERT на 1000 строк — обычно 20–50x, и это лечит больше «медленных баз», чем любой тюнинг конфигов.

5. CAP, PACELC и что на самом деле выбирают

CAP-теорема — самая цитируемая и самая неправильно понятая вещь в области. Формулировка Гилберта и Линча (оригинал, 2002): при сетевом разделении система не может быть одновременно консистентной и доступной. Ключевое слово — при разделении. Это не меню «выберите два из трёх», которое действует всегда: разделение — редкое событие, и CAP говорит только о поведении в этот момент.

Куда полезнее PACELC Дэниела Абади (статья, 2012): если Partition, выбирай между Availability и Consistency; Else — между Latency и Consistency. Второй половиной вы платите каждый день, а не раз в год. Синхронная репликация на кворум — это плюс один сетевой round-trip на каждый коммит, всегда, а не только при аварии.

Практический перевод для архитектора:

Что вы объявили Что это значит в коде Пример
CP при разделении часть узлов отвечает ошибкой, клиент повторяет etcd, Spanner, CockroachDB, Mongo с majority
AP при разделении все узлы отвечают, потом разрешают конфликты Cassandra с ONE, Redis-кластер, DynamoDB (настраиваемо)
EL — низкая латентность читаете с ближайшей реплики и видите устаревшее любая асинхронная read-replica, включая Postgres
EC — консистентность читаете с лидера или с кворума, платите RTT SELECT ... FOR UPDATE, readConcern: linearizable

Самая частая ошибка на этой оси — не выбрать AP или CP, а не заметить, что выбор уже сделан за вас. Асинхронная реплика Postgres, с которой читает ваше приложение, — это eventual consistency, со всеми последствиями: пользователь сохранил профиль, перезагрузил страницу, увидел старые данные, написал в поддержку. Лечится либо read-your-writes через чтение с лидера для «своих» запросов, либо ожиданием LSN.

6. Процедура выбора

Теперь соберём всё в воспроизводимую процедуру. Она сознательно начинается не с продукта, а с вопроса, который вы задаёте данным.

Три комментария к схеме, без которых она вредна.

PostgreSQL как дефолт — это не вкусовщина, а математика рисков. Он покрывает реляционную модель, JSONB-документы, полнотекстовый поиск, гео (PostGIS), временные ряды (TimescaleDB), векторы (pgvector) и очереди (SKIP LOCKED) — достаточно хорошо для подавляющего большинства нагрузок. «Достаточно хорошо в одной системе» почти всегда дешевле, чем «отлично в пяти системах», потому что пять систем — это пять моделей отказа, пять графиков апгрейдов и пять наборов знаний в дежурной смене. Детали — https://courses.digitable.life/post/databases/02-postgresql/.

Пороги в ромбе — ориентиры, а не законы. Одиночный Postgres на хорошем железе спокойно живёт с десятками терабайт и десятками тысяч записей в секунду; известны инсталляции много больше. Уходить в распределённые системы надо не когда «стало много данных», а когда упёрлись в конкретный измеренный предел и исчерпали дешёвые способы его отодвинуть: партиционирование, архивирование холодных данных, вынос blob-ов в объектное хранилище (https://courses.digitable.life/post/databases/16-object-storage/), пулер соединений, реплики для чтения.

Специализированное хранилище добавляется рядом, а не вместо. Elasticsearch, ClickHouse, векторная база — это обычно производные представления, а источник истины остаётся в реляционной БД. Такая асимметрия резко упрощает жизнь: производное можно перестроить из источника, значит его бэкапы, миграции и аварии дешевле.

7. Один вопрос — три модели

Абстракции становятся понятны на конкретике. Задача: «показать последние 20 заказов пользователя с позициями».

Реляционная модель. Факты хранятся один раз, форма ответа собирается в момент запроса.

-- Схема: нормализованная, инварианты выражены ограничениями.
CREATE TABLE orders (
    id          bigserial PRIMARY KEY,
    user_id     bigint      NOT NULL REFERENCES users(id),
    status      text        NOT NULL CHECK (status IN ('new','paid','shipped','cancelled')),
    total_cents bigint      NOT NULL CHECK (total_cents >= 0),
    created_at  timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE order_items (
    order_id    bigint  NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
    sku         text    NOT NULL,
    qty         int     NOT NULL CHECK (qty > 0),
    price_cents bigint  NOT NULL,
    PRIMARY KEY (order_id, sku)
);

-- Ключевой индекс: составной, порядок колонок = порядок фильтрации и сортировки.
CREATE INDEX idx_orders_user_created ON orders (user_id, created_at DESC);

-- Запрос: сначала отбираем 20 заказов, потом подтягиваем позиции.
-- Джойн ДО лимита читал бы все позиции всех заказов пользователя.
WITH recent AS (
    SELECT id, status, total_cents, created_at
    FROM orders
    WHERE user_id = $1
    ORDER BY created_at DESC
    LIMIT 20
)
SELECT r.id, r.status, r.total_cents, r.created_at,
       i.sku, i.qty, i.price_cents
FROM recent r
JOIN order_items i ON i.order_id = r.id
ORDER BY r.created_at DESC, i.sku;

План выполнения — то, что надо уметь читать (подробно в https://courses.digitable.life/post/databases/06-indexes-and-query-plans/):

Sort  (cost=214.8..215.3 rows=180 width=64) (actual time=0.412..0.418 rows=173 loops=1)
  ->  Nested Loop  (cost=0.85..208.1 rows=180 width=64) (actual time=0.031..0.352 rows=173 loops=1)
        ->  Limit  (cost=0.43..21.9 rows=20 width=32) (actual time=0.019..0.041 rows=20 loops=1)
              ->  Index Scan using idx_orders_user_created on orders
                    Index Cond: (user_id = 42)          -- индекс отработал: без Filter
        ->  Index Scan using order_items_pkey on order_items i
              Index Cond: (order_id = r.id)             -- 20 быстрых точечных обращений
Planning Time: 0.21 ms
Execution Time: 0.44 ms

Что здесь важно: Index Scan вместо Seq Scan, Index Cond вместо Filter (значит условие ушло в индекс, а не проверяется после чтения), и расхождение между rows=180 в оценке и rows=173 фактически — небольшое, то есть статистика адекватна. Расхождение в 100+ раз — главный признак того, что планировщик выберет неверный алгоритм соединения.

Документная модель. Форма ответа фиксируется в момент записи.

// Один документ = один заказ вместе с позициями: чтение — одно обращение.
{
  _id: ObjectId("..."),
  userId: 42,
  status: "paid",
  totalCents: 129900,
  createdAt: ISODate("2026-07-10T12:00:00Z"),
  items: [
    { sku: "KB-01", qty: 1, priceCents: 99900 },
    { sku: "MS-07", qty: 2, priceCents: 15000 }
  ]
}

// Запрос тривиален и очень быстр — ровно под этот паттерн.
db.orders.find({ userId: 42 })
         .sort({ createdAt: -1 })
         .limit(20);
db.orders.createIndex({ userId: 1, createdAt: -1 });

Цена: вопрос «сколько всего продано SKU KB-01 за месяц» требует агрегации с $unwind по всем документам; смена цены SKU не имеет единого места для правки; а инвариант «сумма позиций равна total» теперь обязанность приложения, а не базы. Подробный разбор компромиссов — https://courses.digitable.life/post/databases/10-mongodb/.

Wide-column. Модель диктуется запросом буквально: сначала пишете запрос, потом таблицу под него.

-- Cassandra: partition key = userId (все заказы пользователя на одном узле),
-- clustering key = createdAt DESC (данные физически лежат уже отсортированными).
CREATE TABLE orders_by_user (
    user_id     bigint,
    created_at  timestamp,
    order_id    uuid,
    status      text,
    total_cents bigint,
    items       frozen<list<tuple<text,int,bigint>>>,
    PRIMARY KEY ((user_id), created_at, order_id)
) WITH CLUSTERING ORDER BY (created_at DESC, order_id ASC);

-- Единственный дешёвый запрос: точно тот, под который спроектирована таблица.
SELECT * FROM orders_by_user WHERE user_id = 42 LIMIT 20;

Нужен доступ по order_id? Заводите вторую таблицу orders_by_id и пишете в обе. Это не костыль, а дизайн-принцип класса: денормализация и дублирование записи в обмен на линейное масштабирование и отсутствие координации — https://courses.digitable.life/post/databases/12-cassandra-and-wide-column/.

8. Цена полиглотной персистентности

Как только хранилищ становится больше одного, появляется проблема, которую недооценивают все и всегда: атомарно записать в две системы нельзя. Ни ретраями, ни «мы же в транзакции».

Три правила, которые следуют из этой картинки и стоят дороже любых бенчмарков.

  1. Один источник истины на факт. У каждого факта ровно одно место, где он «настоящий». Всё остальное — производные проекции, которые можно перестроить с нуля. Если перестроить нельзя, у вас не проекция, а второй источник истины и гарантированный будущий инцидент.
  2. Никогда не пишите в две системы в одном обработчике. Пишите в одну атомарно (вместе с outbox-записью), остальное — асинхронно через релей или CDC (Debezium читает WAL). Паттерн описан у Криса Ричардсона.
  3. Потребители обязаны быть идемпотентными. Доставка at-least-once означает дубликаты. Идемпотентность — через версию документа, natural key или дедупликацию по event id.

9. Позиционирование: чем платите за что получаете

Диаграмма читается по диагонали: чем правее продукт (богаче язык запросов), тем труднее ему быть высоко (масштабировать запись), потому что произвольный запрос требует координации между узлами. NewSQL сидит выше и правее остальных именно потому, что платит за это латентностью записи — распределённый консенсус на каждый коммит.

10. Типичные ошибки: симптом → диагноз → что делать

Собрано из реальных инцидентов; каждая строка встречалась не по одному разу.

Симптом в проде Настоящая причина Что делать
«База тормозит», CPU в полке, запросы простые нет индекса под самый частый предикат, Seq Scan на большой таблице pg_stat_statements → топ по total_exec_timeEXPLAIN (ANALYZE, BUFFERS)
Таблица растёт, диск кончается, строк не прибавилось bloat: dead tuples не вычищены, autovacuum не успевает настроить autovacuum агрессивнее, найти долгие транзакции, pg_repack
«Всё встало» при 500 подключениях каждое соединение Postgres — процесс; нет пулера PgBouncer в transaction mode, лимит на пул в приложении
Redis внезапно потерял ключи достигнут maxmemory, сработала политика вытеснения явно задать maxmemory-policy, разделить кэш и данные по инстансам
Cassandra: чтения деградировали за месяцы tombstones от массовых удалений, перекос партиции пересмотреть модель удалений, TTL, следить за размером партиции
Mongo: один шард горячий, остальные скучают shard key с низкой кардинальностью или монотонный hashed shard key или составной; смена ключа — дорого, думать заранее
Elasticsearch: OOM после релиза mapping explosion — динамические поля из пользовательских данных dynamic: strict, отдельное поле-объект flattened
ClickHouse: вставки тормозят, «too many parts» вставляют по одной строке вместо батчей батчи от тысяч строк, async_insert, буферные таблицы
Реплика отстала на часы, отчёты врут долгий запрос на реплике конфликтует с применением WAL hot_standby_feedback, отдельная реплика под аналитику
Пользователь не видит только что сохранённое чтение с асинхронной реплики (read-your-writes нарушен) «свои» чтения — с лидера, либо ожидание LSN
Миграция схемы заблокировала прод на 40 минут ALTER TABLE с перезаписью и полной блокировкой безопасные DDL, lock_timeout, pt-online-schema-change/gh-ost

Отдельная категория — ошибки выбора, а не эксплуатации:

  • «Взяли NoSQL ради масштаба» при 50 ГБ данных. Заплатили отсутствием джойнов и транзакций за масштаб, который не понадобился. Это самая дорогая и самая частая ошибка в отрасли.
  • «Взяли Kafka как базу данных». Лог — не хранилище с произвольным доступом; запрос «дай состояние объекта X» требует свёртки всего лога или отдельного стейт-стора.
  • «Elasticsearch как источник истины». Он не транзакционный и допускает потерю при определённых сценариях отказа; это поисковый индекс, а не БД.
  • «Redis как основное хранилище без анализа durability». appendfsync everysec означает окно потери до секунды, а RDB-снапшот — до минут. Для сессий приемлемо, для платежей нет (https://courses.digitable.life/post/databases/11-redis/).
  • «Микросервис = своя база» доведённое до абсурда. Двенадцать сервисов с двенадцатью Postgres — это ещё нормально; двенадцать сервисов с шестью разными СУБД — это дежурство, которое никто не выдержит.

11. Стоимость владения: считаем честно

Лицензия — обычно не главная статья. Порядок статей в TCO на горизонте трёх лет:

Статья Доля Комментарий
Люди (эксплуатация, дежурства, экспертиза) 40–60% доминирует почти всегда; растёт линейно с числом разных СУБД
Инфраструктура (RAM, диски, сеть, реплики) 20–40% RAM дороже всего на гигабайт; отсюда цена Redis и Elasticsearch
Лицензии 0–30% 0 $ для Postgres/MySQL; десятки тысяч за ядро для Oracle
Миграции и техдолг 10–20% скрытая статья; проявляется при апгрейдах мажорных версий

Пара практических ориентиров.

Managed или self-hosted. Управляемый сервис (RDS, Cloud SQL, Atlas) обычно дороже железа в 1.5–3 раза, и почти всегда дешевле, чем инженер, который умеет чинить репликацию в три часа ночи. Self-hosted оправдан при большом масштабе, специфических требованиях (расширения, версии, размещение) или регуляторных ограничениях.

Стоимость на терабайт различается на порядки. Данные в объектном хранилище — единицы долларов за ТБ в месяц; на NVMe в облаке — сотни; в оперативной памяти — тысячи. Отсюда очевидный, но редко применяемый приём: разложить данные по температуре. Горячее — в RAM/OLTP, тёплое — в колоночную БД, холодное — в S3/Parquet (https://courses.digitable.life/post/databases/16-object-storage/). Одно только вынесение архивных данных из основной БД часто даёт больше, чем любой тюнинг.

12. ADR: как зафиксировать выбор

Решение, не записанное в трёх абзацах, через год превратится в «так исторически сложилось». Минимальный шаблон:

# ADR-014: Хранилище для сервиса заказов

## Контекст
- Главный паттерн доступа: чтение заказов по user_id, последние N, с позициями (~95% запросов).
- Второй паттерн: поиск по номеру заказа в админке (~4%).
- Объём: 300 ГБ сейчас, +150 ГБ/год. Пик записи: 1200 заказов/с.
- Требования: строгая консистентность (деньги), инвариант «сумма позиций = total».
- Ограничение: в команде нет опыта эксплуатации распределённых БД.

## Решение
PostgreSQL 17, партиционирование orders по created_at помесячно,
составной индекс (user_id, created_at DESC). Реплика для админки и отчётов.

## Альтернативы и почему нет
- MongoDB: удобная форма документа, но инвариант суммы уходит в приложение,
  а денег в тестах на кросс-документные транзакции нам не хватит.
- Cassandra: не нужен её масштаб (1200 w/s — это ~2% от возможностей одного
  узла Postgres), а цена эксплуатации несопоставима с выигрышем.

## Условия пересмотра
- Пик записи превысил 15k/с, ИЛИ объём превысил 5 ТБ,
  ИЛИ p99 записи стабильно > 50 мс при исчерпанном вертикальном масштабировании.

## Последствия
- Аналитика в основной БД запрещена; для неё — CDC в ClickHouse (ADR-021).
- Требуется PgBouncer с первого дня.

Обратите внимание на секцию условий пересмотра с числами. Она превращает архитектурное решение из религии в инженерную гипотезу с критерием фальсификации.

13. Карта трека

Рекомендованные маршруты:

  • Полный, по порядку — если строите фундамент. Статьи 01, 06, 07, 08 обязательны для всех, даже если вы «нереляционщик»: изоляция и репликация универсальны.
  • Прикладной бэкенд — 01 → 02 → 06 → 07 → 11 → 19.
  • Аналитика и данные — 01 → 06 → 13 → 14 → 16, дальше в трек https://courses.digitable.life/post/data-engineering/00-overview/.
  • Распределённые системы — 07 → 08 → 09 → 12 → 18.
  • AI-приложения — 02 → 15, дальше https://courses.digitable.life/post/ai-engineering/00-overview/.

Все статьи трека:

# Статья О чём
01 https://courses.digitable.life/post/databases/01-relational-model/ отношения, нормальные формы, SQL как декларативный язык
02 https://courses.digitable.life/post/databases/02-postgresql/ MVCC, vacuum, индексы, расширения, эксплуатация
03 https://courses.digitable.life/post/databases/03-mysql-and-mariadb/ InnoDB, репликация, где форки разошлись
04 https://courses.digitable.life/post/databases/04-ms-sql-and-oracle/ корпоративный контур, лицензии, специфика
05 https://courses.digitable.life/post/databases/05-sqlite-and-embedded/ где встраиваемая БД сильнее сервера
06 https://courses.digitable.life/post/databases/06-indexes-and-query-plans/ B-tree, hash, GIN, GiST, покрывающие индексы, EXPLAIN
07 https://courses.digitable.life/post/databases/07-transactions-and-isolation/ ACID, уровни изоляции, аномалии, блокировки
08 https://courses.digitable.life/post/databases/08-replication-and-sharding/ физическая и логическая репликация, failover, шардинг
09 https://courses.digitable.life/post/databases/09-nosql-landscape/ таксономия, CAP, BASE, границы применимости
10 https://courses.digitable.life/post/databases/10-mongodb/ документная модель, агрегации, транзакции, грабли
11 https://courses.digitable.life/post/databases/11-redis/ структуры данных, персистентность, кластер, кэш-паттерны
12 https://courses.digitable.life/post/databases/12-cassandra-and-wide-column/ query-first моделирование, кворумы, compaction
13 https://courses.digitable.life/post/databases/13-clickhouse-and-olap/ MergeTree, партиционирование, проекции
14 https://courses.digitable.life/post/databases/14-timeseries-and-search/ TimescaleDB, InfluxDB, Elasticsearch, OpenSearch
15 https://courses.digitable.life/post/databases/15-vector-databases/ pgvector, Qdrant, Milvus, HNSW, метрики близости
16 https://courses.digitable.life/post/databases/16-object-storage/ S3, MinIO, слои хранения, lakehouse
17 https://courses.digitable.life/post/databases/17-graph-and-keyvalue/ Neo4j, Dgraph, etcd, RocksDB, LMDB
18 https://courses.digitable.life/post/databases/18-newsql-and-distributed/ CockroachDB, YugabyteDB, TiDB, Spanner
19 https://courses.digitable.life/post/databases/19-choosing-and-migrating/ сводное сравнение и техника миграции

14. Чек-лист перед выбором

Пройдите его до того, как открывать сравнение продуктов. Если на большинство пунктов ответ «не знаю» — вы выбираете не базу, а лотерейный билет.

  • Записан главный паттерн доступа одной фразой и его доля в трафике.
  • Известен объём сейчас и оценка роста на 2–3 года.
  • Известны пиковые RPS на чтение и на запись отдельно.
  • Определено, какое горячее множество и влезает ли оно в разумный объём RAM.
  • Явно решено, где нужна строгая консистентность, а где допустима eventual — и что видит пользователь.
  • Записаны инварианты данных и решено, кто их обеспечивает: база или приложение.
  • Проверено, что бюджет ошибки/потери данных совместим с durability-настройками кандидата.
  • Оценена эксплуатация: кто дежурит, есть ли опыт, как выглядит восстановление из бэкапа (и оно проверено).
  • Посчитан TCO на 3 года, включая людей, а не только лицензию.
  • Записано условие пересмотра решения — с числами.

Если после чек-листа кандидатом остаётся PostgreSQL — это, скорее всего, правильный ответ, а не отсутствие воображения.

15. Источники

Книги:

  • Martin Kleppmann, Designing Data-Intensive Applications — основной ориентир по моделям данных, репликации, транзакциям и распределённым системам. Если читать одну книгу — эту.
  • Abraham Silberschatz, Henry Korth, S. Sudarshan, Database System Concepts — учебник, полный текст доступен свободно; фундамент реляционной теории и внутренностей СУБД.
  • Alex Petrov, Database Internals — B-tree, LSM, страничная организация, консенсус.
  • Markus Winand, SQL Performance Explained — индексы и планы, лучшее введение в тему; онлайн-версия бесплатна.

Статьи и документация:

Что дальше

Дальше — фундамент, на котором стоит вся отрасль: как из наблюдений о мире получаются отношения, зачем нужна нормализация и почему SQL, несмотря на возраст и странности, оказался самым живучим интерфейсом к данным.

Реляционная модель, нормализация и SQL как язык

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов