ERD простыми словами: сущности, связи и модель данных
Когда проектируется информационная система, довольно быстро возникает вопрос:
Какие данные система вообще должна хранить и как эти данные связаны между собой?
Например, в системе образовательной программы могут существовать:
Student
Application
Program
Document
Но одного списка сущностей недостаточно.
Нужно понять:
- может ли у одного студента быть несколько заявок;
- относится ли каждая заявка только к одной программе;
- может ли один документ относиться к нескольким заявкам;
- какие поля обязательны;
- каким образом одна запись ссылается на другую;
- что произойдёт при удалении связанной сущности;
- какие ограничения должна обеспечивать сама база данных.
Для формального описания структуры данных используется ERD - Entity Relationship Diagram.
ERD показывает сущности, их атрибуты и связи между ними.
Простейшую модель можно представить так:
Student 1:N Application N:1 Program
Это означает:
- один Student может иметь несколько Application;
- каждая Application относится к одному Student;
- одна Program может иметь множество Application;
- каждая Application относится к одной Program.
ERD является одним из основных инструментов проектирования реляционных баз данных и хорошо связывает требования предметной области с будущей структурой PostgreSQL.
Что такое ERD
ERD расшифровывается как Entity Relationship Diagram.
Основные понятия:
Entity - сущность
Attribute - атрибут
Relationship - связь
Primary Key - уникальный идентификатор
Foreign Key - ссылка на другую сущность
Если сильно упростить:
Сущность -> что храним
Атрибут -> какие свойства у этого объекта
Связь -> как объект связан с другими объектами
Например:
Student
может иметь атрибуты:
id
first_name
last_name
email
created_at
А сущность:
Application
может содержать:
id
student_id
program_id
status
created_at
Поле student_id связывает заявку со студентом, а program_id - с программой.
Уже на этом уровне появляется модель:
Student -> Application -> Program
ERD помогает формализовать эту структуру ещё до написания SQL или Django models.
Сущность
Entity - сущность предметной области, информацию о которой система должна хранить.
Например:
Student
Application
Program
Document
Course
Teacher
Payment
Сущность обычно имеет собственный смысл и жизненный цикл.
Например, Application можно:
- создать;
- отправить;
- проверить;
- согласовать;
- отклонить;
- архивировать.
Это уже хороший признак того, что перед нами полноценная сущность.
Важно отличать сущность от действия.
Например:
Отправить заявку
это действие.
А:
Application
это объект, над которым выполняется действие.
То же самое:
Проверить документ - действие
Document - сущность
При проектировании ERD полезно искать не глаголы, а устойчивые объекты предметной области.
Атрибуты
Attribute - свойство сущности.
Например:
Application
id
status
created_at
submitted_at
comment
Каждый атрибут должен описывать конкретное свойство объекта.
Для Student это могут быть:
id
first_name
last_name
email
birth_date
Для Program:
id
name
start_date
end_date
capacity
Атрибуты позже превращаются в столбцы таблицы.
Упрощённо:
Entity -> Table
Attribute -> Column
Но ERD находится немного выше физической реализации базы данных.
На логическом уровне нас прежде всего интересует смысл данных и отношений между ними.
Primary Key
Каждую запись нужно уметь однозначно идентифицировать.
Для этого используется Primary Key.
Например:
Student.id
Application.id
Program.id
В PostgreSQL:
CREATE TABLE student (
id BIGINT PRIMARY KEY,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL
);
Primary Key гарантирует уникальность значения и запрещает NULL.
То есть двух студентов с одним и тем же id существовать не должно.
На практике идентификатор может быть:
- integer;
- bigint;
- UUID;
- естественным бизнес-ключом.
Однако бизнес-значения вроде email не всегда удобно использовать как основной Primary Key.
Email может измениться, а внутренний идентификатор обычно должен оставаться стабильным.
Поэтому часто используется отдельное поле:
id
а бизнес-уникальность обеспечивается отдельным ограничением:
email UNIQUE
Foreign Key
Foreign Key - поле, которое ссылается на Primary Key или другой уникальный ключ другой таблицы.
Например:
Application.student_id -> Student.id
Это означает:
Каждая заявка связана с конкретным студентом.
В PostgreSQL:
CREATE TABLE application (
id BIGINT PRIMARY KEY,
student_id BIGINT NOT NULL,
status TEXT NOT NULL,
FOREIGN KEY (student_id)
REFERENCES student(id)
);
Теперь база данных сама контролирует ссылочную целостность.
Нельзя создать:
student_id = 999
если такого Student не существует.
Таким образом связь существует не только визуально на ERD, но и обеспечивается ограничением базы данных.
Связи между сущностями
Основные типы связей:
1:1 - one-to-one
1:N - one-to-many
M:N - many-to-many
Кардинальность показывает, сколько объектов одной сущности может быть связано с объектом другой сущности.
Связь 1:1
Один объект связан максимум с одним объектом другой сущности.
Например:
User 1:1 UserProfile
Один пользователь имеет один профиль, а один профиль относится к одному пользователю.
В базе это можно реализовать Foreign Key с ограничением UNIQUE.
Например:
CREATE TABLE user_profile (
id BIGINT PRIMARY KEY,
user_id BIGINT UNIQUE NOT NULL,
FOREIGN KEY (user_id)
REFERENCES users(id)
);
Связь 1:1 нужна не так часто, как кажется.
Иногда две сущности, связанные 1:1, на самом деле можно хранить в одной таблице.
Разделение имеет смысл, если:
- данные имеют отдельный жизненный цикл;
- часть полей используется редко;
- доступ к данным различается;
- данные принадлежат разным подсистемам;
- разделение имеет понятный предметный смысл.
Связь 1:N
Самая распространённая связь.
Например:
Student 1:N Application
Один Student может иметь несколько Application.
Каждая Application относится к одному Student.
Foreign Key находится на стороне N:
Application.student_id -> Student.id
То же самое:
Program 1:N Application
Company 1:N Employee
Client 1:N Deal
Order 1:N OrderItem
Это фундаментальная закономерность реляционного моделирования:
в связи 1:N Foreign Key обычно находится на стороне N
Связь M:N
Many-to-many означает:
Один объект A связан с несколькими B, и один B связан с несколькими A.
Например:
Student M:N Course
Один студент может учиться на нескольких курсах.
На одном курсе может учиться множество студентов.
В реляционной базе напрямую хранить такую связь неудобно.
Поэтому появляется промежуточная сущность:
Student 1:N Enrollment N:1 Course
Enrollment может содержать:
id
student_id
course_id
enrolled_at
status
final_grade
Теперь связь сама превращается в полноценную сущность.
Это важный момент.
Промежуточная таблица не обязательно существует только технически.
Часто сама связь имеет собственные данные.
Например:
Student + Course
создают:
Enrollment
у которого есть:
дата зачисления
статус
оценка
дата завершения
То есть отношение между сущностями само становится объектом предметной области.
Обязательные и необязательные связи
Кардинальность описывает не только максимальное количество объектов, но и минимальное.
Например:
Application -> Program
может означать:
Каждая заявка обязательно принадлежит одной программе.
Тогда:
program_id NOT NULL
Но другая связь может быть необязательной.
Например:
Application -> Reviewer
До начала проверки reviewer ещё не назначен.
Тогда:
reviewer_id NULL
получается допустимым.
Логически:
0..1 - может отсутствовать или существовать один объект
1 - ровно один
0..N - любое количество, включая отсутствие
1..N - хотя бы один
Эта информация важна не меньше самой связи.
Например:
Application 1 -> Program
и:
Application 0..1 -> Reviewer
означают принципиально разные ограничения.
Как показываются связи на ERD
Существует несколько вариантов нотации ERD.
Один из самых распространённых - Crow's Foot notation.
На концах связи используются обозначения, показывающие:
- один;
- много;
- обязательность;
- необязательность.
В текстовом виде можно представлять так:
Student 1:N Application
Program 1:N Application
Application 0..N Document
Но на графической диаграмме кардинальность обычно обозначается специальными символами.
Конкретный внешний вид зависит от используемой нотации и редактора.
Главное не форма линии, а семантика:
сколько объектов может быть связано
и
является ли связь обязательной
Как находить сущности в требованиях
На практике ERD редко начинают с пустого листа.
Обычно сначала существует текстовое описание системы.
Например:
Абитуриент выбирает образовательную программу и подаёт заявку. К заявке он может приложить несколько документов. Оператор проверяет заявку. После согласования заявка становится основанием для создания участника программы.
Теперь можно искать устойчивые бизнес-объекты.
В тексте появляются:
Applicant
Program
Application
Document
Operator
Participant
Но не каждое существительное обязательно должно стать таблицей.
Нужно проверить смысл каждого объекта.
1. Найти устойчивые бизнес-объекты
Спросить:
Нужно ли системе отдельно хранить информацию об этом объекте?
Для Application ответ очевидно да.
Для кнопки "Отправить" - нет.
2. Отделить сущности от действий
Отправить заявку - действие
Application - сущность
Проверить документ - действие
Document - сущность
3. Найти идентификатор
У каждой сущности должен существовать способ отличить одну запись от другой.
Например:
Application.id
Student.id
Program.id
4. Определить связи
Спросить:
Кому принадлежит объект?
С чем он связан?
Может ли существовать самостоятельно?
Сколько таких объектов может существовать?
Например:
Student 1:N Application
Program 1:N Application
Application 1:N Document
5. Проверить жизненный цикл
Если объект имеет собственные:
- состояния;
- даты;
- историю;
- действия;
- правила;
скорее всего, это полноценная сущность.
6. Определить обязательность
Например:
Application обязательно имеет Program
Application может ещё не иметь Reviewer
7. Убрать дублирование смысла
Если две сущности описывают одно и то же понятие разными названиями, модель постепенно становится противоречивой.
Например:
Client
Customer
Buyer
могут оказаться одной сущностью, если бизнес не различает эти понятия.
Нормализация
После выделения сущностей возникает следующая проблема:
Где именно хранить каждый факт?
Представим таблицу:
Application
student_name
student_email
program_name
program_start_date
program_manager
status
На первый взгляд удобно.
Но если один Student подаст пять заявок, его имя и email повторятся пять раз.
Если название программы изменится, его придётся менять во множестве строк.
Возникает дублирование данных.
Нормализация помогает разделять данные так, чтобы один факт по возможности хранился в одном месте.
Например:
Student
Application
Program
и связи:
Student 1:N Application
Program 1:N Application
Теперь:
student_email
хранится в Student,
а:
program_name
в Program.
Application хранит ссылки:
student_id
program_id
Зачем нужна нормализация
Основная проблема дублирования - не размер базы.
Главная проблема - противоречивость данных.
Представим:
application_1 -> program_name = "Data Science"
application_2 -> program_name = "Data Science"
application_3 -> program_name = "Data Sience"
Какое название правильное?
Если Program существует отдельно:
Program.id = 42
Program.name = "Data Science"
все заявки просто ссылаются:
program_id = 42
и источник истины один.
Аномалии данных
Плохая структура может создавать несколько типов проблем.
Update anomaly
Один и тот же факт нужно менять в нескольких местах.
Например:
название программы хранится в 500 заявках
При переименовании нужно обновить 500 строк.
Insert anomaly
Невозможно создать один объект, пока не существует другой.
Например, если информация о Program хранится только внутри Application, нельзя создать программу до появления первой заявки.
Delete anomaly
При удалении одной записи случайно исчезает информация о другом объекте.
Например, удалив последнюю Application, можно потерять единственную запись с информацией о Program.
Нормализация помогает уменьшать такие зависимости.
1NF, 2NF и 3NF без лишней формальности
Первая нормальная форма требует, чтобы значения были атомарными и структура таблицы не содержала повторяющихся групп.
Плохо:
phone_1
phone_2
phone_3
или:
courses = "Python, SQL, ML"
если с отдельными курсами нужно работать как с сущностями.
Вторая нормальная форма становится особенно важна для таблиц с составным ключом и требует, чтобы неключевые атрибуты зависели от полного ключа.
Третья нормальная форма борется с транзитивными зависимостями, когда неключевое поле фактически зависит от другого неключевого поля.
Для практического проектирования важнее запомнить общий принцип:
Каждый факт должен храниться там, где находится сущность, которой этот факт принадлежит.
Но нормализация не означает, что любую базу нужно доводить до максимально возможной нормальной формы.
Иногда денормализация используется осознанно ради производительности или удобства чтения.
Главное - понимать цену такого решения.
Как ERD превращается в PostgreSQL
Логическая модель постепенно превращается в физическую схему.
Основное соответствие:
Entity -> Table
Attribute -> Column
Primary Key -> PRIMARY KEY
Relationship -> FOREIGN KEY
Mandatory relation -> NOT NULL
Cardinality -> структура Foreign Key и constraints
Например, ERD:
Student 1:N Application N:1 Program
может превратиться в:
CREATE TABLE student (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL
);
CREATE TABLE program (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
start_date DATE NOT NULL
);
CREATE TABLE application (
id BIGSERIAL PRIMARY KEY,
student_id BIGINT NOT NULL
REFERENCES student(id),
program_id BIGINT NOT NULL
REFERENCES program(id),
status TEXT NOT NULL,
created_at TIMESTAMPTZ
NOT NULL DEFAULT now()
);
ERD уже подсказала:
- какие таблицы нужны;
- где находятся Foreign Keys;
- какие отношения существуют;
- какие связи обязательны.
Но PostgreSQL добавляет ещё один уровень детализации:
- типы данных;
CHECK;UNIQUE;- индексы;
ON DELETE;- defaults;
- generated columns;
- triggers;
- технические ограничения.
То есть:
ERD -> логическая модель
PostgreSQL schema -> конкретная реализация модели
Business entity и техническая сущность
Не каждая таблица появляется непосредственно из бизнес-терминологии.
В предметной области могут существовать:
Client
Deal
Application
Program
Document
Это business entities.
Но при разработке появляются технические сущности:
AuditLog
OutboxEvent
WebhookDelivery
RefreshToken
BackgroundJob
Например, пользователь бизнеса может никогда не говорить:
Нам нужна сущность WebhookDelivery.
Но она появляется из технических требований к надёжной доставке webhook.
Или:
OutboxEvent
возникает для реализации Transactional Outbox.
Поэтому итоговая ERD production-системы может содержать два слоя:
предметная модель
+
техническая инфраструктура данных
Главное - не смешивать их смысл.
История изменений
Часто недостаточно знать только текущее состояние объекта.
Например:
Application.status = APPROVED
говорит, что заявка одобрена сейчас.
Но не отвечает на вопросы:
- когда она была отправлена;
- сколько времени находилась на проверке;
- кто её согласовал;
- была ли она раньше отклонена;
- сколько раз возвращалась на исправление.
Тогда появляется история.
Например:
Application
id
current_status
и:
ApplicationStatusHistory
id
application_id
old_status
new_status
changed_at
changed_by
Связь:
Application 1:N ApplicationStatusHistory
Теперь текущее состояние удобно читать из Application, а историю можно анализировать отдельно.
Temporal data
В некоторых системах важно знать не только историю изменений, но и период действия записи.
Например:
effective_from
effective_to
Тогда можно спросить:
Какое значение действовало 1 сентября?
Это уже temporal modeling.
Подобная модель используется для:
- тарифов;
- цен;
- договоров;
- ролей;
- организационной структуры;
- справочников;
- исторических состояний.
Упрощённо:
EntityVersion
entity_id
value
effective_from
effective_to
Такая структура сложнее обычного updated_at, зато позволяет восстанавливать состояние системы на определённый момент времени.
ERD и Django ORM
Django ORM тоже описывает связи между сущностями.
Например:
class Student(models.Model):
email = models.EmailField(unique=True)
class Program(models.Model):
name = models.CharField(max_length=200)
class Application(models.Model):
student = models.ForeignKey(
Student,
on_delete=models.CASCADE,
)
program = models.ForeignKey(
Program,
on_delete=models.PROTECT,
)
status = models.CharField(
max_length=30,
)
Здесь:
ForeignKey -> связь N:1
OneToOneField -> связь 1:1
ManyToManyField -> связь M:N
Например:
class Student(models.Model):
courses = models.ManyToManyField(
Course,
)
Django создаст промежуточную таблицу.
Но если связь сама имеет дополнительные данные:
enrolled_at
status
grade
лучше создать отдельную модель:
class Enrollment(models.Model):
student = models.ForeignKey(
Student,
on_delete=models.CASCADE,
)
course = models.ForeignKey(
Course,
on_delete=models.CASCADE,
)
enrolled_at = models.DateTimeField()
status = models.CharField(max_length=30)
Именно здесь особенно хорошо видно:
ORM не отменяет понимание ERD.
Django может создать таблицы автоматически, но разработчику всё равно нужно понимать, какую модель данных он строит.
on_delete тоже является частью модели
Связь между таблицами должна определять поведение при удалении.
Например:
on_delete=models.CASCADE
может означать:
удалили Student -> удалили связанные записи
А:
on_delete=models.PROTECT
запрещает удаление объекта, пока на него существуют ссылки.
Это уже не просто техническая настройка ORM.
Это правило целостности данных.
Поэтому выбор CASCADE, PROTECT, SET_NULL и других вариантов должен соответствовать бизнес-смыслу связи.
ERD и индексы
ERD в первую очередь описывает структуру данных.
Она не является схемой производительности.
Например, из ERD видно:
Program 1:N Application
Но из неё ещё не следует, какие запросы будут выполняться чаще всего.
Представим типичный запрос:
найти все заявки определённой программы со статусом UNDER_REVIEW
SQL:
SELECT *
FROM application
WHERE program_id = 42
AND status = 'UNDER_REVIEW';
Если запрос выполняется постоянно, может появиться индекс:
CREATE INDEX idx_application_program_status
ON application(program_id, status);
Таким образом:
ERD -> логическая структура данных
Indexes -> физическая оптимизация доступа
Индекс не нужно добавлять просто потому, что на ERD существует связь.
Он должен появляться из реальных запросов и профиля нагрузки.
Foreign Key и индекс - не одно и то же
Это важный момент для PostgreSQL.
Foreign Key обеспечивает ссылочную целостность:
Application.program_id должен ссылаться на существующий Program
Индекс отвечает за скорость поиска.
Это разные механизмы.
Primary Key обычно получает индекс автоматически.
Но наличие Foreign Key на referencing column само по себе не означает, что PostgreSQL автоматически создаст на этом поле индекс.
Поэтому индексацию Foreign Key нужно оценивать отдельно.
ERD, UML и BPMN
Эти модели описывают разные стороны одной системы.
Например:
BPMN - как проходит обработка заявки
ERD - какие данные существуют
UML Class Diagram - как устроены программные объекты
State Machine - как Application меняет status
Sequence Diagram - как компоненты взаимодействуют при создании Application
Допустим, BPMN показывает:
Получить заявку -> Проверить -> Согласовать
Из процесса можно выделить сущность:
Application
State Machine покажет:
Draft -> Submitted -> UnderReview -> Approved
ERD покажет:
Student 1:N Application N:1 Program
А Sequence Diagram:
React -> Django -> PostgreSQL
Один и тот же сценарий описывается с разных сторон.
Поэтому ERD не должна пытаться показать бизнес-процесс или порядок API-вызовов.
Типичные ошибки ERD
Одна из самых частых ошибок - называть сущность действием.
Плохо:
CreateApplication
SendDocument
ApproveUser
Для ERD обычно лучше:
Application
Document
User
Действия относятся к поведению, а ERD описывает данные.
Вторая ошибка - отсутствие Primary Key.
Если запись нельзя однозначно идентифицировать, работать с ней становится значительно сложнее.
Третья ошибка - бесконтрольное использование nullable полей.
Если таблица содержит:
field_1 NULL
field_2 NULL
field_3 NULL
field_4 NULL
field_5 NULL
стоит проверить, не смешиваются ли внутри одной таблицы несколько разных сущностей или состояний.
Четвёртая ошибка - M:N без осознания промежуточной сущности.
Student M:N Course
почти всегда требует:
Enrollment
Особенно если у связи есть собственные атрибуты.
Пятая ошибка - хранить большую часть бизнес-модели в одном JSON.
PostgreSQL поддерживает jsonb, и это очень полезный инструмент.
Но конструкция:
Application
id
data JSONB
может скрыть:
- связи;
- constraints;
- обязательность;
- типы;
- бизнес-смысл.
JSONB полезен для действительно гибких или слабоструктурированных данных, но не должен автоматически заменять реляционную модель.
Шестая ошибка - бессистемное использование справочников и enum.
Один status хранится как строка, другой как integer, третий через отдельную таблицу, четвёртый в JSON.
Подобные решения должны иметь единый принцип.
И ещё одна ошибка - пытаться показать в ERD всё устройство приложения.
ERD не должна превращаться в:
Frontend -> API -> RabbitMQ -> Redis -> PostgreSQL
Для такой задачи лучше использовать архитектурную диаграмму, например C4.
Как проверить ERD
Перед тем как считать модель законченной, полезно задать несколько вопросов.
- У каждой ли сущности есть понятный смысл?
- Есть ли Primary Key?
- Понятна ли кардинальность каждой связи?
- Определена ли обязательность?
- Правильно ли расположены Foreign Keys?
- Есть ли M:N связи, которым нужна отдельная сущность?
- Не дублируются ли одни и те же данные?
- Можно ли хранить сущность независимо от связанных объектов?
- Нужна ли история изменений?
- Понятно ли поведение при удалении?
- Не скрыта ли реляционная модель внутри большого JSON?
- Не смешана ли ERD с API, BPMN или инфраструктурой?
Если на эти вопросы есть ясные ответы, модель уже можно постепенно переносить в физическую схему базы данных.
Что важно запомнить
ERD описывает структуру данных через три основных понятия:
Entity - что существует
Attribute - какие свойства у этого есть
Relationship - как объекты связаны
Primary Key позволяет однозначно определить запись:
Application.id
Foreign Key создаёт связь:
Application.student_id -> Student.id
Кардинальность показывает количество связанных объектов:
1:1 - один к одному
1:N - один ко многим
M:N - многие ко многим
Many-to-many обычно превращается в отдельную сущность:
Student M:N Course -> Enrollment
Нормализация помогает хранить один факт в одном месте и уменьшает противоречия.
А переход к реальной базе можно представить так:
Требования -> сущности -> связи -> ERD -> PostgreSQL schema -> constraints и indexes -> Django ORM
Главная цель ERD не в том, чтобы нарисовать много прямоугольников и соединить их линиями.
Хорошая ERD позволяет посмотреть на модель и понять:
какие данные существуют -> кому они принадлежат -> как связаны -> какие ограничения между ними действуют
После этого модель становится основой для PostgreSQL, ORM, API и дальнейшей разработки информационной системы.