Услуги
Сайты и магазиныЛендинги ★Создание сайтовИнтернет-магазины
ИИ и автоматизацияИИ-секретарь ★Автоворонка и прогрев ★Telegram-боты ★Автоматизация ★CRM-разработка
ПродвижениеSEO-продвижениеAI-продвижение ★Контекстная рекламаSMM и соцсети
Наружка, печать и производствоВывески и буквыНаружная рекламаБаннеры и штендерыТипографияВизиткиШирокоформатБрендированиеОклейка автоТорговое оборудованиеМатериалы для рекламы
Дизайн и поддержкаДизайнПоддержка
КомплексDigital под ключ ★
РешенияКейсыПортфолиоЦеныО студииБлогКонтакты Обсудить проект

Модель данных

«Единой модели данных нет» — верно

Самый частый и самый верный упрёк к системам, собранным быстро: единой модели данных в них нет. У нас её тоже нет в том смысле, в каком её понимает инженер с реляционным опытом. Разбираем, что есть вместо, и во что это обходится.

Замер: 9 августа 2026разбор своей системы

Чем проверено: проектная схема DB_SCHEMA.md, контракт API_CONTRACT.md, реестр инцидентов по каждому пункту

Короткий вывод. Упрёк «ИИ собирает быстро, а единой модели данных там нет» — справедливый. У нас её нет в том смысле, в каком её понимает инженер с реляционным опытом: боевое хранилище — это JSON-файлы без схемы, без версии формата и без валидатора при записи. Мы нашли это у себя сами и записали числом: один POST-запрос обнулил базу заявок и прожил так два дня; служебный маркер кодировки молча сломал пять инструментов. Реляционная схема спроектирована и лежит рядом — в статусе проекта, а не прода. Ниже честно: что есть, чем это оплачено и по какому критерию мы перейдём.

Что реально лежит под системой

Боевое хранилище — файлы JSON. У бота это один объект с пользователями, заказами, лидами и обновлениями, который переписывается целиком на каждое событие и вызывается из десятков асинхронных обработчиков и фоновых задач одновременно. Ни временного файла с переименованием, ни блокировок.

Что из этого следует, если назвать вещи как в учебнике:

  • Гонка «последний записавший побеждает». Два одновременных обращения — и одно затирает другое. Это не гипотеза: у нас так уже обнулилась база заявок.
  • Повреждение файла при сбое в момент записи. Пишется весь объект целиком.
  • Линейная деградация. Сериализуется всё хранилище на каждое событие, а не изменённая запись.
  • Стихийная эволюция полей. Схемы нет — значит легаси-поле спокойно живёт рядом с действующим, а читатели угадывают, какое из них истинное. У нас так и было: старое поле менеджера соседствовало с новым.

Как это выглядит в коде. Имена обезличены, механика настоящая

// ── Было: read-modify-write всего хранилища на каждое событие ─────────────
const db = JSON.parse(fs.readFileSync(DB));   // читаем весь файл
db.orders.push(order);                        // меняем одну запись
fs.writeFileSync(DB, JSON.stringify(db));     // пишем весь файл обратно
// два одновременных вызова — один затирает другой;
// сбой в момент записи — на диске остаётся обрезанный JSON.

// ── Стало: временный файл + атомарное переименование ──────────────────────
const tmp = `${DB}.${process.pid}.tmp`;
const fh = await fs.promises.open(tmp, 'w');
await fh.writeFile(JSON.stringify(db));
await fh.sync();                              // на диск, а не в буфер ОС
await fh.close();
await fs.promises.rename(tmp, DB);            // подмена — одна операция ФС

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

Повтор запроса не должен удваивать заявку, а идентификатор — рождаться счётчиком по файлу

// Ключ идемпотентности приходит от клиента и переживает ретрай сети.
const key = body.idempotency_key;
if (db.leads.some(l => l.key === key)) {
  return { ok: true, duplicate: true };     // повтор — не новая заявка
}

// Идентификатор из времени и метки писателя: общего счётчика нет,
// значит нет и гонки за него при нескольких пишущих процессах.
const makeId = () => `${Date.now().toString(36)}-${WRITER}-${rand4()}`;
db.leads.push({ ...lead, key, id: makeId() });
Самое дорогое следствие — не порча данных, а невозможность договориться. Когда у структуры нет схемы, каждый новый инструмент читает её по-своему. Наш замер: три инструмента на одной и той же странице насчитали 6, 7 и 3 цены — и все трое были по-своему правы, потому что «цена» нигде не определена формально.

Что при этом спроектировано

Реляционная схема существует как документ и разбирает текущее хранилище по таблицам: клиенты, водители, заказы и связанные сущности. Несколько решений оттуда, чтобы было видно, что это не набросок:

  • Единый ключ профиля клиента — последние 10 цифр телефона, уникальные. Именно так человек и опознаётся в реальности: один и тот же клиент приходит с сайта, из мессенджера и из телефонного звонка.
  • Деньги — целые тенге, без копеек, как уже сделано в коде. Не число с плавающей точкой.
  • Время — с часовым поясом; свободные и полуструктурные данные — отдельным типом, а не россыпью колонок.
  • Статусная модель не выдумывается, а берётся из работающего кода: операционные статусы и клиентский жизненный цикл формализуются такими, какие они есть.

