Раздел 3 — Проектирование БД и ER-моделирование

Как превратить описание задачи на человеческом языке в схему базы данных

Прогресс курса Раздел 3 из 13

Что вы освоите в этом разделе

8 академических часов: 2 часа теории, 6 часов практики. Это самый объёмный раздел курса и единственный, где нет одного правильного ответа. Проектирование — инженерное решение, а не вычисление.
01

Три этапа проектирования

Между фразой заказчика «нам нужен учёт заказов» и работающей таблицей в PostgreSQL лежат три этапа. Их разделяют, потому что на каждом решаются разные вопросы и участвуют разные люди.

Этапы

  • Концептуальное (инфологическое) проектирование. Что за объекты есть в предметной области и как они связаны. Результат — ER-модель. СУБД ещё не выбрана; модель понятна заказчику и обсуждается с ним.
  • Логическое (даталогическое) проектирование. Перевод ER-модели в конкретную модель данных — для нас реляционную. Результат — набор отношений с ключами и ограничениями. Здесь же выполняется нормализация (раздел 4).
  • Физическое проектирование. Типы данных конкретной СУБД, индексы, размещение файлов, партиционирование. Результат — готовые команды CREATE TABLE. Зависит от версии PostgreSQL и объёма данных.
ЭтапОперирует понятиямиРезультатЗависит от СУБД
Концептуальныйсущность, атрибут, связьER-диаграмманет
Логическийотношение, ключ, зависимостьсхема отношенийнет
Физическийтаблица, тип, индексDDL-скриптда
Самая частая ошибка новичка — начать сразу с третьего этапа: открыть pgAdmin и создавать таблицы. Тогда структура получается такой, какой её увидел один человек за один вечер, а обсудить её с заказчиком невозможно — он не читает DDL. Через месяц выясняется, что у клиента бывает несколько адресов, и схему приходится ломать вместе с накопленными данными.
02

Анализ предметной области

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

Откуда берутся требования

  • Интервью с заказчиком и будущими пользователями — основной источник. Разговаривать нужно не только с руководителем, но и с теми, кто будет вводить данные.
  • Существующие документы — бланки, накладные, журналы, отчёты. Каждое поле бумажного бланка — кандидат в атрибут.
  • Действующая система — если учёт уже ведётся в Excel или в старой программе.
  • Нормативные требования — что обязаны хранить по закону и сколько лет.
  • Отчёты, которые от базы ждут — самый недооценённый источник. Если от системы потребуют отчёт «выручка по менеджерам за квартал», значит, у заказа обязан быть менеджер и дата.

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

Возьмём фрагмент требований:

«Клиент оформляет заказ. В заказе несколько товаров, у каждого указано количество. Товар относится к категории. Заказ обрабатывает менеджер.»

Разбор:

СловоРольКомментарий
клиентсущностьу него будут свои атрибуты
оформляетсвязьклиент → заказ, один-ко-многим
заказсущность
товарсущность
количествоатрибут связипринадлежит паре «заказ — товар», а не товару
категориясущностьне атрибут: у неё будет своё имя и описание
обрабатываетсвязьменеджер → заказ
менеджерсущность

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

Сущность или атрибут

  • Есть ли у объекта собственные характеристики? У категории есть название и описание — сущность. У пола пользователя ничего нет — атрибут.
  • Нужно ли хранить объект сам по себе, даже если на него никто не ссылается? Категорию без товаров хранить нужно — сущность.
  • Может ли значений быть несколько? У клиента несколько телефонов — значит, телефон становится отдельной сущностью или таблицей.
  • Повторяется ли значение во многих строках? Город повторяется тысячу раз — кандидат на вынос в справочник.
Отдельно фиксируйте бизнес-правила — утверждения, которые схема должна гарантировать: «сумма заказа не может быть отрицательной», «нельзя записать двух пациентов на одно время к одному врачу», «удалённый товар не исчезает из прошлых заказов». Часть из них станет ограничениями, часть — связями, часть придётся проверять в коде. Правило, не записанное на этом этапе, будет нарушено данными.
03

Сущности, атрибуты и связи

ER-модель (Entity — Relationship, «сущность — связь») предложена Питером Ченом в 1976 году. В ней всего три строительных блока.

Три понятия

  • Сущность (Entity) — класс объектов, о которых хранятся данные: «Клиент», «Заказ». Отдельный объект называется экземпляром сущности: клиент Иванов. Сущность станет таблицей, экземпляр — строкой.
  • Атрибут (Attribute) — свойство сущности: имя клиента, дата заказа. Станет столбцом.
  • Связь (Relationship) — осмысленная ассоциация между сущностями: клиент оформляет заказ. Станет внешним ключом или отдельной таблицей.
