Job 2026 md

title: Статья 04 — Модели данных и выбор хранилища source: подготовлено 2026-09-17, TASK-45.44; каркас — brief.md §04; развёртка knowledge-base.md §3–3.1 (мосты в §5–6); источники — DDIA 2-е изд. гл. 3–4 (TASK-45.39), Alex Xu гл. 5–6 (TASK-45.38), эталоны classic-designs.md §1–3


Статья 04: модели данных и выбор хранилища

Четвёртая статья серии — про главный контент этапа 4 («детали + БД», 18 минут секции): какие классы хранилищ существуют, как выбирать между ними и как они устроены внутри. Критерий «умею, если» из брифа: для любого кейса называю класс хранилища + 2 аргумента за и 1 против. Обратите внимание на форму критерия: не «назвать продукт» и не «назвать класс» — назвать класс и обосновать трейдоффом. «Возьмём Cassandra» без гарантий и критериев — ошибка №5 методички, и это самый частый способ провалить именно этот этап.

Место раздела в каркасе: статья 03 (03-scaling.md) закончилась словами «стейт — в правильных местах» — эта статья отвечает, какие это места; статья 05 (05-replication-sharding.md) — как выбранные хранилища переживают рост и отказы. Связка с соседями: KV-класс и кэш — родня, но кэш как слой — отдельный раздел 06; событийная синхронизация проекций — материал раздела 07; гарантии согласованности, которыми мы платим за выбор — раздел 09; поведение хранилищ при отказах — раздел 08. Глубина — ../knowledge-base.md §3–3.1; первоисточник обеих половин статьи — DDIA гл. 3 (модели) и гл. 4 (движки).

1. Что на самом деле выбираем: модель, гарантии, масштаб

Вопрос «SQL или NoSQL?» поставлен неправильно — и это первая фраза, которую стоит произнести на секции. Выбираем не лагерь, а пакет из трёх решений:

  • Модель данных — в какой форме живут данные: таблицы со связями, документы-агрегаты, пары ключ-значение, граф, столбцы. Модель определяет, какие запросы естественны (один оператор), а какие — мучительны (ручные циклы в приложении). DDIA: приложение — это слои моделей (объекты кода → модель данных → байты на диске), и каждый слой задаёт словарь следующего.
  • Гарантии — транзакции, изоляция, целостность: строгие у реляционных СУБД (ACID, schema-on-write, ограничения и внешние ключи), ослабленные у большинства NoSQL (eventual consistency, schema-on-read). Это мост в раздел 09.
  • Масштаб и характер нагрузки — read или write, точечные или range-запросы, какой объём и RPS: здесь выбор решают не «религии», а устройство движков (§5) и данные оценок (статья 02).

Классическая пара трейдоффов внутри «модели»: schema-on-write против schema-on-read. Реляционная схема — это защита: испорченную запись в неё не вставить, ошибка ловится в момент записи. Гибкая схема документных БД — это скорость эволюции, но ошибки структуры переносятся в рантайм и становятся ответственностью приложения. Формула ответа: «гибкую схему беру там, где данные реально вариативны (каталог, контент, события разных типов), и не беру там, где домен чёткий и за данные платят деньгами».

И обязательная оговорка про конвергенцию: противостояние давно размылось — в PostgreSQL живёт полноценный JSONB, Mongo научился multi-document-транзакциям, managed-слои приносят шардирование в SQL. Поэтому «у нас реляционная СУБД» больше не означает «не масштабируется», а «у нас NoSQL» не означает «нет транзакций». Спорят не тезисы, а конкретные трейдоффы — их и называем.

2. Классы хранилищ: карта с критерием «2 за — 1 против»

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