Парный документ фиксирует контракт бэкенда: один источник правды на все каналы (панель диспетчера, бот, сайт, мини-приложение), разграничение видимости (клиент никогда не получает телефон водителя, ставки и служебные флаги) и правило «не ломать существующее» — работающие пути сохраняются один в один, новое помечается отдельно.

Почему схема не в проде. Не потому что «руки не дошли», а потому что переход стоит остановки работающего контура и переписывания читателей. Мы записали критерий, при котором он становится обязательным (ниже), и держим до него текущее решение — с открытыми глазами на его цену.

Связь «многие ко многим» уже есть — и это главный сигнал

Документная модель хороша, пока запись самодостаточна. Наша перестала быть такой: клиент связан с рейсами, рейсы со счетами, счета с водителями — и счёт не равен рейсу. Это выяснилось не в теории, а на сверке счетов: один счёт закрывает несколько рейсов, один рейс попадает в несколько документов.

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

-- Ключ профиля клиента — последние 10 цифр телефона: один и тот же человек
-- приходит с сайта, из мессенджера и телефонным звонком.
CREATE TABLE client (
  phone10      CHAR(10) PRIMARY KEY,
  name         TEXT,
  created_at   TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE trip (
  id           BIGSERIAL PRIMARY KEY,
  phone10      CHAR(10) REFERENCES client,
  driver_id    BIGINT REFERENCES driver,
  price_tenge  BIGINT NOT NULL,      -- целые тенге, не число с плавающей точкой
  happened_at  TIMESTAMPTZ NOT NULL   -- время всегда с поясом
);

-- Вот здесь документная модель и кончается: счёт не равен рейсу.
-- Один счёт закрывает несколько рейсов, один рейс попадает в несколько счетов.
CREATE TABLE invoice_trip (
  invoice_id   BIGINT REFERENCES invoice,
  trip_id      BIGINT REFERENCES trip,
  PRIMARY KEY (invoice_id, trip_id)
);

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

Критерий перехода на СУБД мы зафиксировали: регулярные запросы вида «кто из водителей возил этого клиента в июне и по каким счетам». Пока такие запросы разовые — файлы дешевле. Как только они становятся ежедневными, файлы перестают быть решением, и это будет видно по критерию, а не по ощущению.

«Это эволюция Excel, а раньше — Access»

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

Разница в одном месте, и она не в технологиях. У Excel-автоматизации нет способа узнать, что она врёт, — кроме жалобы пользователя. У нас на этом месте стоят проверки и реестр классов ошибок, и именно они показали дыры в модели данных: не архитектор на ревью, а гейт, который покраснел. Это не делает нашу модель лучше — это делает её измеренной.

И про слово ERP — честно. Набор инструментов поверх общего хранилища — это ещё не ERP, как бы много инструментов в нём ни было. ERP — это единая транзакционная модель, где проводка в одном месте согласованно меняет состояние в остальных. У нас такой модели нет, и называть наш контур ERP мы не будем. Он решает свои задачи, и границу мы обозначаем до разговора о цене, а не после.

Что мы с этим делаем — по пунктам

ДыраЧем уже стрелялаЧто делаем
Нет схемы и версии формата у данныхмаркер кодировки сломал 5 инструментов; легаси-поле жило рядом с действующимописание схемы + поле версии + проверка структуры при записи, в том же гейте, что и проверка кода
Запись без атомарности и блокировокPOST обнулил базу заявок, дефект прожил 2 днязапись через временный файл с переименованием; идемпотентная вставка, чтобы повтор не удваивал запись
Идентификатор записи рождается счётчиком по файлупри нескольких писателях коллизия гарантирована по построениюидентификатор из времени и метки писателя — без общего счётчика
Термины разбросаны, единого глоссария нетрасхождения канона между документамиединый словарь как отдельный артефакт, доступный и коду, и человеку

Список получен не самокритикой ради красоты, а наложением профильной инженерной литературы на наши реальные системы. Мы предпочитаем, чтобы он был написан у нас на странице, а не найден заказчиком в середине проекта.

Источники материала

  • Проектная схема хранилища и парный контракт бэкенда — внутренние документы проектирования от 04.06.2026: таблицы, ключи, типы, принципы разграничения видимости.
  • Разбор пяти профильных книг, наложенный на наши системы, 31.07.2026 — семь инженерных дыр с указанием, чем каждая уже стреляла.
  • Реестр систематических ошибок: классы про действие по снимку и про отсутствие перепрогона, инциденты с датами.
  • Инвентарь замеров: сюжет о трёх инструментах, насчитавших разное число цен на одной странице.

На курсе это модуль про источник истины

Где живёт правда по каждому типу утверждения, чем снимок отличается от факта и как найти расхождения между своими же хранилищами. Упражнение стабильно вскрывает два-три расхождения за один заход.

Программа курса и цены
Соседние материалы
Предыдущий замер: Ущерб за границей своей зоныСледующий замер: Архитектура и стек без прикрасВсе материалы журнала

Обсудим ваш проект

Оставьте заявку — свяжемся в течение 15 минут в рабочее время, зададим пару вопросов и пришлём ориентир по цене и срокам. Бесплатно, без обязательств.

  1. 1. Заявка или квиз
  2. 2. Короткий созвон / переписка
  3. 3. Бесплатная смета и план
  4. 4. Договор и старт

Бесплатно и ни к чему не обязывает. Ответим за 15 минут.