Моделирование данных
6. Моделирование данных
Это самый сложный, самый важный и самый контр-интуитивный раздел. Здесь новички, пришедшие из SQL, могут наделать ошибок.
6.1. Single Table Design (STD)
- SQL-подход: Вы создаете 4 таблицы:
Users,Orders,Products,OrderItems. - Проблема:
- Производительность: Чтобы собрать страницу заказа, вашему приложению нужно сделать 4-5 запросов (
Get User,Get Order,Get OrderItems,Get Products). Это медленно (5 x 5мс = 25мс). - Экономика: У вас 4 таблицы.
Users- read-heavy.Orders- write-heavy. Вы должны настраивать RCU/WCU для каждой из них. Это сложно и дорого.
- Производительность: Чтобы собрать страницу заказа, вашему приложению нужно сделать 4-5 запросов (
Single Table Design - это паттерн, при котором вы храните разнородные сущности (Users, Orders) в одной физической таблице, чтобы получать их одним запросом.
Это реализуется через "перегрузку" ключей PK и SK.
- Таблица:
AppTable - Ключи:
PK(string),SK(string)
Пример схемы (E-commerce)
| PK | SK | type (атрибут) | data (атрибут) |
|---|---|---|---|
USER#alice | USER#alice | User | { "name": "Alice", "email": ... } |
USER#alice | ADDR#home | Address | { "city": "Astana", ... } |
USER#alice | ORDER#1001 | Order | { "status": "PENDING", ... } |
USER#alice | ORDER#1002 | Order | { "status": "SHIPPED", ... } |
ORDER#1001 | ITEM#SKU123 | OrderItem | { "product_name": "Book", ... } |
ORDER#1001 | ITEM#SKU456 | OrderItem | { "product_name": "Pen", ... } |
Что это дает? Вы делаете один запрос Query(PK='USER#alice') и получаете сразу и профиль, и адреса, и историю заказов. Это невероятно быстро.
6.2. Почему STD - не серебряная пуля
Долгое время STD продавали как единственный правильный способ. Но у этого подхода есть огромная цена, о которой часто молчат.
-
Высокий порог входа и сложность (Complexity). В SQL вы открываете таблицу
Ordersи видите заказы. Всё понятно. В STD вы открываете таблицу и видите "кашу" из всех данных сразу. Читать такую таблицу глазами в консоли AWS невозможно. Новому разработчику в команде (особенно джуниору) очень сложно объяснить, почему "чтобы найти пользователя, надо искать в поле PK строку, начинающуюся наORG#, а в поле SK - дату регистрации". -
Жесткость схемы (Inflexibility). Это главная проблема. STD требует абсолютного подхода "Access Patterns First". Вы проектируете таблицу под конкретные экраны вашего UI.
- Сценарий: Вы спроектировали ключи так, чтобы идеально показывать "Заказы пользователя".
- Изменение: Бизнес приходит через полгода и говорит: "Мы меняем логику, теперь заказы привязаны не к пользователю, а к отделу компании".
- Боль: В SQL вы бы просто поменяли
Foreign Key. В STD вам, возможно, придется переписывать всю таблицу (ETL-миграция), потому что ваши Primary Keys больше не подходят под новый запрос. Single Table Design оптимизирован под скорость чтения, но он хрупок к изменениям бизнес-требований.
Почему это больнее, чем в обычном подходе?
Может показаться, что в Multi-Table подходе при смене требований тоже нужно менять схему. Но разница фундаментальна:
- В Multi-Table (связь через атрибут): У заказа есть свой независимый ключ
ORDER#123, а связь с пользователем - это просто полеuser_id. Чтобы привязать заказ к отделу, вы делаете дешевый UpdateItem, меняя полеuser_idнаdept_id. Сама запись заказа физически остается на месте. - В STD (связь через ключ): Чтобы получить заказы пользователя одним запросом вместе с профилем, вы вынуждены "запекать" ID пользователя прямо в Partition Key заказа (например,
PK=USER#alice,SK=ORDER#123). - Проблема: В DynamoDB нельзя изменить Primary Key. Вы не можете сделать
UpdateItemи поменятьUSER#aliceнаDEPT#sales. Вам придется физически удалить старую запись и создать новую. Это превращает простое обновление ссылки в сложнейшую миграцию данных (ETL).
- Проблемы с аналитикой. Экспортировать данные в Data Lake сложнее. Если вы выгрузите таблицу в S3, аналитикам придется писать сложные фильтры: "Возьми строки, где SK начинается на
ORDER#, распарси их одним способом, а строкиUSER#- другим".
Когда не использовать Single Table Design?
Современная рекомендация звучит так: Не объединяйте сущности, если вы не запрашиваете их вместе.
- GraphQL: Если у вас GraphQL API (например, AppSync), он сам умеет эффективно "склеивать" данные из разных источников. Выигрыш от STD теряется, а сложность остается.
- Новые проекты (MVP): На старте стартапа вы не знаете своих паттернов доступа. Они поменяются 10 раз. Использовать STD на старте - это преждевременная оптимизация. Создайте отдельные таблицы (
Users,Orders). Это проще рефакторить. - Несвязанные данные: Нет смысла хранить "Логи аудита" и "Профили пользователей" в одной таблице. Они никогда не запрашиваются вместе.
Прагматичный подход (Multi-Table Design)
Вам можно использовать несколько таблиц в DynamoDB. Это не грех.
- Таблица
Users(Профили). - Таблица
Orders(Заказы и позиции заказов - здесь STD оправдан, так как заказ и его товары - это единое целое). - Таблица
Products.
Итог: Используйте Single Table Design только там, где у вас есть доказанная потребность в атомарном получении связанных данных (Parent + Children) за один запрос, и вы готовы платить за это сложностью поддержки. Во всех остальных случаях отдельные таблицы - это нормальный выбор.
6.3. Денормализация
В университете нас учили: "Никогда не дублируй данные! Третья нормальная форма!". В мире DynamoDB это правило меняется на: "Дублируй все, что нужно, чтобы чтение было быстрым".
- Проблема: В схеме выше у
OrderItemестьproduct_name. Но ведьproduct_nameможет измениться! - Ответ: Это нормально. Заказ - это "слепок" (snapshot) на момент времени. Когда заказ
ORDER#1001создан, мы копируемproduct_nameизPRODUCT#SKU123вORDER#1001. Если имя продукта изменится, в старом заказе останется старое имя. Это и есть правильное поведение.
Управление обновлениями дублированных данных
- Проблема: У
USER#aliceестьname: 'Alice'. Это имя скопировано в 100 ее заказов. Алиса меняет имя на 'Alice Smith'. - Решение 1 (Ничего не делать): Оставить 'Alice' в старых заказах. Это часто приемлемо.
- Решение 2 (Event-Driven):
- Обновление
PK: USER#aliceгенерирует событие в DynamoDB Streams. - Lambda-триггер ловит это событие.
- Lambda находит все связанные сущности и обновляет
user_nameв них. Это дорого и сложно. Используйте, только если критично необходимо.
- Обновление
6.4. Сначала - паттерны запросов
По-английски это называется Access Patterns First Approach. Это главное правило DynamoDB.
SQL-подход: Вы рисуете ER-диаграмму (сущности). Вы нормализуете ее. Потом вы пишете SELECT-запросы.
DynamoDB-подход:
- Вы садитесь с бизнес-аналитиком и сначала выписываете все запросы (Access Patterns), которые нужны вашему приложению.
- Нам нужно:
- A1: Найти юзера по ID.
- A2: Найти юзера по Email.
- A3: Найти все заказы юзера (последние первыми).
- A4: Найти заказ по ID.
- A5: Найти все товары в заказе.
- Только теперь вы проектируете таблицу (и ее GSI), которые удовлетворяют этим 5-ти запросам.
Вы не можете "просто построить" таблицу, а потом думать, как ее запрашивать. Вы проектируете таблицу под ваши запросы.
Single Table Design и моделирование данных
Вопросов: 6
6.5. Практика
Моделирование данных и Single Table Design для конкурента Twitter
Вы продолжаете работу как CTO стартапа-конкурента Twitter. Вместе с Назаром разберите Single Table Design (преимущества и недостатки), денормализацию данных, подход "Access Patterns First", и когда лучше использовать несколько таблиц вместо одной.
Назар - ваш персональный ИИ-наставник. Он поможет закрепить материал через практику и ответит на ваши вопросы.
💡 Все обсуждения с ИИ могут быть прочитаны администратором для улучшения качества обучения.