Класс Представители За ×2 Против ×1 Кейсы
Реляционная PostgreSQL, MySQL ACID-транзакции и ограничения целостности; сложные запросы и JOIN, зрелость экосистемы масштаб записи упирается в одну ноду, шардинг — руками деньги, заказы, связи сущностей, источник правды; до единиц ТБ
Документная MongoDB, Couchbase агрегат «документ = объект домена» читается одним куском (локальность); гибкая схема JOIN и связи между агрегатами слабые, транзакции между документами — боль профили, каталоги, контент с вариативной структурой
Key-value Redis, DynamoDB O(1) по ключу, гигантский RPS на ноду; горизонтальный масштаб и TTL нет JOIN и сложных запросов; Redis — память (дорого, ограничен объём) сессии, счётчики, фичи-флаги, кэш, hot path по известному ключу
Wide-column Cassandra, HBase, Bigtable write-throughput на последовательных записях (LSM); равномерное партиционирование по ключу, линейный масштаб модель заточена под запросы по ключу партиции; ad-hoc-запросы и точечные обновления — плохо приём событий, истории сообщений, write-heavy временные ряды
Колоночная (аналитика) ClickHouse, BigQuery агрегации по миллиардам строк со сканом столбцов; сжатие в разы не для OLTP: нет точечных UPDATE, транзакции слабые логи, аналитика, отчёты, event-store «для чтения»
Поисковая Elasticsearch, OpenSearch инвертированный индекс: полнотекст, морфология, fuzzy, фасеты не источник правды — eventual-проекция; дорогая запись поиск по тексту, автодополнение, лог-поиск
Time-series Prometheus, VictoriaMetrics, InfluxDB retention и даунсемплинг из коробки; быстрые оконные агрегаты специализированная модель — только время как ось метрики, мониторинг, телеметрия
Графовая Neo4j index-free adjacency: траверсы связей N-го порядка за постоянное время на шаг нужна редко; массовые обходы большого графа дороги соцграф-запросы, рекомендации, fraud-цепочки
Объектная S3, GCS, MinIO почти бесконечный масштаб и дешевизна; durability 11 девяток высокая латентность, нет запросов по содержимому медиа, бэкапы, стейджинг для batch
Event log Kafka append-only лог: реплей истории, retention, десятки МБ/с на партицию не queryable, порядок гарантирован только внутри партиции интеграция сервисов, event sourcing, конвейеры

Три уточнения, которые отличают уверенный ответ:

  • Wide-column и «колоночная» — разные классы, несмотря на похожие слова. Wide-column (Cassandra) — про запись потока строк с гибким набором колонок, семейство KV; колоночная (ClickHouse) — про чтение агрегатов, хранение по столбцам. «Колоночный» в первом случае — модель строки, во втором — физическая раскладка байтов.
  • KV и wide-column — ближайшие родственники: оба про «ключ → данные», оба на LSM-движках (§5). Отличие в модели: у KV значение непрозрачно, у wide-column внутри ключа — отсортированный набор колонок, что даёт range-запросы внутри партиции (история диалога — канонический кейс).
  • Графовая БД — самая редкая покупка. Запрос «друзья друзей» в реляционной СУБД — один JOIN и работает нормально; графовая оправдана, когда связи — сами данные и глубина траверсы не ограничена (рекомендации, цепочки мошенничества). На секции честнее сказать «соцграф держу в шардированной реляционной по user_id, а глубинные обходы — отдельно» — это и есть трейдофф-мышление, а не подбор продукта под слово из задания.

3. Таблица «кейс → хранилище»

Сводная таблица для этапа 4: типовой кейс из секций уровня «инстаграм/твиттер» → класс → чем обосновываем. Формат каждой строки — готовый ответ по критерию: класс + за + против.

