От PRD к схеме базы данных за 30 минут: доменное моделирование с AI
Что такое проектирование схемы БД с помощью AI?
Проектирование схемы базы данных с помощью AI — это практика использования языковых моделей для ускоренного превращения документов с требованиями (PRD) в production-ready PostgreSQL-схемы. Это важно потому, что большинство ошибок в архитектуре БД закладывается в первой сессии моделирования, а их исправление после запуска обходится значительно дороже, чем на этапе моделирования. В структурированном workflow AI сжимает цикл «гипотеза — валидация — исправление» с часов до минут: генерирует список сущностей, граф связей и SQL-миграции прямо из текста требований.
TL;DR
- -Большинство ошибок в архитектуре БД закладывается в первой сессии моделирования — исправление на раннем этапе обходится значительно дешевле, чем после запуска.
- -Четырёхшаговый 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) нужны с первого дня.
Большинство ошибок в архитектуре базы данных закладывается в первой сессии моделирования — ещё до написания кода. Пропущенная связь many-to-many, неправильная нормализация, отсутствие индексов на частых запросах. Исправление этих ошибок после запуска стоит на порядок дороже, чем на этапе моделирования.
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.