Базы данных
Data Modeling: от концепции до физики
2015 год: стартап добавил EAV таблицу для 'гибкого' хранения атрибутов продуктов. Через 3 года: 50 миллионов строк в product_attributes, запрос списка товаров занимает 8 секунд, команда боится менять схему. Это называется схемный долг. Правильное data modeling в начале стоит дня работы, неправильное - месяцев рефакторинга.
- **GitHub**: переход с UUID на BIGINT PK для ключевых таблиц - 30-50% улучшение insert performance
- **Twitter**: денормализованные followers_count/friends_count в user profile - мгновенное чтение без COUNT JOIN
- **Instagram**: JSONB для гибких метаданных медиа - schema flexibility без EAV антипаттерна
Концептуальная модель данных
Концептуальная модель - высокоуровневое представление бизнес-сущностей и их связей без технических деталей. Используется для коммуникации с бизнесом, не для реализации. Основные инструменты: ER-диаграмма (Entity-Relationship), Domain Model. На этом уровне: только сущности, атрибуты и кардинальность связей (1:1, 1:N, M:N).
Uber Domain Model разбит на поддомены: Trip (поездка), Driver (водитель), Rider (пассажир), Payment (оплата), Map (маршруты). Каждый поддомен моделируется независимо, потом связи между ними - это Bounded Context из Domain-Driven Design (DDD).
Что НЕ входит в концептуальную модель данных?
Логическая модель: нормализация
Логическая модель - детализация концептуальной: добавляются primary keys, foreign keys, нормализация (устранение дублирования), но без привязки к конкретной СУБД. M:N связи превращаются в junction tables. Опциональные атрибуты могут выноситься в отдельные таблицы.
Обратите внимание на unit_price в ORDER_ITEMS: нельзя хранить только ссылку на PRODUCTS.price. Цены меняются, а исторический заказ должен хранить цену на момент покупки. Это денормализация по бизнес-причине, не техническая.
Зачем хранить unit_price в ORDER_ITEMS, если цена есть в PRODUCTS.price?
Физическая модель: оптимизация для СУБД
Физическая модель - логическая модель плюс специфика конкретной СУБД: типы данных, индексы, партиционирование, tablespaces, storage parameters. На этом уровне принимаются решения которые влияют на производительность: UUID vs BIGSERIAL, JSONB vs отдельные колонки, индексы на foreign keys, partial indexes.
GitHub перешёл с UUID PK на BIGINT (auto-increment) для основных таблиц, сохранив UUID как публичный идентификатор. Причина: UUID v4 random - плохой clustered index ключ (фрагментация). BIGSERIAL монотонно возрастает - лучше для B-Tree insert performance.
Почему UUID v4 как PRIMARY KEY в PostgreSQL хуже BIGSERIAL для производительности?
Денормализация: осознанный компромисс
Денормализация - намеренное добавление дублирования данных для ускорения чтения. После нормализации данные консистентны но запросы требуют join. После денормализации - быстрые reads но сложнее поддерживать консистентность при обновлениях. Правило: нормализовать по умолчанию, денормализовать только когда профайлер показывает проблему.
Twitter хранит followers_count и friends_count как денормализованные счётчики в таблице users - читается при каждом запросе профиля. Instagram делает то же для likes_count на постах. Это acceptable денормализация: счётчики обновляются атомарно при каждом follow/like.
Денормализация добавила post_count в таблицу users. Какой риск появился?
Антипаттерны проектирования схемы
Антипаттерны data modeling - повторяющиеся ошибки в проектировании схемы, которые создают проблемы при масштабировании. Наиболее опасны: EAV (Entity-Attribute-Value), Polymorphic Association, хранение списков в строке, God Table с сотнями колонок.
WordPress использует EAV паттерн (wp_postmeta таблица) для кастомных полей - это исторически известная точка боли при масштабировании. Magento (e-commerce) тоже использует EAV для атрибутов продуктов и столкнулся с serious performance issues при тысячах продуктов. Обе системы предлагают способы обойти EAV через специальные индексные таблицы.
EAV (Entity-Attribute-Value) - гибкое и удобное решение для хранения произвольных атрибутов
EAV жертвует type safety, индексируемостью и производительностью ради flexibility; лучшие альтернативы: JSONB, отдельные таблицы по типу сущности, или специализированные document databases
EAV запросы требуют complex JOIN и CAST - медленны и трудночитаемы. Нет проверки типов: can store 'abc' в numeric поле. PostgreSQL JSONB даёт ту же flexibility с GIN индексами, schema validation через CHECK constraints, и нативными операторами.
Почему хранить теги как 'python,javascript,database' в одной VARCHAR колонке - антипаттерн?
Итоги
- **Три уровня** - концептуальный (бизнес-сущности), логический (нормализация + FK), физический (типы, индексы, партиции)
- **Денормализация** - намеренное дублирование для ускорения чтения; всегда с триггером или application logic для поддержания консистентности
- **Антипаттерны** - EAV, списки в строке, polymorphic FK без ссылочной целостности; предпочитать JSONB или нормализованные таблицы
Связанные темы
Data modeling - фундамент для всех остальных тем курса:
- Нормализация — Логическая модель строится на нормальных формах - 1NF, 2NF, 3NF устраняют аномалии данных
- Индексы B-Tree — Физическая модель определяет какие индексы создавать - compound index порядок определяется паттернами запросов
- Партиционирование — Физическая модель включает решение о партиционировании по времени или другому ключу для больших таблиц
Вопросы для размышления
- Интернет-магазин продаёт электронику и одежду - у них очень разные атрибуты. Как смоделировать без EAV антипаттерна?
- Денормализованный счётчик post_count расходится с реальностью из-за бага. Как написать reconciliation query который находит расхождения?
- UUID v7 (IETF стандарт 2024) содержит timestamp в начале и монотонно возрастает. Решает ли это проблему фрагментации UUID v4 при использовании как PK?