Соглашение об именовании: сущность называют существительным в единственном числе («Заказ»), связь — глаголом («оформляет»). Таблицу при этом чаще называют во множественном числе и латиницей (orders). Главное — выбрать одно соглашение и держаться его во всей базе; смешение users и zakaz в одной схеме — верный признак того, что её писали второпях.
04

Виды атрибутов

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

Классификация

  • Простой — неделим: возраст, цена.
  • Составной — распадается на части: ФИО → фамилия, имя, отчество; адрес → город, улица, дом. В таблице обычно раскладывается на отдельные столбцы.
  • Однозначный — одно значение на экземпляр: дата рождения.
  • Многозначный — несколько значений: телефоны клиента, специализации врача. Всегда выносится в отдельную таблицу — иначе нарушается атомарность.
  • Производный (вычисляемый) — выводится из других: возраст из даты рождения, сумма заказа из позиций. Хранить не обязательно, часто вредно.
  • Ключевой — однозначно определяет экземпляр.
  • Необязательный — может отсутствовать, в схеме допускает NULL.

Составной атрибут раскладывать или нет — решение по ситуации:

-- Вариант А: адрес одной строкой
address varchar(300)   -- 'Москва, ул. Ленина, д. 5, кв. 12'

-- Вариант Б: разложен на части
city     varchar(100),
street   varchar(150),
building varchar(20),
flat     varchar(20)

Если по городу нужен отчёт или фильтр — только вариант Б. Если адрес нужен лишь для печати на конверте — достаточно А. Вопрос решается тем, будете ли вы искать по части значения.

Производный атрибут — ловушка. Хранить возраст числом нельзя: он устаревает каждый день, и база начнёт врать. Хранят дату рождения, а возраст вычисляют. Сумму заказа, наоборот, часто сохраняют — но по другой причине: цена товара со временем меняется, а в документе должна остаться цена на момент покупки. Разница в том, изменится ли исходное значение задним числом.
05

Кардинальность и модальность связи

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

Кардинальность

Сколько экземпляров участвует. Отвечает на вопрос «сколько заказов у клиента?» — один или много. Даёт типы 1:1, 1:М, М:М.

Модальность (обязательность)

Обязательно ли участие. Отвечает на вопрос «может ли клиент существовать без заказов?» — да или нет. Даёт NULL / NOT NULL на внешнем ключе.

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

«Один клиент может оформить ноль или более заказов. Один заказ оформлен ровно одним клиентом.»

Отсюда сразу читается схема: внешний ключ client_id в таблице заказов, объявлен NOT NULL, потому что заказ без клиента невозможен.

Сравните с другой связью:

«Один заказ обрабатывается нулём или одним менеджером (пока не назначен — нулём). Один менеджер обрабатывает ноль или более заказов.»

Здесь внешний ключ manager_id допускает NULL. Разница в одном слове требований — разница в ограничении схемы.

Проверяйте модальность вопросом «а бывает ли ноль?». Бывает ли заказ без позиций — в момент создания корзины да. Бывает ли позиция заказа без заказа — нет, никогда. Ответы на эти вопросы задаёт предметная область, а не ваше удобство.
06

Нотация «вороньи лапки»

Нотаций для ER-диаграмм несколько: Чена (ромбы и овалы), IDEF1X (используется в отечественных стандартах), UML, Мартина. На практике чаще всего применяют нотацию Мартина, известную как Crow's Foot — «вороньи лапки». Её понимают все инструменты моделирования.

Каждый конец линии несёт два символа: ближний обозначает модальность, дальний — кардинальность.

СимволЧитаетсяСмысл
──||ровно одинобязательно, единственный
──o|ноль или одиннеобязательно, не более одного
──|<один или болееобязательно, много
──o<ноль или болеенеобязательно, много

Знак «вороньей лапки» < — это сторона «много». Кружок o — «может быть ноль». Вертикальная черта | — «ровно один».

Связь «клиент — заказ» в этой нотации:

   Клиент                                    Заказ
┌───────────────┐                     ┌────────────────────┐
│ PK id         │                     │ PK id              │
│    ФИО        │──||───────────o<────│ FK client_id       │
│    телефон    │                     │ FK manager_id (0..1)│
│    email      │                     │    дата            │
└───────────────┘                     │    статус          │
                                      └────────────────────┘

