Основные понятия

Моделирование данных

Узнайте, как моделировать данные на собеседованиях по System Design

Моделирование данных - это процесс, в котором мы определяем, как данные в приложении устроены, где они хранятся и как связаны друг с другом. На собеседовании по System Design ожидания заметно ниже, чем на отдельном интервью по моделированию данных, которое часто встречается у инженеров данных. Интервьюер ожидает не полной нормализации или идеальной схемы, а понятную, рабочую модель, которая соответствует требованиям системы.

В нашей структуре интервью этот шаг появляется дважды. Сначала, во время проектирования API, мы выделяем основные сущности. Затем, на этапе высокоуровнего дизайна, мы набрасываем рядом с базой данных упрощенную схему: ключевые поля, связи и заметки о том, какие индексы понадобятся для основных запросов. Этого почти всегда достаточно, чтобы интервьюер увидел, что ваша модель данных хорошо себя покажет при дальнейшем проектировании системы.

Моделирование данных на собеседовании

Но хорошая схема - это не просто набор прямоугольников. Она влияет на все остальное: как мы будем масштабировать чтение и запись, где нужна строгая согласованность, как отвечать на вопросы о росте хранилища или аудите данных, не переделывая все с нуля. Неаккуратная модель данных создает болезненные проблемы позже. Напротив, простая и надежная модель позволяет сфокусироваться на интервью на интересных вопросах связанных с конкретной задачей.

Варианты моделей баз данных

Прежде чем проектировать схему, нужно выбрать, с каким типом базы данных мы вообще работаем. Разные модели хранения по-разному влияют на структуру данных, поэтому этот выбор определяет все, что будет дальше.

На интервью лучше не использовать экзотические варианты. В большинстве случаев правильный ответ - реляционная база данных. Это выбор по умолчанию, если только требования явно не подсказывают специализированную модель. Если у вас нет серьезного практического опыта и сильных аргументов в пользу другого варианта, безопаснее всего брать PostgreSQL.

Это не значит, что про другие типы баз данных знать не нужно. Наоборот, понимание того, когда они уместны, показывает, что вы анализируете компромиссы, а не просто повторяете стандартный ответ. Но главная роль часто остается за SQL, поэтому начнем с него, а потом коротко обсудим альтернативы.

Реляционные базы данных (SQL)

Реляционные базы данных организуют данные в таблицы с фиксированной схемой: строки представляют сущности, столбцы - их атрибуты. Связи между сущностями поддерживаются внешними ключами, а транзакции получают гарантии ACID.

Большинство задач по системному дизайну естественно ложатся на эту модель. В соцсети есть пользователи, посты, комментарии и лайки - все это сущности с понятными связями. В интернет-магазине есть пользователи, товары, заказы и платежи. Такие сущности удобно представлять в виде таблиц, где ограничения и внешние ключи помогают сохранять целостность данных.

Таблица users:

id (primary key)usernameemailcreated_at
1jonjon@snow.com2026-01-01 10:00:00
2aryaarya@stark.com2026-02-01 10:05:00
3varysvarys@lys.com2026-03-01 10:10:00

Таблица posts:

id (primary key)user_id (foreign key)contentcreated_at
11Привет, мир!2026-01-01 20:00:00
21Мой первый пост2026-01-01 20:05:00
32Еще один пост2026-01-01 20:10:00

Таблица likes:

id (primary key)user_id (foreign key)post_id (foreign key)created_at
1112026-01-01 20:00:00
2122026-01-01 20:05:00
3232026-01-01 20:10:00

SQL отлично справляется со сложными запросами. Если нам нужно получить "все посты пользователей, на которых подписан конкретный пользователь, отсортированные в обратном хронологическом порядке", JOIN делает это естественным образом. Но с такими запросами нужно быть осторожными - многотабличные JOIN-ы легко превращаются в проблему при масштабировании. На интервью разговор о тяжелых запросах с агрегациями часто вызывает дополнительные вопросы о производительности, так что стоит сразу думать, хватит ли обычных JOIN-ов, или здесь уже понадобятся денормализованные представления, кэширование или предвычисленные результаты. А если среди нефункциональных требований есть строгая согласованность - например, чтобы платеж не прошел дважды или чтобы остатки на складе не уходили в минус, - ACID-гарантии SQL подходят идеально.

