Конспект второй главы DDIA: реляционная, документная и графовая модели, schema-on-write vs schema-on-read, декларативные и императивные языки запросов.

Глава 2 — Модели данных и языки запросов

Модель данных — это не просто способ хранения. Это способ думать о данных и о том, какие вопросы к ним можно задать.

Клеппман начинает с важной мысли: сложность реального мира можно укротить только через абстракции. Данные проходят через несколько слоёв моделей — от объектов в коде приложения до битов на диске. На каждом слое мы прячем сложность нижнего уровня за чистым интерфейсом. Выбор модели данных определяет, что будет легко, а что — невозможно.

В этой главе рассматриваются три доминирующие модели: реляционная, документная и графовая. У каждой — свои сильные стороны и свой круг задач.

Реляционная модель (SQL)

Реляционная модель — самая распространённая и зрелая. Данные представлены в виде таблиц (отношений): строки — это кортежи, столбцы — атрибуты. Над множествами строк выполняются реляционные операторы (select, project, join).

Когда использовать: когда между сущностями есть строгие связи, а данные хорошо нормализуются. Реляционная модель отлично справляется с many-to-one и many-to-many отношениями благодаря join'ам.

Нормализация — ключевая идея: вместо повторения текстовых значений (например, «Greater Seattle Area» в каждой строке) мы храним ID, а расшифровку — в отдельной таблице. Это устраняет дублирование и риск неконсистентности.

users (id, name, region_id)
regions (id, name)

Проблема: реляционная модель не очень хорошо ложится на вложенные структуры, характерные для объектно-ориентированного кода. Возникает impedance mismatch — несоответствие между объектами в памяти и таблицами в базе. ORM-фреймворки частично смягчают, но не устраняют эту проблему.

Документная модель (NoSQL)

Документные базы (MongoDB, CouchDB, RethinkDB) хранят данные в виде самодостаточных документов — чаще всего JSON или XML.

Когда использовать: когда данные образуют дерево (one-to-many), а связи между разными документами редки или не критичны. Классический пример — резюме: у одного человека может быть несколько мест работы, несколько образований, контакты. Всё это естественно укладывается в один JSON-документ.

Преимущества:

  • Лучшая локальность: вся информация о сущности лежит в одном месте, один запрос — один ответ.
  • Гибкая схема: структура документа может меняться от записи к записи. Это удобно для быстро эволюционирующих приложений.
{
  "name": "Bill Gates",
  "positions": [
    { "title": "Co-chair", "org": "Bill & Melinda Gates Foundation" }
  ],
  "education": [
    { "school": "Harvard", "degree": "dropout" }
  ]
}

Проблема: документная модель плохо поддерживает join'ы. Если данные становятся взаимосвязанными (many-to-many), приходится либо эмулировать join в коде приложения, либо денормализовывать и дублировать данные, что возвращает нас к проблемам, от которых уходила реляционная модель.

Графовая модель

Когда всё может быть связано со всем, документы и таблицы начинают трещать по швам. На сцену выходит графовая модель: вершины (vertices) — сущности, рёбра (edges) — связи между ними.

Когда использовать: социальные графы, рекомендательные системы, маршрутизация, анализ зависимостей. Всё, где связи важнее самих сущностей.

Преимущества:

  • Естественная эволюция: добавить новый тип связи или новый тип вершины легко, не ломая схему.
  • Мощные языки запросов: Cypher, SPARQL, Datalog позволяют выражать сложные паттерны обхода графа декларативно.
MATCH (person:Person)-[:WORKS_AT]->(company:Company)
WHERE company.name = "Microsoft"
RETURN person.name

Проблема: графовые базы — нишевый инструмент. Они не заменяют реляционные или документные базы для типовых задач. Если у вас нет реальных many-to-many связей, граф — избыточная сложность.

Schema-on-write vs Schema-on-read

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

  • Schema-on-write (реляционные БД): схема задана при записи. Данные валидируются на входе. Изменение схемы требует ALTER TABLE, что может быть болезненно.
  • Schema-on-read (документные БД): схема не навязывается базой. Приложение само интерпретирует данные при чтении. Гибче, но ответственность за консистентность ложится на код.
Это не вопрос «какая схема лучше». Это вопрос «где вы хотите платить за гибкость — на записи или на чтении».

Языки запросов: декларативные vs императивные

Клеппман подчёркивает: декларативные языки (SQL, Cypher, Datalog) описывают что мы хотим получить, а не как это сделать. Это даёт свободу оптимизатору базы данных — он может переупорядочить операции, использовать индексы, распараллелить выполнение.

Императивный подход (MapReduce, код на Python) заставляет программиста описывать алгоритм. Это мощнее, но лишает базу возможности оптимизировать.

-- Декларативно: что мы хотим
SELECT region, COUNT(*) FROM users GROUP BY region;

-- Императивно: как это сделать (псевдокод)
counts = {}
for user in users:
    counts[user.region] += 1

Что запомнить

  • Реляционная модель — для строгих связей и сложных join'ов. Зрелая, но неудобна для вложенных структур.
  • Документная модель — для самодостаточных деревьев. Лучшая локальность, гибкая схема, но слабые join'ы.
  • Графовая модель — для many-to-many связей, где сами связи — главное.
  • Нормализация устраняет дублирование, но требует join'ов. Денормализация ускоряет чтение, но создаёт риск неконсистентности.
  • Schema-on-write — валидация на входе. Schema-on-read — интерпретация на выходе.
  • Декларативные языки дают оптимизатору свободу. Императивные — дают программисту контроль.
  • Нет «лучшей» модели данных. Есть подходящая под задачу.