Читается слева направо: один заказ оформлен ровно одним клиентом.
Читается справа налево: один клиент оформил ноль или более заказов.

Связь «многие-ко-многим» на концептуальном уровне рисуют напрямую, а на логическом раскрывают через связующую сущность:

Концептуальный уровень — связь М:М показана как есть:

   Заказ                                    Товар
┌──────────┐                            ┌──────────┐
│ PK id    │──|<──────────────────o<────│ PK id    │
└──────────┘   содержит                 └──────────┘


Логический уровень — появилась связующая сущность:

   Заказ                Позиция заказа              Товар
┌──────────┐          ┌────────────────┐       ┌──────────┐
│ PK id    │──||──o<──│ PK,FK order_id │       │ PK id    │
│   дата   │          │ PK,FK product_id│──||──│   название│
└──────────┘          │    количество  │  o<   │   цена   │
                      │    цена_покупки│       └──────────┘
                      └────────────────┘

«Позиция заказа» несёт собственные атрибуты — количество и цену
на момент покупки. Это признак того, что связующая сущность
обязана существовать, а не является технической подпоркой.
Цена в позиции заказа дублирует цену товара намеренно. Товар подорожает — прошлые заказы должны остаться с прежними суммами. Это не ошибка нормализации, а требование предметной области: документ фиксирует состояние на момент совершения операции.
07

Слабые сущности

Слабая сущность не существует самостоятельно и не имеет собственного ключа — она идентифицируется только через родительскую.

Признаки слабой сущности

  • Не может существовать без родителя: позиция заказа без заказа бессмысленна.
  • Ключ включает ключ родителя: (order_id, product_id).
  • Удаляется вместе с родителем — типичный случай для ON DELETE CASCADE.

Примеры: позиция заказа, строка накладной, комментарий к посту, оценка в ведомости. Связь с родителем называется идентифицирующей.

Практическая польза от распознавания слабых сущностей — вы сразу знаете три вещи: составной первичный ключ, NOT NULL на внешнем ключе и ON DELETE CASCADE. Три решения из определения, без размышлений.
08

Переход от ER-модели к реляционной схеме

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

Правила преобразования

  • Правило 1. Сущность → таблица. Каждая сильная сущность становится таблицей, её ключевой атрибут — первичным ключом.
  • Правило 2. Простой атрибут → столбец. Тип подбирается на физическом этапе.
  • Правило 3. Составной атрибут → несколько столбцов либо один, если части по отдельности не нужны.
  • Правило 4. Многозначный атрибут → отдельная таблица с внешним ключом на владельца.
  • Правило 5. Производный атрибут → не хранится, вычисляется запросом или представлением. Исключение — значение, которое обязано «застыть» на момент операции.
  • Правило 6. Связь 1:М → внешний ключ на стороне «многих». Обязательность связи даёт NOT NULL.
  • Правило 7. Связь М:М → связующая таблица с двумя внешними ключами; её первичный ключ — составной из них, плюс собственные атрибуты связи.
  • Правило 8. Связь 1:1 → внешний ключ с UNIQUE в той таблице, где участие обязательно; либо общий первичный ключ.
  • Правило 9. Слабая сущность → таблица с составным ключом, включающим ключ родителя, и каскадным удалением.

Применим правила к разобранному фрагменту про магазин:

-- Правило 1, 2: сущности «Категория», «Товар», «Клиент», «Менеджер», «Заказ»

CREATE TABLE categories (
    id    integer      PRIMARY KEY,
    name  varchar(100) NOT NULL UNIQUE
);

-- Правило 6: связь «товар относится к категории», 1:М, обязательная
CREATE TABLE products (
    id          integer       PRIMARY KEY,
    category_id integer       NOT NULL REFERENCES categories(id),
    name        varchar(200)  NOT NULL,
    price       numeric(12,2) NOT NULL CHECK (price >= 0)
);

-- Правило 6: клиент обязателен, менеджер — нет (модальность 0..1)
CREATE TABLE orders (
    id         integer     PRIMARY KEY,
    client_id  integer     NOT NULL REFERENCES clients(id),
    manager_id integer     REFERENCES managers(id),  -- NULL допустим
    created_at timestamptz NOT NULL DEFAULT now(),
    status     varchar(20)  NOT NULL
);

-- Правило 7 + 9: связь М:М со своими атрибутами = слабая сущность
CREATE TABLE order_items (
    order_id   integer       NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
    product_id integer       NOT NULL REFERENCES products(id),
    quantity   integer       NOT NULL CHECK (quantity > 0),
    price      numeric(12,2) NOT NULL,  -- цена на момент покупки
    PRIMARY KEY (order_id, product_id)
);