Чаще всего против реляционных баз приводят аргумент про масштабирование, но его обычно сильно преувеличивают. Современные SQL базы масштабируются с помощью реплик чтения, шардирования, пула соединений и кэша. Крупные компании по всему миру по-прежнему опираются на реляционные базы данных. Масштабирование зависит не только от выбора базы данных, но и от того, как мы строим систему вокруг нее.

Примеры технологий: PostgreSQL, MySQL и SQLite.

Документные базы данных

Документные базы данных хранят данные в JSON-подобных документах с гибкой схемой. Они хорошо подходят для быстро меняющихся приложений, где заранее непонятно, какие поля понадобятся. В такой модели мы больше думаем не о нормализации между таблицами, а о вложенности и встраивании связанных данных внутрь документов.

Посмотрите, как меняется та же самая модель из SQL-примера. Вместо отдельных таблиц мы встраиваем посты прямо в документ пользователя. Это избавляет от JOIN-ов, но обновление одного поста теперь означает поиск и изменение всего документа пользователя.

Коллекция users:

{
  "_id": "1",
  "username": "jon",
  "email": "jon@snow.com",
  "posts": [
    {
      "content": "Привет, мир!",
      "created_at": "2026-01-01 20:00:00"
    },
    {
      "content": "Мой первый пост",
      "created_at": "2026-01-01 20:05:00"
    }
  ],
  "created_at": "2026-01-01 10:00:00"
}

В задачах по System Design функциональные требования обычно специально ограничивают до компактного и понятного набора. Поэтому сценарий "схема часто меняется" встречается не так уж часто, а это как раз главный аргумент в пользу документной базы. Рассматривайте ее только если интервьюер явно говорит о быстро эволюционирующих структурах данных.

Когда выбрать вместо SQL: когда схема часто меняется, когда у вас сильно вложенные данные, которые в SQL потребовали бы много JOIN-ов, или когда разные записи имеют очень разную структуру. Например, профиль пользователя, где у одного есть длинная история карьеры, а у другого только несколько полей.

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

Примеры технологий: MongoDB, Firestore и CouchDB.

Хранилища ключ-значение

Хранилища "ключ-значение" дают очень простую модель доступа: мы берем значение по точному ключу. Они очень быстрые, но почти не поддерживают более сложные запросы.

Когда выбрать вместо SQL: для кэша, хранения сессий, feature flags или любого сценария, где нам нужен доступ только по одному идентификатору. Они также подходят для систем с большим объемом записи, где критична производительность, а сложные запросы не нужны.

Фраза "вместо SQL" здесь немного вводит в заблуждение. На практике мы часто используем оба варианта одновременно. SQL остается источником истины, а перед ним ставится кэш, такой как Redis, для часто используемых данных. Так мы получаем быстрый доступ без потери надежности и гибкости запросов.

Как это влияет на моделирование данных: схема становится очень плоской. Нам приходится сильно денормализовывать данные и дублировать их под разные ключи, чтобы поддержать нужные паттерны доступа, потому что JOIN-ы и сложные запросы недоступны. Это хорошо для чтения, но плохо для согласованности при обновлениях.

Кэш типа ключ-значение

Примеры технологий: Redis, DynamoDB и Memcached.

Базы данных с семействами столбцов (wide-column databases)

В базах данных с семействами столбцов строки могут содержать разный набор столбцов. Они оптимизированы под интенсивные нагрузки на запись и временные ряды.

Когда пользователь создает новый пост, мы добавляем строку с ключом вида (user_id, timestamp). Строки с одинаковым ключом партиционирования, то есть user_id, хранятся рядом. Это делает запись быстрой - мы просто дописываем данные в раздел, - а чтение удобным, потому что можно эффективно сканировать непрерывный диапазон постов пользователя.

База данных с семействами столбцов

Когда выбрать вместо SQL: когда у вас огромные объемы записи, временные ряды или аналитические нагрузки, где данные в основном дописываются, а затем агрегируются. Типичные примеры - телеметрия, логирование событий или IoT-сенсоры.

Как это влияет на моделирование данных: проектировать схему приходится еще сильнее отталкиваясь от запросов, чем в SQL. Мы часто дублируем данные между разными семействами столбцов под разные паттерны чтения. Время становится полноправным элементом модели.

Примеры технологий: Cassandra и HBase.

Графовые базы данных

Графовые базы данных хранят данные в виде узлов и ребер и оптимизированы под обход связей между сущностями.

Графовая база данных