Кейс в дизайне Класс / представитель Два «за» Один «против» (и что платим)
Заказы, платежи, кошельки Реляционная, PostgreSQL ACID + ограничения целостности; JOIN и сложные отчёты write-масштаб одной ноды — шарды руками (мост в 05)
Сессии, фичи-флаги, rate-limit счётчики KV, Redis O(1) и TTL из коробки; сотни тысяч RPS на ноду данные в памяти: дорогой объём, обязательны TTL и eviction
Кэш лент, hot-путь чтения KV, Redis-кластер латентность в микросекундах; consistent hashing масштабирует кластер кэш может врать — нужна инвалидация (раздел 06)
Профиль пользователя, каталог товаров Документная, MongoDB агрегат одним чтением; гибкая схема под вариативные поля связи между агрегатами и JOIN — в приложении
Посты, статьи (источник правды) Реляционная, шардированная по росту целостность и предсказуемые запросы; 70 ТБ/год — шардами живут спокойно горячий путь чтения мимо SQL — нужен кэш-слой (06)
Истории сообщений чата Wide-column, Cassandra (или реляционная по conversation_id) write-heavy на LSM; range по времени внутри партиции диалога вторичные запросы мимо ключа партиции — scatter-gather
Приём событий, логи, метрики приёма Wide-column / колоночная, Cassandra / ClickHouse последовательная запись на LSM; сжатие столбцов нет точечных UPDATE, спайки компактации (§5)
Аналитика и отчёты по миллиардам строк Колоночная, ClickHouse скан и агрегат столбцов с сжатием; SQL-совместимость не для точечных обновлений — питаем из конвейера
Полнотекстовый поиск, автодополнение Поисковая, Elasticsearch инвертированный индекс: морфология, fuzzy, фасеты проекция с лагом и дорогой записью — не источник правды
Метрики мониторинга продукта Time-series, VictoriaMetrics retention/даунсемплинг автоматом; оконные агрегаты специализированная модель — не кладём туда домен
Фото, видео Объектная, S3 + CDN дешевизна и durability; разгрузка всего остального латентность — лечим CDN; метаданные — отдельно в RDBMS
Интеграция сервисов, fan-out публикации Event log, Kafka реплей и retention; порядок в партиции не queryable — читающие проекции строим отдельно

Правило чтения таблицы: один и тот же сервис почти всегда занимает несколько строк — это нормально и правильно. Лента из эталона — пять хранилищ сразу (§7).

4. Источник правды один: полихранилищная архитектура

Как только хранилищ становится больше одного, появляется главный риск этапа 4 — и ошибка №10 методички («не рассмотреть частичные отказы хранилищ»). Каноническая архитектура, которой отвечаем:

Источник правды один — реляционная СУБД. Всё остальное — производные проекции с известным лагом.

  • Проекция устарела или потерялась → она перестраивается из источника (пересев кэша, реиндекс поиска, пересчёт read-модели). Деградация латентности, не потеря данных — это витрина раздела 08.
  • Синхронизация проекций — не «как-нибудь реплицируется», а событийная: outbox-паттерн + CDC (транзакция в источнике пишет событие в тот же коммит, конвейер раскладывает по проекциям; идемпотентность потребителя обязательна — мост в раздел 07, гарантии — в 09).
  • Лаг каждой проекции называется числом: поиск отстаёт на секунды, кэш — на миллисекунды, аналитика — на минуты. «Известный лаг» — это SLO, а не приписка.

Формулировка на секцию целиком: «источник правды — PostgreSQL: деньги и связи; hot-путь — Redis-зеркало с инвалидацией по событиям; поиск — Elasticsearch, реиндексируется из источника; аналитика — ClickHouse, питается конвейером. Расхождение проекций ловим мониторингом лага, восстанавливаем пересевом из источника». Одна фраза — и видно, что этап 4 у вас в руках.

5. B-tree vs LSM: «а как внутри вашей БД?»

Вопрос-зонд этапа 4, отличающий «назвал продукт» от «понимаю хранилище». Оба семейства — OLTP-движки, ответ держится на одной антитезе: обновлять на месте против писать только добавлением.

5.1 Прародитель: append-only лог

Простейшая БД — файл, в который записи дописываются в конец (пример DDIA: две bash-функции db_set/db_get). Запись O(1) и молниеносная (последовательная), но чтение — линейный скан. Вся история движков — ответ на вопрос «как искать по логу быстро»: hash-индекс в памяти (нет range), а дальше — два великих ответа: LSM и B-tree.

5.2 LSM-tree: запись как последовательные сегменты

LSM (RocksDB, Cassandra, HBase, LevelDB): запись попадает в memtable (отсортированная структура в памяти) → при заполнении сбрасывается на диск как SSTable — иммутабельный отсортированный файл со sparse-индексом и компрессией блоков → фоновая компактация сливает сегменты как mergesort. Удаление — tombstone (маркер, зачистка при компактации); Bloom filter отсекает чтения отсутствующих ключей. Стратегии компактации: size-tiered (сливаем равные свежие) vs leveled (уровни по размеру) — компромисс write-амплификации и пространства.

Что получаем: запись — только последовательная, отсюда высокий write-throughput и дешёвая компрессия. Что платим: чтение старого ключа может требовать обхода нескольких SSTable; range-запросы слабее (Bloom диапазоны не ускоряет); компактация конкурирует за IO с запросами → латентность-спайки и backpressure при недогоне.

