Представьте отчёт по воронке, который руководитель отдела продаж открывает в понедельник утром. Он собирает сделки за квартал, подтягивает ответственных, стадии, суммы, источники и для красоты считает конверсию по неделям. Если такой отчёт каждый раз ходит в живую CRM за всем набором данных, он конкурирует за ресурсы с теми, кто в это же время звонит клиентам и двигает карточки. Менеджеры не должны чувствовать чужую аналитику как «Битрикс сегодня задумчивый». Аналитике, в свою очередь, не нужно каждый раз заново проходить весь путь с самого начала.
Решение простое по идее: завести рядом собственную копию нужных данных и строить отчёты по ней. Сложность прячется в слове «копия». Копия делается не мгновенно, CRM за время обхода продолжает меняться, часть записей удаляют, сеть иногда подводит, а читатель отчёта по умолчанию считает, что цифры «на сейчас». Ниже разберём, как построить такую выгрузку, чтобы она не мешала рабочей системе, переживала сбои и честно сообщала, на какой момент ей можно верить.
Сначала — вопросы отчёта, потом — колонки
Соблазн выгрузить «всё, что есть» понятен: вдруг пригодится. Но у широкой выгрузки есть цена. Каждое лишнее поле увеличивает ответ, время выполнения метода и размер таблицы. В документации по лимитам REST API Битрикс24 прямо рекомендуется уменьшать число лишних полей, а ещё выбирать данные постранично и выносить долгие задачи во внешнюю очередь (Лимиты REST API). Собственный сервер на VPS как раз и есть такая внешняя очередь: задача живёт на нём, а CRM видит лишь аккуратные порции запросов.
Поэтому начинать стоит с бумаги. Выпишите вопросы, на которые должен отвечать отчёт, и под каждый найдите поля. Допустим, нужен анализ воронки сделок: сколько сделок создано, на какой стадии застряли, у кого из менеджеров какая сумма и сколько дней сделка провела без изменений. Тогда набор получится примерно такой:
- идентификатор сделки — ключ, без него невозможно ни обновление, ни сверка;
- название — удобно при разборе аномалий, хотя в агрегатах не участвует;
- воронка и стадия — основа конверсии;
- сумма и валюта — вместе, иначе суммы в разных валютах сложатся в бессмыслицу;
- ответственный — идентификатор пользователя, а имя можно держать в отдельном справочнике;
- время создания и время последнего изменения — по ним строятся периоды и инкрементальные обновления;
- источник и одно-два пользовательских поля, которые реально участвуют в срезах.
Сущность тоже нужно назвать явно. Для универсальной выборки элементов CRM используется метод crm.item.list, и он работает с конкретным типом сущности, который вы указываете параметром entityTypeId. Поля у разных типов отличаются, а формат пользовательских полей надо проверять отдельно для каждого типа (crm.item.list). Значит, «сделки», «лиды», «контакты» и «смарт-процессы» не получится выгрузить одним универсальным кодом и одной таблицей без разбора: для каждой сущности нужен свой список полей и своя схема. Старый метод crm.deal.list для нового кода брать не стоит: он считается устаревшим, и новую интеграцию разумнее строить сразу на универсальном методе.
Пользовательские поля вызывают больше всего сюрпризов. Они бывают строковыми, числовыми, списками, множественными значениями, привязками к другим сущностям. Если для поля типа «список» в копию попадёт только числовой код варианта, через год никто не вспомнит, что 1043 означало «Рекомендация партнёра». Поэтому для таких полей полезно либо хранить справочник значений отдельной таблицей, либо сохранять и код, и текст на момент выгрузки. А множественные значения лучше не пытаться впихнуть в одну строковую колонку: либо массив, либо дочерняя таблица.
Один запрос: select, filter, order, start
Метод принимает четыре параметра, которые нас интересуют: select задаёт список полей, filter ограничивает выборку, order определяет сортировку, а start указывает, с какого места брать страницу. Ниже условный пример первой страницы для сделок; значения подставлены для иллюстрации, а имена полей перед использованием сверьте с описанием вашего типа сущности.
{
"entityTypeId": 2,
"select": ["id", "title", "stageId", "opportunity", "currencyId", "assignedById", "createdTime", "updatedTime"],
"filter": {">=updatedTime": "2026-10-01T00:00:00+03:00"},
"order": {"id": "ASC"},
"start": 0
}Обратите внимание на сочетание: фильтр отбирает записи, изменённые после контрольной даты, а порядок идёт по идентификатору, не по времени изменения. Почему так, станет ясно чуть позже, когда дойдём до разговора о живых данных. Пока достаточно заметить, что select здесь перечисляет только то, что объявлено в списке колонок, и ничего лишнего.
В ответе вместе с массивом элементов придёт значение next, если следующая страница существует. Его и нужно подставлять в start следующего запроса. Когда next отсутствует, обход закончен. Это кажется мелочью, но именно здесь рождается самая типичная ошибка самодельных выгрузок: разработчик видит, что страницы по условному размеру, и начинает сам считать смещение, прибавляя к start число, которое когда-то видел в ответе. Пока всё работает, разницы нет. Как только размер порции изменится или метод для какого-то типа сущности поведёт себя иначе, счётчик разойдётся с реальностью, и часть записей будет пропущена молча. Правило надёжнее: позицию следующей страницы всегда берёт из ответа, а не из арифметики.
Журнал страниц: собственная таблица состояния
Теперь о главном для устойчивости. Обход тысяч сделок занимает много запросов, и где-то посередине обязательно случится что-нибудь неприятное: сетевой таймаут, перезапуск процесса, превышение допустимой нагрузки со стороны Битрикс24. Если у обхода нет памяти, единственный путь — начинать заново. Для маленькой CRM это терпимо, для большой это ровно та нагрузка, от которой мы пытались уйти.
Память обхода — это не лирика, а обычный код интеграции: пара таблиц в той же базе, где лежит копия. Одна описывает запуск целиком, вторая — каждую пройденную страницу.
CREATE TABLE sync_run (
run_id bigserial PRIMARY KEY,
entity text NOT NULL,
started_at timestamptz NOT NULL,
window_from timestamptz,
status text NOT NULL, -- running, done, failed
next_start integer, -- значение next из последнего принятого ответа
finished_at timestamptz
);
CREATE TABLE sync_page (
run_id bigint NOT NULL REFERENCES sync_run,
start_value integer NOT NULL,
rows_count integer NOT NULL,
first_id bigint,
last_id bigint,
received_at timestamptz NOT NULL,
PRIMARY KEY (run_id, start_value)
);В sync_run хранится запуск: какую сущность выгружаем, когда начали, с какой границы времени брали изменения и где остановились. В sync_page — запись о каждой принятой странице: с какого start её запрашивали, сколько записей пришло, какой первый и последний идентификатор. Запись страницы и сами строки сделок попадают в базу одной локальной транзакцией: либо страница принята целиком вместе с новым значением next_start, либо не принята совсем. Атомарность здесь распространяется только на вашу базу; сам запрос к Битрикс24 в эту транзакцию не входит, и отменить его нельзя. Поэтому порядок такой: получили ответ, открыли транзакцию, записали строки, журнал и позицию, закрыли транзакцию, и только потом просим следующую страницу.
Журнал полезен не только для восстановления. Он отвечает на вопросы, которые иначе приходится гадать. Почему вчера в базе сделок меньше, чем в CRM? Смотрим журнал: последний запуск завершился со статусом «failed» на странице с start равным конкретному числу. Почему обход идёт слишком долго? Сравниваем received_at соседних страниц и видим, где возникли паузы.
Сбой посередине: зачем нужна повторяемая страница
Самое поучительное место — момент между «ответ получен» и «позиция сохранена». Допустим, процесс упал после получения ответа, но до коммита. Что видит база после перезапуска? Позиция указывает на последнюю принятую страницу, а ту, которую мы только что успели получить, там нет. Запрашиваем снова с сохранённого start и получаем ту же страницу. Для наблюдателя это выглядит как повтор: те же идентификаторы, те же поля. Так и должно быть. Из повтора вытекают два правила.
Первое: запись сделки в копию должна быть идемпотентной по естественному ключу. Если в таблице уникальна пара «портал + сущность + идентификатор элемента», то повторная вставка превращается в обновление той же строки, и дублей нет. Второе: журнал страниц должен позволять отличить «страница уже принята» от «страницу получили, но не зафиксировали». Первичный ключ (run_id, start_value) как раз это и обеспечивает: повторная попытка принять ту же страницу упрётся в уже существующую запись и будет понятна как повтор, а не как новая порция.
Сбой может случиться и в обратном порядке: страница записана, а процесс умер до запроса следующей. Тогда после перезапуска берём next_start из sync_run и идём дальше, ничего не повторяя. Важно, что мы продолжаем с сохранённого состояния, а не с «примерно того же места»: догадка вроде «кажется, мы дошли до десятой страницы» оставляет либо дыру, либо лишний круг. Таблица состояния превращает догадку в факт.
Есть ещё один нюанс, который проявляется только на живых данных. Повтор страницы после сбоя не обязан быть идентичным байт в байт: за время простоя менеджеры могли изменить пару сделок. Это нормально. Идемпотентное обновление просто запишет более свежие значения. Но отсюда следует вывод: сохранённое next_start — это позиция в выборке, а не «снимок мира на тот момент».
Порядок выборки — не то же самое, что неподвижные данные
Теперь вернёмся к вопросу, почему в примере выше сортировка идёт по id. Параметр order определяет, в каком порядке выдаются записи, но CRM при этом не замораживается. Пока вы листаете страницы, менеджеры создают, правят и удаляют сделки. Что случится в каждом из вариантов?
Пусть сортировка идёт по времени изменения по возрастанию. Вы получили первые страницы, и вдруг менеджер правит сделку, которая уже была отдана раньше: её время изменения становится самым свежим, и в сортировке она уезжает в конец. Если смещение считается от начала списка, остальные записи сдвигаются на одну позицию к началу, и одна из них может проскочить мимо вашей следующей страницы. Это не ошибка метода, а естественное следствие того, что вы листаете список, который меняется во время чтения.
Сортировка по идентификатору ведёт себя спокойнее. Идентификатор элемента не меняется при правке, а новые сделки получают бо́льшие номера и оказываются в конце списка, где обход их в конце концов подхватит. Остаётся одна неприятность: если запись удалили в уже пройденной части списка, смещение чуть сдвигается, и в редких случаях на границе страниц может быть пропущена одна запись. Мы обсудим, как с этим жить, в разделе про удаления: небольшая страховочная сверка закрывает эту дыру.
А что с изменениями в уже выгруженных записях, ведь сортировка по идентификатору их не замечает? Здесь работает фильтр. Каждый регулярный запуск берёт не всю CRM, а только то, что менялось после контрольной точки, то есть >=updatedTime. Самые тяжёлые полные обходы остаются для редких сверок, а ежедневная работа лёгкая: десятки или сотни изменившихся записей вместо всей базы.
Контрольную точку нужно выбирать осторожно. Если после завершения запуска записать в неё «сейчас», будут потеряны изменения, которые произошли во время самого обхода: они случились уже после того, как мы прошли соответствующую область, но до отметки «сейчас». Поэтому в качестве следующей границы берут время начала запуска, причём с небольшим запасом назад — например, на несколько минут раньше. Запас приводит к тому, что часть записей будет получена дважды; благодаря идемпотентному обновлению это безвредно. Лишняя минута повторной выборки стоит дешевле, чем тихо потерянная правка, из-за которой стадия сделки в отчёте не совпадает со стадией в CRM.
Заодно полезно сохранять и время получения: в каждой строке копии должна быть колонка вроде fetched_at. Время изменения из CRM говорит о том, когда менеджер что-то правил, а fetched_at — о том, когда мы это увидели. Для отчёта о свежести нужны обе даты, и путать их нельзя.
Исходные идентификаторы и что с ними делать
В копии нельзя придумывать собственные номера вместо Битрикс24. Внутренний суррогатный ключ в таблице допустим, но исходный идентификатор элемента и тип сущности должны храниться рядом отдельными колонками и быть уникальными в связке. Иначе первая же сверка сломается: чтобы понять, какая запись исчезла, надо сравнивать списки идентификаторов, и сравнивать их нужно именно в терминах CRM.
Если вы выгружаете несколько порталов — например, у компании два Битрикс24 или есть отдельный для филиала, — добавьте колонку с идентификатором портала. Иначе сделка №100 из одного портала перезапишет сделку №100 из другого, и это будет тот редкий случай, когда ошибка не видна ни в одном журнале: всё «успешно».
Хорошая привычка — хранить не только разобранные колонки, но и исходный ответ по элементу в виде JSON-поля. Когда завтра отчёту понадобится ещё одно поле, часть истории можно будет достать из сохранённого документа, не заходя повторно в CRM. Для таких «сырых» слоёв удобны и документные базы; если вы склоняетесь к ним, загляните в материал про установку и безопасную настройку MongoDB на VPS. А если схема отчётов жёсткая и много соединений с агрегатами, обычная реляционная база с полем JSON справляется не хуже.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Удалённые записи: как заметить отсутствие
Инкрементальная выгрузка по времени изменения умеет находить только то, что есть. Удалённую сделку она не вернёт ни с какой датой, потому что её просто больше нет в выборке. Если ничего не делать, копия будет копить «призраков»: сделки, которых в CRM давно нет, но которые продолжают входить в суммы отчёта. Оценить масштаб таких расхождений можно только сверкой.
Сверка устроена просто. Раз в сутки или реже, ночью, выгружаем из CRM только идентификаторы (в select один id — это самый лёгкий вариант) за весь нужный период и сравниваем со списком идентификаторов в копии. Всё, что есть у нас, но не пришло из CRM, помечаем в копии колонкой missing_since с датой первого обнаружения. Физически строку не удаляем: отчёт по прошлому кварталу может зависеть от сделки, которую удалили позже, и бывают случаи, когда удаление было ошибочным. Дальше в отчётах решаем явно: либо исключаем помеченные строки из текущих агрегатов, либо показываем их отдельной строкой «удалено в CRM».
Две оговорки. Во-первых, если запись просто перестала попадать под фильтр — например, её перенесли в другую воронку, а вы ограничили выборку одной, — она тоже «пропадёт» из сверки. Поэтому фильтр сверки должен быть шире рабочего или хотя бы совпадать с ним по смыслу, и в колонке причин стоит различать «исчезла» и «вышла из области выгрузки». Во-вторых, одно отсутствие в одной сверке не всегда окончательно: пока обход шёл, запись могла быть на границе страниц. Страховка простая — считать запись удалённой только после повторного отсутствия в следующей сверке либо перепроверить её адресным запросом по идентификатору.
Лимиты: облако, коробка и признаки перегрузки
Для облачной версии Битрикс24 действуют лимиты на интенсивность запросов и на ресурсоёмкость. Для коробочной версии картина другая: там многое зависит от настроек вашего сервера, и переносить облачные правила на неё без проверки не стоит (Лимиты REST API). Это разные режимы, и интеграция, которая пишется под обе, должна читать параметры из настроек, а не зашивать их в код.
Тонкость облака в том, что ограничения не сводятся к «не больше N запросов в секунду». В ответе приходит поле operating, связанное с накопленным временем выполнения метода. Иначе говоря, один тяжёлый запрос с кучей полей и широким фильтром может «стоить» больше, чем десяток лёгких. Поэтому две привычки приносят больше пользы, чем любая искусственная задержка: сокращать select до нужного и сужать filter по времени. Первая обход облегчает, вторая сокращает его длину.
Практически это означает такой цикл: после каждой страницы смотрим на значения, которые вернул портал, и если время выполнения растёт, делаем паузу перед следующим запросом. Конкретные пороги подбираются наблюдением за своим порталом, и записывать их в статью было бы самообманом: у разных тарифов и нагрузок они разные. Достаточно, чтобы пауза была настраиваемой, а в журнал страниц попадало и время ответа — тогда по таблице видно, где обход стал тяжёлым.
На коробочной версии вопрос звучит по-другому: у вас есть собственный сервер Битрикс24, и выгрузка конкурирует с менеджерами за его процессор и базу. Расписание тогда определяется не лимитом портала, а часами наименьшей нагрузки. Ночное окно и небольшие порции, как и в облаке, остаются разумным выбором.
Когда API недоступен
Рано или поздно портал не ответит: плановые работы, сетевой сбой, превышение лимита, ошибка авторизации. Для выгрузки главное — не превратить временную недоступность в испорченные данные. Ответ с ошибкой не должен менять ни копию, ни позицию. Запуск остаётся в статусе «running» с прежним next_start, а повторная попытка делается через растущие паузы: сначала через минуту, потом через пять, потом реже. Если повторы не помогают, запуск получает статус «failed» вместе с текстом последней ошибки, и об этом должно узнать человек.
Здесь важно разделять ошибки по смыслу. Сетевой таймаут и временное ограничение — повод подождать и повторить. Ошибка доступа означает, что вебхук отозван или потерял права: повторять её сто раз бессмысленно, надо остановиться и сообщить. Неожиданный формат ответа — повод остановиться и разбираться, потому что продолжать писать в копию то, что вы не понимаете, значит портить данные.
Следующий успешный запуск продолжит с сохранённой позиции, а если с момента сбоя прошло много времени, то пересмотрит окно изменений: контрольная точка не сдвигалась, значит, всё, что изменилось за простой, будет подхвачено. Это и есть главное преимущество хранения состояния в базе, а не в памяти процесса или в имени файла.
Где всё это живёт: VPS под отчётную базу
Выгрузке нужны три вещи: планировщик, процесс, который ходит в API, и база, где лежит копия. На VPS это удобно собирается в одном месте, без привязки к рабочим компьютерам и без нагрузки на CRM. Здесь же работает интерфейс отчётов или подключённый BI-инструмент. На VPS MAATRIX доступны NVMe-диски, процессоры AMD EPYC и root-доступ, а список расположений включает UK, US, RU и FR; при выборе локации учитывайте, откуда ваш сервер будет обращаться к порталу и кто читает отчёты, и проверьте доступность портала с выбранного адреса, а не предполагайте её заранее. Автоматическое создание сервера после оплаты занимает пару минут, поэтому пробный стенд для проверки схемы можно поднять быстро. В каталоге есть n8n с установкой: если вам привычнее собирать расписание визуально, его можно использовать как оркестратор, а наличие образа и версию ОС стоит посмотреть в форме заказа.
Размер сервера определяется не абстрактным «чем больше, тем лучше», а тем, что вы собираетесь делать. Выгрузка сама по себе почти не требовательна: она в основном ждёт ответов от API. Ресурсы нужны отчётным запросам к базе и самому хранилищу. Тариф Ba (1 vCPU, 2 ГБ RAM, 40 ГБ NVMe) подходит для опыта и небольшой копии одной сущности, а при росте числа сущностей, истории и пользователей отчётов логично смотреть на Uab или Heka. Но это отправная точка для измерений на ваших данных, а не обещание ёмкости: оцените размер таблиц после первой полной выгрузки и посмотрите, как ведёт себя самый тяжёлый отчёт.
Одно важное напоминание: копия отчётных данных не является полноценной резервной копией CRM. Она не сохраняет настройки воронок, роли, автоматизацию, шаблоны документов и всё, что не вошло в ваш список полей. Для аналитики это не требуется, а вот называть её «бэкапом Битрикса» не стоит. Резервное копирование самого сервера с отчётной базой — отдельная задача, и её нужно настроить самостоятельно: автоматические копии для всех тарифов в предложении не заявлены.
Права: чего достаточно выгрузке и кому читать копию
Копия сделок содержит суммы, названия клиентов, иногда телефоны и комментарии. Значит, у неё должен быть собственный круг доступа, не совпадающий с кругом доступа CRM. Менеджер видит свои сделки в Битрикс24 по правам портала, а в отчётной базе все сделки лежат рядом. Если дать всем сотрудникам прямое подключение к базе, вы одним движением снимете ограничения, которые строили в CRM годами.
Практичное разделение выглядит так. Для выгрузки заводится отдельный вебхук или приложение с минимально нужными правами на чтение нужных сущностей и ничего больше. Принцип наименьших прав для секретов прямо рекомендуется в материалах OWASP (Secrets Management Cheat Sheet): если секрет утечёт, пострадает только то, что он открывал. Сам секрет не кладётся в репозиторий и не прописывается в тексте скриптов; он хранится в переменных окружения или в защищённом хранилище на сервере, а доступ к нему ограничивается пользователем, под которым запускается выгрузка. Там же рекомендуется ротация секретов, по возможности автоматизированная: если вебхук периодически перевыпускается, такая процедура должна быть описана заранее, иначе в день замены выгрузка тихо упадёт с ошибкой доступа, и вы узнаете об этом из устаревшего отчёта.
В самой базе стоит завести минимум три роли. Роль выгрузки — пишет в таблицы копии и журнала, но ничего не читает за их пределами. Роль отчётов — только читает, и лучше через представления, которые скрывают лишние колонки: например, не отдают телефоны, если для отчёта они не нужны. Администраторская роль нужна для миграций и используется редко. Порт базы не стоит открывать всему интернету: доступ к ней ограничивают адресами или туннелем, а аналитикам выдают доступ к готовым отчётам или к ограниченному представлению.
Свежесть: цифра, которую нельзя спрятать
Теперь о читателе отчёта. Он видит график и по умолчанию считает его актуальным. Копия по определению отстаёт: её обновляют по расписанию, значит, в любой момент в ней нет последних изменений. Обещать «данные в реальном времени» здесь нельзя, и никакой настройкой это не исправить. Зато можно сделать отставание видимым, и тогда оно перестаёт быть проблемой и становится характеристикой.
Для этого нужна одна небольшая таблица или представление: когда началась и закончилась последняя успешная выгрузка каждой сущности, какая у неё контрольная точка и сколько времени прошло. Из журнала запусков такая информация достаётся запросом.
SELECT entity,
max(started_at) AS data_as_of,
now() - max(started_at) AS lag
FROM sync_run
WHERE status = 'done'
GROUP BY entity;Обратите внимание, что «данные по состоянию на» берётся из времени начала успешного запуска, а не из его окончания. Запуск мог идти сорок минут, и сделка, которую правили на двадцатой минуте, в него может не попасть. Время начала — честная нижняя граница: всё, что изменилось до этой минуты, уже в копии.
Чем показывать свежесть? Достаточно одной строки в шапке отчёта: «Данные Битрикс24 по состоянию на 14:05, обновляются раз в час». Для людей, которые принимают решения, важнее второе: если выгрузка не отрабатывала дольше обычного, строка должна менять вид. Например, при отставании больше двух расписанных интервалов она перекрашивается или получает надпись «обновление задерживается». Так руководитель не примет цифры двухдневной давности за сегодняшние, а аналитик сразу увидит, где искать причину, не открывая серверных логов.
Полезна и вторая метка: отдельное напоминание о том, что сверка удалений выполняется реже, чем основное обновление. Если сверка идёт раз в сутки, то удалённая утром сделка может числиться в отчёте до ночи. Об этом достаточно один раз написать в описании отчёта, не превращая каждую страницу в таблицу оговорок.
Как выглядит один полный цикл
Соберём всё вместе на условном примере: сделки, расписание раз в час, сверка раз в ночь. Числа ниже выбраны для иллюстрации и не описывают реальный портал.
- Планировщик запускает процесс. Тот читает из таблицы последнюю успешную контрольную точку и вычитает небольшой запас, получая нижнюю границу окна.
- Создаётся запись в
sync_runсо статусом «running» и временем начала. Если есть незавершённый запуск, процесс продолжает его с сохранённогоnext_start, а не открывает новый. - Выполняется запрос с
selectиз списка колонок,filterпо времени изменения,orderпо идентификатору иstartиз состояния. - Страница принимается одной транзакцией: строки сделок вставляются или обновляются по ключу, добавляется запись в
sync_page, обновляетсяnext_start. - Если в ответе есть
next, процесс делает паузу, зависящую от наблюдаемого времени выполнения, и повторяет с четвёртого шага. Еслиnextнет, запуск получает статус «done». - Представление свежести обновляется само, потому что строит данные из журнала.
В ночном запуске шаги те же, только select состоит из одного id, filter охватывает весь нужный период, а результат сравнивается со списком идентификаторов в копии, чтобы расставить пометки об исчезновении записей.
Если в какой-то час портал оказался недоступен, запуск останется незавершённым, представление покажет растущее отставание, а следующая попытка продолжит с того же места. Если процесс упал посередине страницы, он повторит эту страницу, и идемпотентная запись не создаст дублей. Если менеджер правил сделку во время обхода, она либо попадёт в текущий запуск, либо будет подобрана следующим благодаря запасу контрольной точки.
Отчёт, который можно открыть спокойно
Когда эта конструкция собрана, меняется сама природа отчёта. Он больше не бегает в рабочую CRM за каждым числом и не мешает менеджерам. Он читает локальную таблицу, спокойно считает свои агрегаты и тратит ресурсы собственного сервера. А рядом с графиком стоит скромная строка, в которой написано, на какой момент в этом графике можно верить каждой цифре.
Эта строка и есть главный результат работы: не мгновенная точность, которую получить нельзя, а точное знание о том, какой именно точности ждать. Тот, кто открывает отчёт в понедельник утром, видит дату состояния, понимает, что свежие правки последнего часа могут не успеть попасть, и принимает решение с поправкой на это. Рабочая CRM продолжает жить своей жизнью, а копия показывает, какой она была на последней контрольной точке.