Когда выбрать вместо SQL: почти никогда на интервью. Классические примеры - соцсети и рекомендательные системы, но даже крупнейшая соцсеть в мире хранит свой социальный граф на MySQL. Если это достаточно хорошо для продуктов таких масштабов, этого почти наверняка достаточно и для вашего интервью.

Примеры технологий: Neo4j и Amazon Neptune.

Графовые базы - частая ошибка на интервью. Они звучат заумно, но почти всегда добавляют лишнюю сложность. Даже компании с очень плотными графами, такие как LinkedIn и Twitter, используют SQL для своих основных данных о связях. Другие типы баз обычно покрывают основные запросы без операционной сложности специализированного графового хранилища.

Основы проектирования схемы

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

Начните с требований

Все строится вокруг трех факторов, которые мы обычно уже прояснили на этапе сбора требований и проектирования API.

Объем данных определяет, где эти данные вообще могут физически храниться. В соцсети с миллионами пользователей может потребоваться распределение данных по нескольким хранилищам данных, и это напрямую влияет на схему. Если сущности находятся в разных системах по причинам производительности или по организационным причинам, у них будут разные схемы, и нам придется аккуратно продумать, как они ссылаются друг на друга.

Паттерны доступа - самый важный фактор, именно они определяют большинство решений. Как именно будут читать и обновлять данные? Если нам нужна лента новостей вида "свежие посты пользователей, на которых я подписан", стоит задуматься о денормализации или как минимум о хорошо продуманных индексах. Если нужен аналитический дашборд по временным периодам, структура таблиц может быть совсем другой. Обычно ответ на этот вопрос естественно вытекает из нашего API.

Требования к согласованности определяют, насколько тесно данные должны быть связаны. Финансовые операции требуют строгой согласованности - нельзя допускать частичного списания средств, - а значит связанные данные часто лучше держать в одной базе с ACID-гарантиями. Но для ленты активности пользователя вполне приемлема согласованность в конечном счете - ничего страшного, если лайк появится через пару секунд, - и это дает нам больше свободы для распределения данных по разным системам и схемам.

На интервью важно прямо связывать решения по схеме с этими факторами. Например: "Нам нужно быстро загружать ленту, а лайки могут быть согласованными в конечном счете, поэтому мы денормализуем счетчик лайков в таблицу постов". Это показывает, что вы не повторяете заученные шаблоны, а рассуждаете применительно к конкретной задаче.

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

Сущности, ключи и связи

После того как мы выделили основные сущности, следующий шаг - превратить их в таблицы или коллекции с понятными идентификаторами и связями.

В соцсети это могут быть пользователи, посты, комментарии и лайки. У каждой сущности должен быть первичный ключ, который однозначно идентифицирует запись. Лучше использовать системные идентификаторы, такие как user_id или post_id, а не бизнес-поля, такие как email. Системные ключи стабильнее и не меняются вместе с бизнес-логикой.

users: id (PK), username, email
posts: id (PK), user_id (FK -> users.id), content, created_at
comments: id (PK), post_id (FK -> posts.id), user_id (FK -> users.id), content
likes: user_id (FK -> users.id), post_id (FK -> posts.id)

Здесь хорошо видны основные связи: каждый пост принадлежит одному пользователю (posts.user_id), каждый комментарий связан и с постом, и с пользователем, а лайки связывают пользователей с постами. Обозначение (PK) означает первичный ключ (primary key), (FK) - внешний ключ (foreign key), а стрелки показывают, на что именно он ссылается.

На интервью почти всегда достаточно выбрать очевидный первичный ключ и коротко объяснить его назначение. Например: "post_id будет первичным ключом, потому что по нему мы однозначно идентифицируем пост и сможем ссылаться на него из комментариев и лайков".

После этого нужно проговорить сами типы связей: один ко многим, многие ко многим и один к одному.

  • Один ко многим (1:N): у пользователя много постов, у поста много комментариев.
  • Многие ко многим (N:M): пользователь может лайкнуть много постов, и один пост может быть лайкнут многими пользователями.
  • Один к одному (1:1): в реальных системах встречается редко и часто может означать, что таблицы стоит объединить.

В SQL такие связи поддерживаются через внешние ключи, например posts.user_id -> users.id, а в NoSQL - чаще логикой приложения. Внешние ключи помогают сохранять ссылочную целостность: не дают оставить пост без существующего автора или комментарий без существующего поста. Но у них есть и цена, потому что база должна проверять их при вставке и обновлении. На очень большом масштабе некоторые компании отключают такие ограничения ради скорости записи и переносят контроль целостности в код приложения. На интервью даже простое упоминание этого компромисса уже выглядит хорошо.