5.3 B-tree: страницы и обновление на месте

B-tree (PostgreSQL, MySQL, почти вся классика): данные в страницах фиксированного размера (4–16 КиБ), обновление на месте, поиск за O(log n) — на практике глубина 3–4 уровня, чего хватает на сотни терабайт при ветвлении ~500. Заполнение страницы → page split; надёжность записи — WAL: сначала журнал с fsync, потом страница. Вариант copy-on-write (LMDB) даёт снапшоты без блокировок.

Что получаем: предсказуемое точечное чтение (всегда 3–4 уровня) и сильные range-запросы. Что платим: write-амплификация — одна логическая запись тянет WAL + перезапись страницы; случайные записи дороже последовательных.

5.4 Сравнение и выбор

LSM B-tree
Запись быстрее: только последовательные сегменты, ниже write-амплификация дороже: WAL + страница целиком на месте
Чтение старый ключ — несколько SSTable; спайки от компактации предсказуемо: 3–4 уровня страницы
Range слабее (Bloom не помогает) сильно (лист — отсортированный диапазон)
Компрессия отличная (иммутабельные сегменты) скромнее
Фон компактация ест IO → джиттер латентности ровнее, но page split + WAL
Где живёт Cassandra, HBase, RocksDB, LevelDB, ClickHouse-семейство PostgreSQL, MySQL, большинство RDBMS

Нюанс, который любят слушать: на SSD последовательная запись всё равно быстрее случайной (erase block ~512 КиБ, GC контроллера), а write-амплификация изнашивает flash — поэтому LSM на flash-нодах особенно уместен.

Формула выбора: write-heavy поток (приём событий, истории, метрики) → LSM-семейство; читающий профиль с range-запросами и транзакциями (домен, деньги) → B-tree-классика. И связка с уже выбранными классами: это не два независимых решения — «Cassandra» из §2 автоматически означает LSM с его спайками, «PostgreSQL» — B-tree с его записью. На секции фраза «выбор класса у меня совпал с выбором движка: write-heavy кейс → LSM-семейство» закрывает зонд одним махом.

6. Индексы: платим записью за чтение

Индекс — любая дополнительная структура, которая существует только чтобы ускорить чтение, оплаченная замедлением записи и местом. Отсюда оба правила разом: индексы строим под access pattern (иначе зачем платим) и не строим «на всякий случай» (каждый лишний — налог на каждый INSERT/UPDATE).

Минимальный словарь, который нужен на секции:

  • Кластерный (первичный) — сам порядок хранения данных: по ключу строки лежат физически (InnoDB — кластерный по PK). Определяет, что range-запрос по этому ключу читает последовательно.
  • Вторичный — отдельная структура: ключ индекса → ссылка на строку. Hash — только equality-lookup; B-tree — и точечные, и диапазоны, и сортировку. По умолчанию в RDBMS — B-tree.
  • Составной — по нескольким колонкам с правилом крайнего левого префикса: индекс (user_id, created_at) обслуживает «все посты пользователя по времени», но не «всё за сегодня по всем пользователям». Это правило прямо отвечает, почему индексы проектируют от запросов.
  • Покрывающий — содержит все колонки запроса: чтение не идёт к строке вообще (index-only scan). Честная цена: индекс толще, запись дороже.

В распределённых системах (мост в статью 05, там развёрнуто) вторичные индексы делятся на local — каждый шард ищет сам, запрос по всем шардам (scatter-gather, дорого) — и global — индекс сам пошардирован по ключу индекса, поиск в один прыжок, но отстаёт от записи и страдает от hot spot'ов. Формула та же: «частые вторичные запросы → global ценой лага; редкие → local + scatter-gather».

И связка с лестницей масштабирования статьи 03: индексы — первая ступень, дешевейший способ ускорить чтение до всяких реплик, кэшей и шардов. «Сначала посмотрел планы и индексы» — зрелая фраза; «сразу поставим кэш» — нет.

7. Прикладное: лента и чат — критерий брифа

Применяем всё выше к двум сервисам-эталонам (../classic-designs.md §2–3). Это готовый скелет этапа 4: каждое хранилище схемы — классом, представителем и обоснованием.

