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

Схема базы данных — это план того, как ваше приложение будет запоминать вещи. На практике это:
Когда схема соответствует реальной работе, она отражает то, что люди действительно делают — создают, просматривают, утверждают, планируют, назначают, отменяют — а не то, что звучит аккуратно на доске.
Пользовательские истории и acceptance criteria описывают реальные нужды простым языком: кто что делает и что значит «готово». Если ориентироваться на них как на исходник, схема с меньшей вероятностью пропустит важные детали (например, «мы должны хранить, кто утвердил возврат» или «бронирование можно перенести несколько раз»).
Начинать со сторий также честно про масштаб: если этого нет в историях (или в workflow), считайте это опциональным, а не тихо строите сложную модель «на всякий случай».
ИИ может помочь работать быстрее, например:
ИИ не может надёжно:
Относитесь к ИИ как к мощному ассистенту, а не как к лицу, принимающему окончательное решение.
Если вы хотите превратить этого ассистента в реальное ускорение, платформа vibe-кодинга вроде Koder.ai может помочь быстрее перейти от решений по схеме к рабочему приложению React + Go + PostgreSQL — при этом вы остаётесь в контроле над моделью, ограничениями и миграциями.
Проектирование схемы — это цикл: черновик → тест по историям → поиск недостающих данных → правка. Цель не в идеальном первом результате; цель — модель, которую вы можете проследить до каждой пользовательской истории и уверенно сказать: «Да, мы можем сохранить всё, что нужно этому workflow — и объяснить, зачем нужна каждая таблица.»
Прежде чем превращать требования в таблицы, проясните, что именно вы моделируете. Хорошая схема редко начинается с пустой страницы — она начинается с конкретной работы людей и доказательств, которые вам понадобятся позже (экраны, выходные данные и крайние случаи).
Пользовательские истории — это заголовок, но их недостаточно. Соберите:
Если вы используете ИИ, эти входные данные закрепляют модель в реальности. ИИ может быстро предложить сущности и поля, но ему нужны реальные артефакты, чтобы не выдумать структуру, не соответствующую вашему продукту.
Acceptance criteria часто содержат самые важные правила хранения, даже если прямо не упоминают данные. Ищите формулировки вроде:
Расплывчатые истории («Как пользователь, я могу управлять проектами») скрывают множество сущностей и рабочих процессов. Ещё одна частая пустота — отсутствие крайних случаев: отмены, повторы, частичные возвраты или переназначения.
Прежде чем думать о таблицах или диаграммах, прочитайте пользовательские истории и выделите существительные. В написании требований сущительные обычно указывают на «вещи», которые система должна запомнить — они часто становятся сущностями в вашей схеме.
Короткая мысль: существительные → сущности, а глаголы → действия или workflows. Если в истории: «Менеджер назначает технику на задание», вероятные сущности — manager, technician, job — а «назначает» подсвечивает связь, которую вы смоделируете позже.
Не каждое существительное должно стать отдельной таблицей. Сильным кандидатом на сущность является существительное, когда оно:
Если существительное встречается только один раз или описывает что‑то иное («красная кнопка», «пятница»), то, возможно, это не сущность.
Обычная ошибка — превращать каждую деталь в таблицу. Правило‑эмпирика:
Два классических примера:
ИИ может ускорить обнаружение сущностей, просканировав истории и вернув черновой список существительных, сгруппированных по темам (люди, рабочие элементы, документы, локации). Полезный промпт: «Извлеки существительные, которые представляют данные, которые мы должны хранить, и сгруппируй дубликаты/синонимы.»
Относитесь к выводу как к отправной точке, а не к окончательному ответу. Задавайте уточняющие вопросы:
Цель Шага 1 — короткий, чистый список сущностей, который вы сможете защитить ссылками на реальные истории.
Как только вы назвали сущности (например, Order, Customer, Ticket), следующая задача — зафиксировать детали, которые понадобятся позже. В базе данных эти детали — поля (или атрибуты) — напоминания, которые система не должна забыть.
Начните с пользовательской истории, затем прочитайте acceptance criteria как чеклист того, что обязано храниться.
Если требование говорит «Пользователи могут фильтровать заказы по дате доставки», то delivery_date не опционален — это поле (или надёжно выводимое значение). Если сказано «Показывать, кто утвердил запрос и когда», скорее всего нужны approved_by и approved_at.
Практический тест: Понадобится ли это для отображения, поиска, сортировки, аудита или вычислений? Если да — вероятно поле.
First name и Last name отдельно, если будете искать или сортировать по ним. Не упаковывайте несколько значений в одно поле (например, «red, blue»).Во многих историях встречаются слова вроде «status», «type» или «priority». Рассматривайте их как контролируемые словари — ограниченный набор допустимых значений.
Если набор небольшой и стабильный, подойдёт enum‑поле. Если он может расти, нужен label или права управления (например, админские категории) — используйте отдельную lookup‑таблицу (например, status_codes) и храните ссылку.
Так истории превращаются в поля, которым можно доверять — пригодные для поиска, отчётов и строгого ввода.
Когда вы перечислили сущности (User, Order, Invoice, Comment и т.д.) и набросали их поля, следующий шаг — связать их. Связи отражают «как эти вещи взаимодействуют», что подразумевается в историях.
Один-к-одному (1:1) — «одна вещь имеет ровно одну другую».
User ↔ Profile (часто можно объединить, если нет причин разделять).Один-ко-многим (1:N) — «одна вещь может иметь много других». Самое распространённое.
User → Order (храните user_id в Order).Многие-ко-многим (M:N) — «много вещей может относиться к многим вещам». Требует дополнительной таблицы.
Базы данных плохо хранят «список product_id» в колонке Order — это создаёт проблемы при поиске, обновлении и отчётности. Вместо этого создают join table, которая сама представляет отношение.
Пример:
OrderProductOrderItem (join table)OrderItem обычно включает:
order_idproduct_idquantity, unit_price, discountЗаметьте: детали истории («quantity») часто принадлежат отношению, а не любой из сущностей отдельно.
Истории также подсказывают, является ли связь обязательной или иногда отсутствует.
Order должен быть user_id (нельзя пустой).phone может быть пустым.shipping_address_id может быть пустым для цифровых товаров.Если из истории понятно, что запись нельзя создать без связи, считайте её обязательной. Если есть «может», «можно» или исключения — опциональной.
Когда вы читаете историю, перепишите её в простое отношение:
User 1:N CommentComment N:1 UserСделайте это для каждого взаимодействия из историй. В конце у вас будет связанная модель, отражающая, как работа действительно происходит — ещё до открытия инструмента для ER‑диаграмм.
Пользовательские истории говорят, ЧТО хотят люди. Workflows показывают, КАК работа на самом деле движется, шаг за шагом. Перевод workflow в данные — один из самых быстрых способов поймать «мы забыли это сохранить» проблемы ещё до строительства.
Запишите workflow как последовательность действий и смены состояний. Пример:
Эти выделенные слова часто становятся полем status (или небольшой таблицей «state») с явным набором разрешённых значений.
Пробегая шаги, спрашивайте: «Что нам нужно будет знать позже?» Workflows обычно выявляют поля типа:
submitted_at, approved_at, completed_atcreated_by, assigned_to, approved_byrejection_reason, approval_notesequence для многоступенчатых процессовЕсли workflow включает ожидание, эскалацию или передачу, обычно нужен как минимум один таймстемп и поле «кто сейчас отвечает».
Некоторые шаги workflow — это не просто поля, а отдельные структуры данных:
Дайте ИИ оба набора: (1) пользовательские истории и acceptance criteria, и (2) шаги workflow. Попросите его перечислить каждый шаг и указать требуемые данные для него (состояние, актор, таймстемп, выходы), затем выделить любое требование, которое не поддерживается текущими полями/таблицами.
На платформах типа Koder.ai такая «проверка пробелов» особенно практична: вы быстро меняете допущения схемы, регенерируете каркас и продолжаете без долгих отложений на ручной рутинной работе.
Когда вы превращаете истории в таблицы, вы не просто перечисляете поля — вы решаете, как данные остаются опознаваемыми и консистентными со временем.
Primary key уникально идентифицирует одну запись — это постоянная «карта ID» строки.
Зачем каждой строке нужен такой ID: истории подразумевают обновления, ссылки и историю. Если сказано «Поддержка может просмотреть заказ и оформить возврат», нужен стабильный способ ссылаться на этот заказ — даже если клиент сменил email, адрес отредактировали или статус заказа изменился.
На практике это внутренний id (обычно число или UUID), который не меняется.
Foreign key — это способ безопасно ссылаться из одной таблицы на другую. Если orders.customer_id ссылается на customers.id, база гарантирует, что каждый заказ принадлежит реальному клиенту.
Это совпадает с историями вроде «Как пользователь, я могу видеть свои счета». Счёт не плавает отдельно — он прикреплён к клиенту (и часто к заказу или подписке).
Пользовательские истории часто содержат скрытые требования уникальности:
Эти правила предотвращают путаницу и дубли, которые всплывают позже как «баги данных».
Индексы ускоряют поиски вроде «найти клиента по email» или «список заказов клиента». Начните с индексов, соответствующих вашим частым запросам и правилам уникальности.
Что отложить: тяжёлую индексацию для редких отчётов или спекулятивных фильтров. Зафиксируйте эти потребности в историях, валидируйте схему сначала, затем оптимизируйте по реальным данным и медленным запросам.
Цель нормализации проста: предотвращать конфликтующие дубли. Если один и тот же факт можно сохранить в двух местах, со временем они разойдутся (две орфографии, две цены, два «текущих» адреса). Нормализованная схема хранит факт один раз и ссылается на него.
1) Следите за повторяющимися группами
Если видите шаблоны вроде Phone1, Phone2, Phone3 или ItemA, ItemB, ItemC, это сигнал в пользу отдельной таблицы (например, CustomerPhones, OrderItems). Повторяющиеся группы усложняют поиск, валидацию и масштабирование.
2) Не копируйте одно и то же в разные таблицы
Если CustomerName появляется в Orders, Invoices и Shipments, вы получили несколько источников истины. Храните детали клиента в Customers, а в других местах храните customer_id.
3) Избегайте «нескольких колонок для одного и того же»
Колонки вроде billing_address, shipping_address, home_address могут быть оправданы, если это действительно разные концепции. Но если вы моделируете «много адресов разных типов», используйте таблицу Addresses с полем type.
4) Отделяйте lookup‑таблицы от свободного текста
Если пользователь выбирает из известного набора (status, category, role), моделируйте это последовательно: либо ограниченный enum, либо lookup‑таблица. Это избавит от вариаций вроде «Pending» vs «pending» vs «PENDING».
5) Проверьте, зависит ли каждое поле не от чего‑то другого
Полезный тест: в таблице колонка, описывающая не основную сущность таблицы, вероятно должна быть в другом месте. Пример: Orders не должен хранить product_price, если это не «цена на момент заказа» (историческая снимка).
Иногда дублирование — осознанный выбор:
Ключ — делать это намеренно: документируйте источник истины и способ обновления копий.
ИИ может заметить подозрительные дубли (повторные колонки, похожие имена полей, несоответствующие поля статусов) и предложить расколы на таблицы. Люди выбирают компромисс — простота vs гибкость vs производительность — исходя из реального использования продукта.
Полезное правило: сохраняйте факты, которые нельзя надёжно восстановить позже; вычисляйте всё остальное.
Хранимые данные — источник истины: отдельные позиции, таймстемпы, изменения статусов, кто что сделал. Вычисляемые — результаты этих фактов: итоги, счётчики, флаги вроде «просрочен», агрегации уровня «текущее количество на складе».
Если два значения можно получить из одних и тех же фактов, предпочитайте хранить факты и вычислять остальное, иначе появится риск противоречий.
Производные значения меняются при изменении входных данных. Если вы храните и входы, и результат, вам нужно поддерживать синхронность при каждом сценарии (редакты, возвраты, частичные отгрузки, правки с запозданием). Один промах — и база начнёт рассказывать две разные истории.
Пример: хранение order_total вместе с order_items. Если изменили количество или применили скидку, и итог не обновился — бухгалтерия увидит одно, а корзина — другое.
Workflows показывают, когда вам нужна историческая правда, а не только «текущее состояние». Если пользователи должны знать, какое значение было в момент события — сохраните снимок.
Для заказа вы можете хранить:
order_total на момент оформления (snapshot), потому что налоги, скидки и правила ценообразования могут измениться позжеДля инвентаря «уровень запасов» часто вычисляют из движений (приходы, продажи, корректировки). Но если нужен аудит, храните движения и, при необходимости, периодические снимки для скорости отчётов.
Для логина храните last_login_at как факт (таймстемп события). Вопрос «активен ли пользователь за последние 30 дней?» лучше вычислять.
Возьмём знакомое приложение поддержки — support ticket. Пройдём от пяти историй до простой ER‑модели (сущности + поля + связи), затем проверим её по одному workflow.
От существительных получаем основные сущности:
До (частая ошибка): Ticket имеет assignee_id, но мы забыли гарантировать, что назначен только агенту.
После: ИИ пометил это, и вы добавляете практическое правило: assignee должен быть User с role = “agent” (реализуется через валидацию в приложении или через ограничение/политику в базе, в зависимости от стека). Это не позволит «назначить клиенту» и сломать отчёты позже.
Схема «готова», когда на каждую пользовательскую историю можно ответить данными, которые реально можно сохранить и запросить. Простейшая валидация — взять каждую историю и спросить: «Мы можем надёжно ответить на этот запрос из базы данных в каждом случае?» Если ответ «может быть», модель не завершена.
Перепишите историю как один или несколько тестовых вопросов — то, что вы захотите в отчёте, на экране или в API. Примеры:
Если вы не можете выразить историю как ясный вопрос — история неясна. Если можете — но не можете ответить на неё схемой, не хватает поля, связи, статуса/события или ограничения.
Создайте маленький набор данных (5–20 строк для ключевых таблиц), включив нормальные и «неудобные» случаи (дубли, отсутствующие значения, отмены). «Пройдитесь» по историям с этими данными — вы быстро найдёте проблемы вроде «мы не можем сказать, какой адрес использовался при покупке» или «некуда сохранить, кто утвердил изменение».
Попросите ИИ сгенерировать проверочные вопросы по каждой истории (включая крайние случаи и сценарии удаления) и перечислить данные, нужные для ответов. Сопоставьте этот список со схемой: любое несоответствие — конкретное действие, а не расплывчатое ощущение, что «что‑то не так».
ИИ может ускорить моделирование данных, но также повышает риск утечки чувствительной информации или жёсткой фиксации плохих допущений. Относитесь к нему как к очень быстрому ассистенту: полезен, но с ограждениями.
Давайте ИИ реалистичные, но санитаризованные данные:
invoice_total: 129.50, status: "paid")Избегайте всего, что может идентифицировать человека или раскрыть конфиденциальные операции:
Если нужен реализм, генерируйте синтетические образцы в нужных форматах — никогда не копируйте production‑строки.
Схемы чаще всего ломаются, потому что «все по‑разному предполагали». Рядом с ER‑моделью (или в том же репозитории) храните короткий журнал решений:
Это превращает вывод ИИ в командные знания, а не в разовый артефакт.
Схема будет эволюционировать с новыми историями. Защитите изменения так:
Если вы используете платформу вроде Koder.ai, используйте механизмы снимков и отката при итерациях по схеме и экспортируйте исходники для глубоких правок и традиционного ревью.
Начните со самих историй и выделите существительные, которые представляют вещи, которые система должна запоминать (например, Ticket, User, Category).
Повышайте существительное до сущности, когда оно:
idДержите короткий список, который можно обосновать конкретными предложениями из историй.
Используйте тест «атрибут или сущность":
customer.phone_number).Быстрая подсказка: если вам когда‑нибудь нужно «много таких», скорее всего нужна отдельная таблица.
Воспринимайте acceptance criteria как чеклист для хранения. Если требование говорит, что вы должны фильтровать/сортировать/отображать/аудитить что‑то, это нужно хранить (или надежно вычислять).
Примеры:
approved_by, approved_atdelivery_dateПерепишите текст истории в предложения отношений:
customer_id в orders)order_items)Если в отношении есть собственные данные (quantity, price, role), эти данные принадлежат связующей таблице.
Моделируйте M:N через связующую таблицу, которая хранит оба внешних ключа и поля, специфичные для отношения.
Типичный паттерн:
ordersproductsПройдитесь по каждому шагу workflow и спросите: «Что нам нужно будет доказать позже?»
Типичные добавления:
submitted_at, closed_atНачните с:
id)orders.customer_id → customers.id)Затем добавьте индексы для наиболее частых запросов (например, , , ). Отложите спекулятивную индексацию до появления реальных паттернов запросов.
Быстрая проверка консистентности:
Phone1/Phone2, вынесите в дочернюю таблицу.Денормализацию делайте позже и осознанно (производительность, отчётность, снимки аудита) и документируйте authoritative source.
Храните факты, которые нельзя надёжно воссоздать позже; вычисляйте всё остальное.
Хорошо хранить:
Хорошо вычислять:
Если вы храните производные значения (например, ), заранее опишите механизм синхронизации и протестируйте крайние случаи (возвраты, правки, частичные отгрузки).
Используйте ИИ для черновиков, затем сверяйте с артефактами.
Практичные подсказки для работы с ИИ:
Ограждения:
emailorder_items с order_id, product_id, quantity, unit_priceИзбегайте хранения «списка ID» в одной колонке — на уровне запросов, обновлений и целостности это быстро усложняется.
created_by, assigned_to, closed_byrejection_reasonЕсли нужно знать «кто что и когда поменял», добавьте таблицу событий/аудита вместо перезаписи одного поля.
emailcustomer_idstatus + created_atorder_total