Затем имеет смысл добавить ограничения вроде NOT NULL, UNIQUE или CHECK. Они переносят часть правил корректности прямо в базу данных: email должен быть уникальным, цена - положительной и так далее. Это помогает защищать качество данных, хотя и добавляет накладные расходы на запись.

Держите схему как можно ближе к предметной области: users, tweets, follows, если вы моделируете Twitter, а не абстрактные entities и relationships. После этого покажите, как первичные и внешние ключи и ограничения делают эту модель корректной и масштабируемой.

Индексация с учетом паттернов доступа

Индексы - это структуры данных, которые помогают базе быстро находить записи, не просматривая всю таблицу. Представьте оглавление в книге: вместо чтения каждой страницы в поисках термина "нормализация" вы открываете указатель и сразу попадаете на нужную страницу. На интервью почти всегда полезно сказать, какие столбцы вы бы индексировали и почему.

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

  • Индекс по posts.user_id, чтобы быстро получать все посты пользователя.
  • Индекс по posts.created_at, чтобы легко загружать последние посты по времени.
  • Составной индекс по (user_id, created_at), чтобы эффективно получать последние посты конкретного пользователя.

Если хотите глубже разобраться, как индексы устроены под капотом, какие бывают типы индексов (B-деревья, хеш-индексы и т. д.) и расширенные стратегии индексации, смотрите нашу статью Индексирование баз данных.

Для интервью важнее другое: связывайте индекс не с таблицей, а с конкретным API. Рассуждение формата "эндпоинту GET /users/{id}/posts нужен индекс по posts.user_id" показывает, что вы думаете о реальной производительности запросов.

Нормализация и денормализация

Нормализация означает, что каждая единица информации хранится ровно в одном месте. Данные пользователя хранятся только в таблице users, а не копируются по другим таблицам. Это защищает от аномалий, когда данные обновились в одном месте, но не обновились в другом, и система оказалась в противоречивом состоянии.

Нормализованный вариант:

idusernameemail
1jonjon@snow.com
2aryaarya@stark.com
iduser_id (FK)contentcreated_at
11Привет, мир!2026-01-01 20:00:00
21Мой первый пост2026-01-01 20:05:00

Денормализованный вариант:

iduser_idusernameemailcontentcreated_at
11jonjon@snow.comПривет, мир!2026-01-01 20:00:00
21jonjon@snow.comМой первый пост2026-01-01 20:05:00

В денормализованной версии изменение username означает, что нам нужно обновить каждый пост этого пользователя. Если хотя бы одна запись останется старой, мы получим несогласованные данные.

На собеседованиях по System Design лучше почти всегда начинать с чистой нормализованной модели и денормализовывать только там, где это действительно нужно. Не повторяйте одни и те же данные без серьезной причины. Дублирование данных расточительно и создает проблемы согласованности, которые обычно сложнее, чем те проблемы производительности, которые мы пытались решить.

Есть несколько типичных исключений, где денормализация оправдана:

  • Аналитика и отчетность, где мы часто агрегируем данные, которые меняются редко.
  • Журналы событий, где нам важно сохранить снимок состояния в конкретный момент времени.
  • Системы, оптимизированные под интенсивное чтение, такие как поисковые движки, где скорость важнее строгой согласованности.

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

Масштабирование и шардирование

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

Шардируйте по основному паттерну доступа. Если большинство запросов выглядит как "получить посты пользователя", шардируйте по user_id. Тогда все посты пользователя окажутся в одной базе, и вы избежите дорогих межшардовых запросов.

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

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

Шардирование данных

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

Заключение

Моделирование данных - важная, но не главная часть собеседования по System Design. Цель не в том, чтобы нарисовать идеальную схему, а в том, чтобы показать: вы умеете предложить разумную модель данных, которая поддерживает требования системы, и после этого двигаться дальше.

Начните с основных сущностей как можно раньше. Затем, когда дойдете до базы данных в высокоуровневом дизайне:

  1. Определите тип базы данных.
  2. Перечислите поля, которые нужны, чтобы закрыть функциональные требования.
  3. Укажите первичный ключ и внешние ключи для связей.
  4. Назовите поля, которым нужны индексы.
  5. Решите, нужна ли денормализация ради производительности.
  6. Подумайте, нужно ли шардирование. Если да, выберите ключ для шардирования под основной паттерн доступа.
Войдите чтобы отмечать прогресс