Лента новостей (инстаграм-класс) — пять хранилищ, каждое по своей строке таблицы §3:

  • Посты — реляционная, шардированная при росте (70 ТБ/год — оценка из статьи 02): источник правды, целостность, запрос «пост по ID» — по ключу шарда. Против — горячий путь чтения мимо SQL: поэтому дальше кэш.
  • Ленты — KV, Redis-кластер по user_id через consistent hashing: лента = ключ user_feed:<id>, микросекунды на горячем пути. Против — кэш может врать: инвалидация по событиям (раздел 06), пересев из источника.
  • Соцграф — отдельная реляционная БД по user_id: горячий запрос постинга «подписчики автора» — одним шардом; глубинные рекомендации не на горячем пути (там, если дойдут, — графовое хранилище или офлайн-обходы).
  • Медиа — S3 + CDN, метаданные — в той же реляционной БД постов.
  • Поиск и статистика — Elasticsearch как проекция + ClickHouse для аналитики, питание через outbox/CDC (§4).

Формула этапа 4: «пять хранилищ — потому что пять access pattern'ов; источник правды один, остальные — проекции с лагом».

Мессенджер — спорный кейс, на нём показывают зрелость:

  • Сообщения — write-heavy история с range-чтением «диалог по времени» → либо wide-column (Cassandra: партиция = conversation_id, история внутри партиции, LSM тянет поток записей), либо реляционная с шардами по conversation_id (транзакции и предсказуемость ценой ручного шардинга). Называть оба варианта и трейдофф — сильнее, чем выбрать один: LSM даёт запись, но спайки и слабые вторичные запросы; B-tree даёт транзакции, но write-масштаб строим шардами.
  • Онлайн-доставка — не хранилище вовсе: stateless-шлюзы + реестр user_id → gateway_id в Redis с TTL (отдельно от истории); непрочитанные/счётчики — Redis (TTL, O(1)).

8. Ошибки этого раздела

Подмножество топ-10 ошибок (../methodology.md §4), относящееся к выбору хранилища:

Ошибка Как выглядит Противоядие
«Возьмём Cassandra» (ошибка №5) Продукт назван, гарантии и критерии — нет Формат брифа: класс + 2 за + 1 против (§1–2)
«NoSQL, потому что масштаб» Без чисел; read-heavy кейс на репликах SQL живёт спокойно Сначала оценки (статья 02), потом класс; «влезает в одну ноду до единиц ТБ»
Одна БД на всё Поиск LIKE'ом по таблице, аналитика JOIN'ом по миллиардам строк Полихранилище: класс под каждый access pattern (§3)
Проекции без плана синхронизации (ошибка №10) «Поиск как-нибудь реплицируется» Источник правды + outbox/CDC + мониторинг лага (§4)
«Схема не нужна — гибкость» Schema-on-read переносит порчу данных в рантайм Schema-on-write — защита; гибкость только там, где реально вариативно (§1)
Redis как основная БД Память под терабайты, persistence «настроим потом» KV — hot path и временный стейт; правду держит RDBMS (§2)
LSM «быстрее на записи» везде Не названы спайки компактации и дорогие чтения старых ключей Таблица §5.4: write-heavy → LSM, читающий профиль → B-tree
Индекс на каждый столбец Запись платит за десяток структур Индексы под access pattern; составные с левым префиксом (§6)
Графовая БД «потому что соцсеть» Neo4j под «друзья друзей», который живёт в одном JOIN Глубина траверсы как критерий; соцграф на шардах по user_id (§2, §7)

9. Что назвать на секции (чек-лист раздела)

Обязательные фразы и действия, по которым видно, что раздел освоен. Полный чек-лист этапов — ../methodology.md §7; канон — ../knowledge-base.md §3.

На этапе 2 (сущности и API): - Каждое хранилище на схеме называть классом и представителем, не просто «БД»: «посты — реляционная, PostgreSQL», «ленты — KV, Redis-кластер». - Уже здесь проговорить роли: «это источник правды, это проекция» — архитектура полихранилища начинается с первого блока, а не с этапа 4.