Обратите внимание на два разных ON DELETE в последней таблице. Заказ удаляется — позиции уходят вместе с ним (CASCADE). Товар удалить нельзя, пока он есть в заказах — по умолчанию NO ACTION, и это правильно: история покупок не должна разрушаться.

Обычно товары вообще не удаляют, а помечают снятыми с продажи флагом is_active или датой archived_at. Приём называется «мягкое удаление» и решает конфликт между «убрать из каталога» и «сохранить в истории».
09

Ловушки проектирования

Две классические ошибки ER-моделирования выглядят на диаграмме безобидно, а проявляются только тогда, когда запрос начинает возвращать неправильные данные.

Ловушка разветвления (fan trap)

Возникает, когда от одной сущности расходятся две связи 1:М, и путь между «листьями» через центр кажется существующим, но неоднозначен.

Отдел ──||───o<── Сотрудник
  │
  ||
  │
  o<
Проект

Вопрос: «над каким проектом работает Иванов?»
Ответ через отдел получить нельзя: в отделе три проекта
и десять сотрудников, а кто над чем работает — неизвестно.
Связь «сотрудник — проект» просто не смоделирована.

Исправление: добавить связь напрямую, чаще всего М:М.

Сотрудник ──|<── Участие ──o<── Проект

Ловушка разрыва (chasm trap)

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

Отдел ──||───o<── Сотрудник ──o|───o<── Кабинет
                              ↑
                    сотрудник может не иметь кабинета

Вопрос: «какие кабинеты закреплены за отделом?»
Кабинеты, за которыми не закреплён ни один сотрудник,
в ответ не попадут — путь через сотрудника разорван.

Исправление: связать отдел и кабинет напрямую,
если такая связь существует в предметной области.
Обе ловушки ищутся одинаково: выпишите список вопросов, на которые база обязана отвечать, и для каждого проследите путь по диаграмме. Если путь неоднозначен или прерывается — модель неполна. Это единственная надёжная проверка ER-модели, и делать её нужно до того, как схема попадёт в код.
10

Инструменты моделирования

Чем рисовать

  • dbdiagram.io — схема описывается текстом на языке DBML, диаграмма строится автоматически. Быстрее всего для учебных задач, результат экспортируется в SQL.
  • draw.io (diagrams.net) — универсальный редактор диаграмм, есть готовые фигуры ER. Работает офлайн.
  • pgModeler — специализированный инструмент под PostgreSQL, умеет прямое и обратное проектирование.
  • DBeaver — строит диаграмму по существующей базе. Полезен, чтобы разобраться в чужой схеме.
  • Карандаш и бумага — на этапе обсуждения с заказчиком быстрее любого редактора.

Текстовая нотация DBML удобна тем, что схема хранится в git и правится как код:

// dbdiagram.io — описание схемы текстом
Table categories {
  id   integer [pk]
  name varchar [not null, unique]
}

Table products {
  id          integer [pk]
  category_id integer [not null]
  name        varchar [not null]
  price       decimal
}

Ref: products.category_id > categories.id  // много к одному
Диаграмма — средство коммуникации, а не отчётный документ. Её ценность в том, что заказчик посмотрел и сказал «нет, у клиента бывает несколько адресов». Красивая диаграмма, которую никто не обсуждал, стоит ровно ничего.

Ключевые выводы раздела

Запомните главное

  • Три этапа: концептуальный (ER-модель) → логический (отношения) → физический (DDL)
  • Начинать с CREATE TABLE — значит проектировать вслепую и без заказчика
  • Существительные из требований — кандидаты в сущности, глаголы — в связи
  • Сущность отличается от атрибута наличием собственных характеристик
  • Кардинальность отвечает «сколько», модальность — «обязательно ли»; вторая даёт NULL или NOT NULL
  • Связь формулируется словами с обеих сторон и с числами
  • Многозначный атрибут всегда выносится в отдельную таблицу
  • М:М раскрывается связующей сущностью; если у неё есть свои атрибуты — она полноценная сущность
  • Слабая сущность даёт составной ключ, NOT NULL и ON DELETE CASCADE
  • Модель проверяется списком вопросов, на которые база обязана отвечать
Практические задания — на учебном портале. Задания, критерии оценивания, сдача работ и оценки преподавателя — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Раздел 2: Реляционная модель Раздел 4: Нормализация