От PRD к схеме базы данных за 30 минут: доменное моделирование с AI
Что такое проектирование схемы БД с помощью AI?
Проектирование схемы базы данных с помощью AI — это практика использования языковых моделей для ускоренного превращения документов с требованиями (PRD) в production-ready PostgreSQL-схемы. Это важно потому, что 70% ошибок в архитектуре БД закладываются в первые два часа работы, а их исправление после запуска обходится в 10–50 раз дороже, чем на этапе моделирования. В структурированном workflow AI сжимает цикл «гипотеза — валидация — исправление» с часов до минут: генерирует список сущностей, граф связей и SQL-миграции прямо из текста требований.
TL;DR
- -70% ошибок в архитектуре БД закладываются в первые два часа — исправление на этапе моделирования обходится в 10–50 раз дешевле, чем после запуска.
- -Четырёхшаговый workflow с AI (извлечение сущностей → построение связей → генерация SQL → валидация) превращает PRD в production-ready PostgreSQL-схему за 30 минут.
- -AI стабильно пропускает поля аудита (created_at, updated_at, created_by) и RLS-политики — добавляйте явные инструкции для обоих в каждый промпт.
- -Валидируйте схемы на трёх уровнях: формальная корректность SQL, покрытие User Stories (все сценарии выполнимы через ≤2 JOIN), анализ кардинальности под 12-месячную нагрузку.
- -В SaaS с 500 организациями таблицы задач и комментариев достигают миллионов строк за год — составные индексы на (project_id, status) и (task_id, created_at) нужны с первого дня.
70% ошибок в архитектуре базы данных закладываются в первые два часа проектирования. Пропущенная связь many-to-many, неправильная нормализация, отсутствие индексов на частых запросах. Исправление этих ошибок после запуска стоит в 10-50 раз дороже, чем на этапе моделирования.
AI меняет экономику этого процесса. Не потому что генерирует идеальные схемы автоматически, а потому что сжимает цикл «гипотеза — валидация — исправление» с часов до минут. PRD на входе, проверенная PostgreSQL-схема на выходе. 30 минут вместо двух дней.
Дальше — четыре шага, которые превращают текстовый документ с требованиями в production-ready миграцию: извлечение сущностей, построение связей, генерация SQL, валидация.
Шаг 1: извлечение сущностей из PRD
PRD описывает продукт языком бизнеса. База данных работает с сущностями, атрибутами и связями. Задача первого шага — пересечь эту границу.
Типичный PRD содержит разделы: User Stories, Functional Requirements, Non-Functional Requirements. Сущности скрываются в существительных User Stories и описаниях функциональности.
Промпт для извлечения сущностей
Ты — senior database architect. Проанализируй PRD и извлеки все доменные сущности.
Для каждой сущности определи:
1. Имя (snake_case, единственное число)
2. Бизнес-описание (одно предложение)
3. Ключевые атрибуты с типами данных
4. Является ли сущность основной (core) или вспомогательной (lookup/junction)
PRD:
"""
{вставить текст PRD}
"""
Формат ответа — таблица. Не добавляй сущности, которые явно не следуют из требований.
Пример: PRD трекера задач
Допустим, PRD описывает систему управления проектами. Раздел User Stories содержит:
Как менеджер проекта, я хочу создавать проекты, добавлять участников и назначать задачи. Как участник, я хочу видеть назначенные мне задачи, оставлять комментарии и прикреплять файлы. Как администратор, я хочу управлять ролями и правами доступа в рамках организации.
AI извлекает сущности:
| Сущность | Тип | Описание |
|---|---|---|
organization | core | Организация-владелец проектов |
user | core | Пользователь системы |
project | core | Проект внутри организации |
task | core | Задача внутри проекта |
comment | core | Комментарий к задаче |
attachment | core | Прикреплённый файл |
role | lookup | Роль пользователя (admin, manager, member) |
organization_member | junction | Связь пользователя с организацией и ролью |
project_member | junction | Связь пользователя с проектом |
Девять сущностей из трёх User Stories. Обратите внимание: AI выделил junction-таблицы organization_member и project_member, которые в PRD не упоминаются явно, но следуют из фразы «добавлять участников».
Что проверять на этом этапе
Три типичные ошибки AI на этапе извлечения:
Переизбыток сущностей. AI создаёт отдельные таблицы для каждого упомянутого понятия. «Приоритет задачи» становится таблицей priority, хотя достаточно enum-поля. Правило: если у понятия нет собственных атрибутов и оно не растёт со временем, это поле, а не таблица.
Пропуск аудита. PRD редко содержит требования вроде «записывать, кто и когда изменил задачу». Но в продакшене created_at, updated_at, created_by нужны почти всегда. Добавьте в промпт явное указание: «Включи стандартные поля аудита для каждой сущности».
Смешение доменной и инфраструктурной модели. Таблицы session, audit_log, notification_queue — это инфраструктура. На этапе domain modeling они отвлекают. Фокус на бизнес-сущностях.
Шаг 2: построение связей между сущностями
Сущности без связей — просто список таблиц. Связи определяют, как данные взаимодействуют. Этот шаг критичен: ошибки здесь приводят к невозможным запросам или дублированию данных.
Промпт для построения связей
На основе списка сущностей определи все связи между ними.
Для каждой связи укажи:
1. Тип: one-to-one, one-to-many, many-to-many
2. Обязательность (nullable или NOT NULL)
3. Правило каскадного удаления (CASCADE, SET NULL, RESTRICT)
4. Направление владения (какая сторона "владеет" связью)
Сущности:
"""
{список сущностей из шага 1}
"""
Формат: таблица связей. Для many-to-many укажи junction-таблицу.
Результат: граф связей
organization 1──N project (CASCADE)
organization 1──N organization_member (CASCADE)
user 1──N organization_member (CASCADE)
role 1──N organization_member (RESTRICT)
project 1──N project_member (CASCADE)
user 1──N project_member (CASCADE)
project 1──N task (CASCADE)
task N──1 user (SET NULL, nullable — assignee)
task N──1 user (RESTRICT — creator)
task 1──N comment (CASCADE)
user 1──N comment (RESTRICT)
task 1──N attachment (CASCADE)
user 1──N attachment (RESTRICT)
Здесь видно принципиальные решения. Удаление организации каскадно удаляет проекты и участников. Удаление пользователя, назначенного на задачу, обнуляет assignee_id (SET NULL), но не удаляет задачу. Удаление роли запрещено, если есть участники с этой ролью (RESTRICT).
Ловушка: скрытые many-to-many
PRD говорит: «пользователь назначается на задачу». Звучит как one-to-many, у задачи один assignee. Но через два спринта появляется требование «несколько исполнителей на одной задаче». One-to-many превращается в many-to-many, требуется junction-таблица, миграция данных, переписывание запросов.
Правило: если бизнес-логика допускает расширение «один → несколько», заложите many-to-many сразу. Стоимость пустой junction-таблицы близка к нулю. Стоимость миграции на продакшене — дни работы.
Задайте AI прямой вопрос:
Для каждой связи one-to-many оцени вероятность того,
что она станет many-to-many в ближайшие 6 месяцев.
Обоснуй на основе типичной эволюции подобных продуктов.
Шаг 3: генерация PostgreSQL-схемы
Сущности определены, связи описаны. Следующий шаг — SQL-миграция. Здесь AI экономит больше всего времени: правильные типы данных, индексы, constraints, комментарии.
Промпт для генерации SQL
Сгенерируй PostgreSQL-миграцию на основе списка сущностей и связей.
Требования:
- UUID v4 для первичных ключей (gen_random_uuid())
- created_at/updated_at TIMESTAMPTZ на каждой таблице
- Индексы на все внешние ключи
- Составные уникальные ограничения где логически необходимо
- CHECK constraints для enum-подобных полей
- Комментарии COMMENT ON TABLE/COLUMN для документации
Сущности и связи:
"""
{результат шагов 1 и 2}
"""
Формат: один SQL-файл, таблицы в порядке зависимостей (referenced перед referencing).
Результат: миграция
-- Migration: 001_initial_schema.sql
-- Generated from PRD: Project Management System
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- ============================================================
-- Lookup tables
-- ============================================================
CREATE TABLE role (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(50) NOT NULL UNIQUE,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE role IS 'Роли пользователей в организации';
INSERT INTO role (name, description) VALUES
('admin', 'Полный доступ к организации'),
('manager', 'Управление проектами и участниками'),
('member', 'Базовый доступ к назначенным проектам');
-- ============================================================
-- Core tables
-- ============================================================
CREATE TABLE organization (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE organization IS 'Организация — верхний уровень иерархии';
CREATE INDEX idx_organization_slug ON organization (slug);
CREATE TABLE "user" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(320) NOT NULL UNIQUE,
full_name VARCHAR(255) NOT NULL,
avatar_url TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE "user" IS 'Пользователь системы';
CREATE INDEX idx_user_email ON "user" (email);
CREATE TABLE organization_member (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organization (id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES "user" (id) ON DELETE CASCADE,
role_id UUID NOT NULL REFERENCES role (id) ON DELETE RESTRICT,
joined_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (organization_id, user_id)
);
COMMENT ON TABLE organization_member IS 'Членство пользователя в организации';
CREATE INDEX idx_org_member_org ON organization_member (organization_id);
CREATE INDEX idx_org_member_user ON organization_member (user_id);
CREATE TABLE project (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organization (id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
description TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'archived', 'completed')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE project IS 'Проект внутри организации';
CREATE INDEX idx_project_org ON project (organization_id);
CREATE INDEX idx_project_status ON project (status);
CREATE TABLE project_member (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES project (id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES "user" (id) ON DELETE CASCADE,
added_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (project_id, user_id)
);
COMMENT ON TABLE project_member IS 'Участие пользователя в проекте';
CREATE INDEX idx_proj_member_project ON project_member (project_id);
CREATE INDEX idx_proj_member_user ON project_member (user_id);
CREATE TABLE task (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES project (id) ON DELETE CASCADE,
created_by UUID NOT NULL REFERENCES "user" (id) ON DELETE RESTRICT,
assignee_id UUID REFERENCES "user" (id) ON DELETE SET NULL,
title VARCHAR(500) NOT NULL,
description TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'todo'
CHECK (status IN ('todo', 'in_progress', 'review', 'done')),
priority SMALLINT NOT NULL DEFAULT 0
CHECK (priority BETWEEN 0 AND 3),
due_date DATE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE task IS 'Задача внутри проекта';
COMMENT ON COLUMN task.priority IS '0=none, 1=low, 2=medium, 3=high';
CREATE INDEX idx_task_project ON task (project_id);
CREATE INDEX idx_task_assignee ON task (assignee_id);
CREATE INDEX idx_task_status ON task (status);
CREATE INDEX idx_task_created_by ON task (created_by);
CREATE TABLE comment (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
task_id UUID NOT NULL REFERENCES task (id) ON DELETE CASCADE,
author_id UUID NOT NULL REFERENCES "user" (id) ON DELETE RESTRICT,
body TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE comment IS 'Комментарий к задаче';
CREATE INDEX idx_comment_task ON comment (task_id);
CREATE INDEX idx_comment_author ON comment (author_id);
CREATE TABLE attachment (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
task_id UUID NOT NULL REFERENCES task (id) ON DELETE CASCADE,
uploaded_by UUID NOT NULL REFERENCES "user" (id) ON DELETE RESTRICT,
file_name VARCHAR(500) NOT NULL,
file_url TEXT NOT NULL,
file_size BIGINT NOT NULL CHECK (file_size > 0),
mime_type VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE attachment IS 'Прикреплённый файл к задаче';
CREATE INDEX idx_attachment_task ON attachment (task_id);
-- ============================================================
-- Trigger: auto-update updated_at
-- ============================================================
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_organization_updated BEFORE UPDATE ON organization
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER trg_user_updated BEFORE UPDATE ON "user"
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER trg_project_updated BEFORE UPDATE ON project
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER trg_task_updated BEFORE UPDATE ON task
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER trg_comment_updated BEFORE UPDATE ON comment
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER trg_role_updated BEFORE UPDATE ON role
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
Разбор ключевых решений
UUID вместо SERIAL. В распределённых системах и при работе с Supabase UUID предпочтительнее. Нет коллизий при merge данных из разных окружений, нет утечки информации о количестве записей.
CHECK constraints вместо отдельных таблиц. status и priority заданы CHECK-ами. Если список значений меняется раз в год, CHECK достаточно. Если значения управляются пользователем (кастомные статусы), нужна отдельная таблица.
Триггер updated_at. Один триггер на все таблицы. Без него updated_at требует явного обновления в каждом запросе, что ведёт к рассинхронизации.
Шаг 4: валидация PostgreSQL-схемы
Сгенерированная схема требует трёхуровневой проверки перед коммитом.
Уровень 1: формальная корректность
Запустите миграцию на чистой базе. Все ли таблицы создаются без ошибок? Все ли foreign keys ссылаются на существующие таблицы?
psql -d test_db -f 001_initial_schema.sql
Если миграция выполняется без ошибок, формальная корректность пройдена.
Уровень 2: покрытие требований
Для каждой User Story из PRD напишите SQL-запрос, который реализует её. Если запрос невозможен или требует JOIN через пять таблиц, схема неоптимальна.
Для каждой User Story из PRD напиши SQL-запрос,
который реализует основной сценарий.
Отметь запросы, которые требуют более двух JOIN
или не могут быть выполнены на текущей схеме.
Пример проверки для «менеджер видит все задачи проекта с именами исполнителей»:
SELECT
t.id,
t.title,
t.status,
t.priority,
u.full_name AS assignee_name,
t.due_date
FROM task t
LEFT JOIN "user" u ON u.id = t.assignee_id
WHERE t.project_id = :project_id
ORDER BY t.priority DESC, t.created_at;
Один JOIN, индекс на project_id уже создан. Запрос оптимален.
Уровень 3: нагрузочный анализ
Для каждой таблицы оцените ожидаемый объём данных через 12 месяцев. Это определяет, какие индексы критичны.
Оцени кардинальность каждой таблицы через 12 месяцев
для SaaS-продукта с 500 организациями,
средний размер организации — 20 пользователей.
На основе оценок проверь:
1. Достаточно ли текущих индексов для типичных запросов?
2. Нужны ли составные индексы?
3. Нужна ли партиционирование для больших таблиц?
Типичный результат для трекера:
| Таблица | Строк через 12 мес. | Индексы достаточны |
|---|---|---|
organization | 500 | Да |
user | 10 000 | Да |
task | 500 000 | Добавить составной (project_id, status) |
comment | 2 000 000 | Добавить составной (task_id, created_at) |
attachment | 200 000 | Да |
На основе этого анализа добавьте составные индексы:
CREATE INDEX idx_task_project_status ON task (project_id, status);
CREATE INDEX idx_comment_task_created ON comment (task_id, created_at);
Полный workflow: 30 минут от PRD до схемы
Разбивка по времени:
| Шаг | Время | Результат |
|---|---|---|
| Извлечение сущностей | 5 мин | Список из 8-15 сущностей с типами |
| Построение связей | 5 мин | Граф связей с каскадными правилами |
| Генерация SQL | 10 мин | Миграция с индексами и constraints |
| Валидация | 10 мин | Проверенная схема с запросами |
Ключевой принцип: AI генерирует, человек валидирует. Каждый шаг завершается ревью. Не передавайте результат одного промпта в следующий без проверки.
Типичные ошибки AI при моделировании схемы
Избыточная нормализация. AI создаёт таблицу address с полями country, city, street, zip. Для MVP часто достаточно JSONB-поля. Нормализуйте, когда появится потребность в запросах по отдельным полям адреса.
Отсутствие soft delete. Для сущностей, где удаление должно быть обратимым (задачи, комментарии, проекты), добавьте deleted_at TIMESTAMPTZ. AI не сделает этого без явной инструкции.
Игнорирование RLS. При работе с Supabase Row Level Security — не опция, а необходимость. Добавьте в промпт: «Сгенерируй RLS-политики для каждой таблицы на основе ролевой модели из PRD». Подробнее о защите данных на edge — в статье про circuit breaker в edge functions.
Отсутствие partial indexes. Для полей с низкой кардинальностью (status, is_active) обычные B-tree индексы неэффективны. Partial index WHERE status = 'active' занимает меньше места и работает быстрее для типичного запроса.
Расширение: от схемы к типобезопасному коду
Сгенерированная схема — не конечная точка. Следующий шаг — типизация на уровне приложения. Для TypeScript + Supabase:
npx supabase gen types typescript --local > src/types/database.ts
Эта команда превращает PostgreSQL-схему в TypeScript-типы. Каждая таблица становится интерфейсом, каждый столбец — полем с правильным типом. Изменение схемы автоматически обновляет типы, компилятор ловит несоответствия.
Для более сложных сценариев промпт-инженерии и управления контекстом при генерации кода полезен гайд по context engineering.
Чеклист: что проверить перед коммитом миграции
- Все таблицы создаются в порядке зависимостей (нет forward references)
- Каждый foreign key имеет индекс
ON DELETEправила соответствуют бизнес-логикеupdated_atобновляется триггером, а не вручную- Enum-подобные поля используют CHECK или отдельную таблицу (сознательный выбор)
- UUID вместо SERIAL для первичных ключей в распределённых системах
- Составные уникальные ограничения на junction-таблицах
- Комментарии на таблицах и неочевидных столбцах
- Partial indexes для полей с низкой кардинальностью
- RLS-политики (для Supabase)
Этот процесс масштабируется. PRD из 3 User Stories порождает 9 таблиц. PRD из 30 User Stories — 40-60 таблиц. Структура промптов и порядок шагов не меняются. Меняется только время на валидацию: чем больше сущностей, тем больше связей проверять. AI берёт на себя рутину генерации. Инженер фокусируется на решениях, которые определят жизнь системы на годы вперёд.
Нужна помощь с проектированием базы данных? Я помогаю стартапам внедрять AI-решения и строить продукты — belov.works.
FAQ
Можно ли использовать этот workflow с ORM (Prisma, SQLAlchemy) вместо сырого SQL?
Да. Пройдите четыре шага до получения валидированной SQL-миграции, а затем попросите модель преобразовать её в формат схемы вашего ORM. Начинать с SQL безопаснее, чем генерировать ORM-схему напрямую: SQL-шаг вынуждает явно принять решения по cascade rules и индексам, которые ORM обычно оставляет по умолчанию.
Как обрабатывать изменения схемы при эволюции PRD в процессе разработки?
Воспринимайте дифф PRD как новый входной документ. Передайте в промпт оригинальный список сущностей, текущую схему и изменённые требования, попросив модель определить, что нужно изменить. Генерируйте только дельта-миграцию, а не полную перезапись. После любого изменения схемы обязательно повторяйте второй уровень валидации (покрытие User Stories) — новые требования часто ломают запросы, которые раньше работали корректно.
Каков практический предел сложности схемы, при котором workflow перестаёт работать?
Workflow надёжно справляется с 40–60 сущностями в одном контекстном окне. Сверх этого модель начинает генерировать непоследовательные FK-ссылки и пропускать индексы. Решение: разбить домен на ограниченные контексты, прогнать четыре шага для каждого, а затем добавить финальный шаг для связей между контекстами.