На этапе 4 (детали + БД) — ядро раздела: - Про каждое хранилище — класс + 2 аргумента за + 1 против. Это формальный критерий брифа; минимум один полный разбор сделать вслух целиком. - «Выбираю не SQL vs NoSQL, а модель данных + гарантии + масштаб» — снять ложную дихотомию до того, как интервьюер её задаст. - Источник правды один (RDBMS), остальные — проекции с известным лагом + как синхронизируются (outbox/CDC) + что при расхождении (пересев из источника, деградация, не потеря). - На зонд «а как внутри вашей БД»: B-tree против LSM одной минутой — memtable/SSTable/компактация против страниц/WAL; вывод «write-heavy кейс → LSM-семейство, читающий профиль → B-tree» + назвать цену каждого (спайки против write-амплификации). - Индексы — от access pattern: «составной (user_id, created_at) под ленту пользователя; покрывающий под горячий путь; каждый индекс — налог на запись».

На этапе 5 (ресурсы): - Объём хранения → «влезает/не влезает в ноду»: единицы ТБ — реплики без шардов; десятки ТБ/год — шардирование (мост в статью 05), с числами-обоснованием из оценок.

На этапе 6 (эксплуатация): - Отказ проекции (кэш, поиск) → деградация латентности, источник жив; отказ источника → failover (мост в 08) — различать эти два класса отказов явно. - «Лаг проекций — метрика с алертом» — синхронизация без мониторинга считается несделанной.

Сквозные формулы раздела: - «Класс + 2 за + 1 против» — формат любого ответа про хранилище. - «Источник правды один, остальное — проекции с известным лагом». - «Выбираю модель + гарантии + масштаб, а не лагерь». - «Индекс — это платим записью за чтение».

10. Самопроверка «умею, если»

Критерий раздела из брифа: для любого кейса называю класс хранилища + 2 аргумента за и 1 против. Проверяется прогоном с закрытыми материалами: взять 5–7 кейсов из таблицы §3 вразброс (или из эталонов: URL shortener, медиа-хостинг) — на каждый 30–60 секунд: класс, представитель, два «за», один «против» с ценой. Затем зонды: «а как внутри вашей БД?» — LSM и B-tree по минуте с выводом о выборе; «почему не всё в одной БД?» — источник правды + проекции + синхронизация за три фразы; индексы — объяснить составной индекс через крайний левый префикс на примере ленты. Для ленты — полный полихранилищный список §7 от руки, пять хранилищ с ролями. Протокол тренировок и рубрика 0–3 — ../methodology.md §6; сверка с эталонами — ../classic-designs.md §1–3. Если на любой кейс ответ идёт по шаблону «класс → за → против» без пауз, а «как внутри» не вызывает ступора — раздел готов.

Связанные документы

  • 00-overview.md — обзорная статья по всем 12 разделам брифа (TASK-45.40)
  • 03-scaling.md — соседняя статья: лестница масштабирования, на которой индексы — первая ступень, а выбор хранилищ — куда ложится стейт
  • 05-replication-sharding.md — соседняя статья: репликация и шардирование выбранных хранилищ, local/global вторичные индексы в распределённой системе
  • ../brief.md — бриф: раздел 04 с критерием «умею, если»
  • ../knowledge-base.md — §3 таблица классов и критерии выбора, §3.1 движки (LSM vs B-tree), §2 числа для «влезает/не влезает»
  • ../methodology.md — §2 карточка этапа 4, §4 топ-10 ошибок (№5 и №10 отсюда), §6 протокол тренировок, §7 одностраничная карточка
  • ../classic-designs.md — §1 URL shortener, §2 лента (пять хранилищ), §3 мессенджер (conversation_id) — эталоны §7
  • ../materials/ddia-2ed/ch-03-data-models.md · ../materials/ddia-2ed/ch-04-storage-retrieval.md — источники: полный текст гл. 3–4 DDIA 2-го изд.
  • ../materials/alex-xu-vol1/ch-06-key-value-store.md — источник: устройство KV-хранилища (§2, §5)
  • Соседние статьи: 06-caching.md (KV-класс как кэш-слой) · 07-queues-streams.md (outbox/CDC, конвейеры проекций) · 08-fault-tolerance.md (отказ проекции vs отказ источника) · 09-consistency.md (гарантии, которыми платим за выбор)