Регистр выбирают за минуту, а перепроводят историю выходными

Архитектура решения на 1С складывается из нескольких десятков выборов, и большая часть из них делается в первые недели проекта: какой вид регистра завести, какие измерения в него положить, включать ли разделение итогов, как гонять данные между базами. В момент выбора все варианты выглядят рабочими, разница между ними видна только под нагрузкой и на объёме, которого пока нет.
Через год объём появляется. Отчёты по остаткам открываются минутами, закрытие месяца растягивается на ночь, пользователи ловят таймауты при проведении. Разработчики смотрят планы запросов, добавляют индексы, разносят регламентные задания — помогает на неделю, потом всё возвращается. Чинят следствие, а причина лежит в схеме данных и схеме обмена.
Разберём несколько таких решений. Каждое принимается за минуту, каждое потом определяет поведение системы годами, и половина требует перепроведения истории, если ошиблись.
Вид регистра выбирают один раз и навсегда
Начнём с выбора, который делают в первые дни проекта и почти никогда не пересматривают.
Регистр накопления бывает двух видов, и разница между ними не в том, что вы храните, а в том, какие запросы вы сможете писать дальше. Остатки умеют отдавать состояние на момент времени, обороты — только движения за период.
Соблазн взять остатки понятен: они умеют всё, что умеют обороты, плюс ещё остаток. Расплата приходит позже. Регистр остатков ведёт дополнительную таблицу итогов, обновляет её при каждой записи и требует места. Если по этому регистру вы никогда не спрашиваете «сколько сейчас», а спрашиваете «сколько прошло за месяц» — таблица итогов работает вхолостую.
Обратная ошибка дороже. Регистр оборотов выбрали для взаиморасчётов, а потом бизнесу понадобился остаток долга на дату. Считать его приходится сложением всех движений с начала времён:
ВЫБРАТЬ
Взаиморасчёты.Контрагент,
СУММА(Взаиморасчёты.СуммаОборот) КАК Долг
ИЗ
РегистрНакопления.Взаиморасчёты.Обороты(, &Дата, , ) КАК Взаиморасчёты
СГРУППИРОВАТЬ ПО
Взаиморасчёты.КонтрагентПока движений тысячи, никто не замечает. На миллионах запрос начинает читать всю таблицу целиком при каждом открытии отчёта, и вот тут появляются те самые сорок минут.
Переделка регистра оборотов в регистр остатков означает перепроведение всех документов, которые в него пишут. На работающей базе это выходные с остановкой работы, а иногда и не одни.
Проверка на этапе проектирования простая: спросите у заказчика, будет ли когда‑нибудь нужен вопрос «сколько на дату». Ответ «сейчас нет, но вообще интересно» означает остатки.
Итоги, которые никто не двигает
Второе решение вытекает из первого.
Регистр остатков хранит итоги не за всю историю, а до границы, которая называется периодом рассчитанных итогов. За этой границей платформа считает остатки честным сложением движений. Границу двигают вперёд — обычно регламентным заданием, раз в месяц.
Ломается это тихо. Задание отключили при переносе базы на новый сервер, или оно падало по таймауту, и никто не смотрел в журнал. Граница осталась там, где была год назад, и каждый запрос остатков теперь дочитывает движения за год после неё.
Посмотреть текущее положение можно прямо из кода:
Граница = РегистрыНакопления.ОстаткиТоваров.ПолучитьМаксимальныйПериодРассчитанныхИтогов();
Сообщить("Итоги рассчитаны до: " + Формат(Граница, "ДФ=dd.MM.yyyy"));Дальше вопрос в том, как её двигать. Метод пересчёта за период выглядит безобидным, а на большой базе способен взяться за всю историю разом и не закончиться до утра. Аккуратнее сдвигать границу шагами, месяц за месяцем:
УстановитьПривилегированныйРежим(Истина);
ТекущаяГраница = НачалоМесяца(
РегистрыНакопления.ОстаткиТоваров.ПолучитьМаксимальныйПериодРассчитанныхИтогов());
Цель = НачалоМесяца(ДобавитьМесяц(ТекущаяДата(), -1));
Пока ТекущаяГраница < Цель Цикл
ТекущаяГраница = ДобавитьМесяц(ТекущаяГраница, 1);
РегистрыНакопления.ОстаткиТоваров.УстановитьМаксимальныйПериодРассчитанныхИтогов(
КонецМесяца(ТекущаяГраница));
КонецЦикла;Когда текущие итоги выключены, оперативный остаток берётся на последний рассчитанный месяц, а не на текущий момент. Схема, где итоги отключили ради скорости записи, а потом строят на оперативных остатках контроль отгрузки, даёт остатки прошлого месяца и никаких сообщений об ошибке.
Разделение итогов включили ради параллельности и потеряли скорость чтения
Механизм, который решает реальную проблему и создаёт другую, если включить его не там.
При параллельной записи в один и тот же набор измерений сессии конкурируют за одну строку таблицы итогов. Разделение итогов снимает конкуренцию: запись дублируется с разными значениями служебного разделителя, и сессии перестают мешать друг другу.
Расплачиваетесь вы при чтении. Чтобы отдать остаток, платформе теперь приходится сворачивать разделённые записи, и получение остатков замедляется. Для регистра, по которому идёт контроль остатка при отгрузке, это ровно то, чего делать не следовало.
Правило получается такое: разделение итогов включают на регистрах с интенсивной записью и редким чтением остатков — накопительная статистика, движения по складу, которые читаются отчётами раз в день. На регистрах, где остаток проверяется в момент проведения каждого документа, его не включают.
Формулируется просто, а обнаруживается сложно: включённое разделение не даёт ни ошибок, ни предупреждений, только медленное чтение, которое списывают на объём данных.
Составной тип в измерении регистра
Измерение с составным типом — скажем, «Заказ покупателя, Заказ поставщику, Реализация» в одном поле — платформа разворачивает в несколько физических колонок: отдельно тип, отдельно ссылка на каждую таблицу. Соединение по такому измерению превращается в соединение по нескольким полям с проверкой типа.
ВЫБРАТЬ
Остатки.Заказ,
Остатки.КоличествоОстаток
ИЗ
РегистрНакопления.ЗаказыКлиентов.Остатки(&Дата, ) КАК Остатки
ГДЕ
Остатки.Заказ ССЫЛКА Документ.ЗаказПокупателяУсловие с проверкой типа в запросе почти никогда не индексируется так, как вы рассчитывали, и на большой таблице итогов уходит в сканирование.
При проектировании составного измерения стоит спросить: действительно ли эти сущности взаимозаменяемы в отчётах, или их просто сложили в одно поле, чтобы не заводить три регистра. Второй случай встречается чаще, а лечится он разделением регистра либо выносом типа в отдельное измерение.
К этому же примыкает вопрос о количестве измерений: каждое из них входит в ключ таблицы итогов, и число строк в ней растёт как произведение количества значений по всем измерениям. Регистр с семью измерениями, где два из них — дата с точностью до секунды и номер строки документа, даёт таблицу итогов больше самой таблицы движений. Такое встречается, и обнаруживается обычно по размеру файлов базы.
Транзакция проведения, в которую попало лишнее
Проведение документа целиком выполняется в транзакции. Всё, что вы туда положили, удерживает блокировки до конца проведения — включая то, что к записи движений отношения не имеет.
Процедура ОбработкаПроведения(Отказ, Режим)
// движения по регистрам
ЗаписатьДвижения();
// а вот это здесь лишнее
ОтправитьУведомлениеПоЭлектроннойПочте(Ссылка);
ЗапроситьКурсВалютыИзВнешнегоСервиса();
ПересчитатьСебестоимостьЗаМесяц();
КонецПроцедурыВнешний сервис отвечает три секунды — значит, блокировки держатся три секунды. При десяти одновременных проведениях остальные встают в очередь, а ждать они будут ровно до таймаута блокировки, по умолчанию двадцать секунд. Дальше пользователи получают ошибку и звонят в поддержку.
Из транзакции проведения выносят всё, кроме записи движений. Уведомления, обращения к внешним системам, тяжёлые пересчёты уходят в фоновые задания или в очередь заданий, которая разбирает их после проведения.
Отдельно стоит посмотреть на пересчёты «за месяц» внутри проведения одного документа. Такая конструкция означает, что проведение последнего документа месяца обходится дороже, чем первого, и к концу периода система замедляется без всякой видимой причины.
Обмен, который душит сам себя
План обмена регистрирует изменения в служебной таблице, и устроена она так: на каждый объект для каждого узла заводится одна запись. Не по одной на каждое изменение, а именно одна, которая обновляется.
Из этого вытекает конкуренция, о которой узнают под нагрузкой. За эту единственную запись борются три разных действия: регистрация нового изменения, выборка изменений на выгрузку и удаление после подтверждения. Когда обмен идёт часто, а объекты меняются активно, сессии выстраиваются в очередь на одних и тех же строках.
Метод выборки изменений выглядит как чтение, а на деле пишет: он проставляет в записи регистрации номер сообщения обмена, и на время этой записи объект блокируется.
// выглядит как чтение, ведёт себя как запись
Выборка = ПланыОбмена.ВыбратьИзменения(Узел, НомерСообщения);Отсюда две вещи, которые надо посчитать заранее. Количество узлов умножает объём регистрации: пятьдесят узлов означают пятьдесят записей на каждое изменение каждого объекта, и таблица регистрации начинает расти быстрее данных, ради которых её завели. А обмены, идущие параллельно по разным узлам, всё равно конкурируют между собой, если трогают одни и те же объекты.
Что с этим делают на этапе проектирования
Разносят обмены по времени вместо того, чтобы гонять их одновременно каждые пять минут. Регистрируют только реально изменившиеся объекты, а не перезаписывают всё подряд — типовая ошибка здесь в том, что объект перезаписывается программно без изменений и получает регистрацию. И честно смотрят, нужен ли план обмена вообще: для интеграции с не-1С системами он часто оказывается тяжелее, чем обычная очередь сообщений.
Двусторонний обмен без правила, кто прав
Две базы обмениваются в обе стороны. Один и тот же документ поправили в обеих между сеансами обмена. Дальше побеждает тот, чьи изменения приехали последними, и это происходит без всяких уведомлений.
Приоритет узлов надо задавать явно, до того как расхождения появятся. Рабочий вариант — версия объекта, по которой сравнивают:
Функция ПринятьВходящийОбъект(Входящий, Существующий)
Если Существующий = Неопределено Тогда
Возврат Истина;
КонецЕсли;
// выигрывает тот, кого правили позже
Если Входящий.ВерсияИзменения > Существующий.ВерсияИзменения Тогда
Возврат Истина;
Иначе
ЗарегистрироватьКонфликт(Входящий, Существующий);
Возврат Ложь;
КонецЕсли;
КонецФункцииОсновное здесь не в самой функции, а в том, что конфликт попадает в журнал, а не исчезает. Расхождение, о котором никто не узнал, всплывает через месяц при сверке, и восстановить, какая версия была верной, к тому моменту нельзя.
При этом конфликтующие объекты не удаляют вручную. Удаление объекта, участвующего в обмене, ломает цепочку регистрации, и следующий сеанс приносит новые расхождения уже в других местах.
Как это найти в работающей базе
Всё перечисленное диагностируется до того, как начнутся жалобы, и почти всё — обычными запросами к самой базе.
Границу рассчитанных итогов по всем регистрам остатков удобно снять разом, обработкой в консоли:
Для Каждого Метаданное Из Метаданные.РегистрыНакопления Цикл
Если Метаданное.ВидРегистра <> Метаданные.СвойстваОбъектов.ВидРегистраНакопления.Остатки Тогда
Продолжить;
КонецЕсли;
Менеджер = РегистрыНакопления[Метаданное.Имя];
Граница = Менеджер.ПолучитьМаксимальныйПериодРассчитанныхИтогов();
Отставание = РазностьДат(Граница, ТекущаяДата(), "ДЕНЬ");
Если Отставание > 60 Тогда
Сообщить(Метаданное.Имя + ": итоги отстают на " + Отставание + " дн.");
КонецЕсли;
КонецЦикла;Шестьдесят дней здесь взяты как ориентир: при ежемесячном сдвиге границы нормальное отставание не превышает двух месяцев, всё что больше — либо задание не отработало, либо его никто не заводил.
Размер таблиц регистрации изменений относительно данных смотрят прямо на стороне СУБД. Признак нездоровья простой: служебная таблица сопоставима по размеру с той, изменения которой она регистрирует, или больше неё.
SELECT TOP 20
t.name AS Таблица,
SUM(p.rows) AS Строк,
SUM(a.total_pages) * 8 / 1024 AS МБ
FROM sys.tables t
JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
JOIN sys.allocation_units a ON p.partition_id = a.container_id
WHERE t.name LIKE '_ReferenceChngR%' OR t.name LIKE '_DocumentChngR%'
GROUP BY t.name
ORDER BY SUM(a.total_pages) DESC;Про технологический журнал стоит сказать отдельно, потому что его включают обычно уже во время аварии. Событие ожидания на управляемой блокировке пишется туда вместе с тем, кто кого ждал, и по нему видно и виновника, и длительность:
<event>
<eq property="Name" value="TLOCK"/>
<eq property="Name" value="TDEADLOCK"/>
<eq property="Name" value="TTIMEOUT"/>
</event>
<property name="all"/>Собранные за неделю обычной работы записи дают картину без всякой аварии: если ожидания на одних и тех же регистрах повторяются изо дня в день, вы нашли будущую проблему до того, как она стала заметна пользователям.
Что проверить у себя
Все семь решений объединяет то, что ни одно из них не даёт ошибки. Система работает, отчёты открываются, документы проводятся — просто с каждым месяцем чуть медленнее, и списывают это на рост данных. Плавное падение производительности без привязки к релизам и есть главный признак того, что копать надо в схеме, а не в последних доработках.
Раз в квартал по базе стоит пройтись коротким списком. Двигается ли граница рассчитанных итогов. Нет ли регистров оборотов, по которым в отчётах считают остаток сложением. Не включено ли разделение итогов там, где идёт контроль при проведении. Как соотносятся размеры таблиц регистрации изменений и самих данных. Что накопилось в процедурах проведения сверх записи движений.
Если хотите заодно проверить себя, пройдите короткий тест по архитектуре 1С. Он поможет понять, какие темы уже не вызывают вопросов, а где остались пробелы.
Половина находок чинится настройками и переносом кода из транзакции, вторая половина требует перепроведения истории. Вторую половину лучше находить пораньше: окно выходных, за которое база перепроводится сегодня, через два года превращается в неделю простоя, а такое бизнес уже не согласует.

Ошибки в архитектуре 1С дешевле находить до того, как они превращаются в перепроведение базы и выходные с остановкой системы. На бесплатных открытых уроках OTUS можно посмотреть, как сделать этот контроль частью самого процесса разработки — от технического проекта до тестов и релиза.
27 августа в 20:00 — «Технический проект 1С с ИИ: от структуры до ревью». Записаться
8 сентября в 19:00 — «Автотесты 1С через ИИ: от запуска до контроля результата». Записаться
17 сентября в 20:00 — «Управление релизами в 1С: GitFlow, code review и CI/CD на практике». Записаться
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.