Как быстро заполнить новое измерение в регистре накопления без перепроведения документов в 1С?

Программист 1С v8.3 (Управляемые формы) Управленческий учет IT и автоматизация бизнеса
← На главную

В процессе развития и доработки корпоративных информационных систем на базе платформы 1С разработчики регулярно сталкиваются с классической задачей: в существующий и активно используемый регистр накопления (например, Продажи) добавляется новое измерение (например, Продавец). Начиная с этого момента, новые документы будут корректно записывать данные в это измерение, но как быть с историческими данными за текущий год или прошлые периоды?

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

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

Анатомия решения: Почему именно перезапись наборов записей?

Для изменения исторических данных в обход стандартной логики проведения документов используется механизм прямого обращения к движениям через объект РегистрНакопленияНаборЗаписей с установкой фильтра по регистратору. Этот подход позволяет точечно прочитать движения конкретного документа, программно изменить значение в новой колонке (измерении) и записать набор обратно в базу данных.

При этом критически важно использовать свойство ОбменДанными.Загрузка = Истина. Этот режим отключает платформенные и конфигурационные проверки (такие как проверка остатков, расчеты себестоимости, подписки на события при записи движений и т.д.), что гарантирует колоссальную скорость работы и минимизирует вероятность возникновения конфликтов блокировок.

Проанализируем базовый алгоритм действий, который нам предстоит реализовать:

  1. Выбрать с помощью запроса все документы-регистраторы, которые сформировали движения по интересующему нас регистру накопления за необходимый исторический период.
  2. В цикле по каждому регистратору прочитать его движения в виде набора записей.
  3. Для каждой строки движений найти соответствующего аналитического аналитика (в нашем примере — продавца) из самого документа (например, из его табличной части Товары).
  4. Заполнить новое измерение Продавец в записи регистра.
  5. Установить режим обмена данными и записать измененный набор обратно в базу.

Пошаговая реализация базового алгоритма

Рассмотрим пример кода внешней обработки, который решает эту задачу. Предположим, что в табличной части Товары наших документов продажи есть реквизит Менеджер (или Продавец), значение которого нам нужно транслировать в аналогичное новое измерение регистра накопления.


// Шаг 1. Получаем список документов-регистраторов, сделавших движения в периоде
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ РАЗЛИЧНЫЕ
    |   РегистрПродажи.Регистратор КАК Регистратор
    |ИЗ
    |   РегистрНакопления.Продажи КАК РегистрПродажи
    |ГДЕ
    |   РегистрПродажи.Период МЕЖДУ &НачалоПериода И &КонецПериода";

Запрос.УстановитьПараметр("НачалоПериода", НачалоГода(ТекущаяДата()));
Запрос.УстановитьПараметр("КонецПериода", КонецДня(ТекущаяДата()));

РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();

// Шаг 2. Обходим каждый регистратор в цикле
Пока Выборка.Следующий() Цикл
    
    // Создаем набор записей с отбором по конкретному регистратору
    НаборЗаписей = РегистрыНакопления.Продажи.СоздатьНаборЗаписей();
    НаборЗаписей.Отбор.Регистратор.Установить(Выборка.Регистратор);
    НаборЗаписей.Прочитать();
    
    Если НаборЗаписей.Количество() = 0 Тогда
        Продолжить;
    КонецЕсли;
    
    // Получаем объект документа для чтения его табличной части
    ДокументОбъект = Выборка.Регистратор.ПолучитьОбъект();
    Если ДокументОбъект = Неопределено Тогда
        Продолжить;
    КонецЕсли;
    
    ДанныеИзменены = Ложь;
    
    // Шаг 3. Сопоставляем движения и строки табличной части документа
    Для Каждого Запись Из НаборЗаписей Цикл
        
        // Пытаемся найти строку ТЧ по номеру строки (наиболее надежный и быстрый способ)
        СтрокаТЧ = ДокументОбъект.Товары.Найти(Запись.НомерСтроки, "НомерСтроки");
        
        Если СтрокаТЧ <> Неопределено Тогда
            // Если нашли строку — заполняем новое измерение
            Запись.Продавец = СтрокаТЧ.Менеджер;
            ДанныеИзменены = Истина;
        Иначе
            // Альтернативный поиск по Номенклатуре, если номера строк не совпадают
            ПараметрыОтбора = Новый Структура("Номенклатура", Запись.Номенклатура);
            НайденныеСтроки = ДокументОбъект.Товары.НайтиСтроки(ПараметрыОтбора);
            Если НайденныеСтроки.Количество() > 0 Тогда
                Запись.Продавец = НайденныеСтроки[0].Менеджер;
                ДанныеИзменены = Истина;
            КонецЕсли;
        КонецЕсли;
        
    КонецЦикла;
    
    // Шаг 4. Записываем измененный набор обратно в ИБ
    Если ДанныеИзменены Тогда
        НаборЗаписей.ОбменДанными.Загрузка = Истина;
        НаборЗаписей.Записать();
    КонецЕсли;
    
КонецЦикла;

Этот код отлично справится с задачей на относительно небольших базах данных. Однако, если объемы исчисляются миллионами записей, нам потребуются серьезные оптимизационные архитектурные решения.

Оптимизация производительности для крупных баз данных

Для крупных информационных систем последовательное выполнение такой обработки «в лоб» в основном сеансе может парализовать работу пользователей и привести к переполнению транзакционного лога СУБД. Разберем ключевые методики оптимизации.

1. Использование нового режима замещения «Обновление» (в версиях платформы 8.3.26+)

В традиционных версиях платформы 1С запись набора записей регистра накопления всегда происходила по жесткому принципу: «полное удаление старых движений по регистратору -> вставка нового пакета движений». При миллионных объемах это создавало колоссальную нагрузку на дисковую подсистему СУБД.

Начиная с версий платформы 1С **8.3.26 / 8.3.27**, разработчики получили долгожданный инструмент — системное перечисление РежимЗамещения при вызове метода Записать(). Для нашей задачи идеально подходит РежимЗамещения.Обновление.

При использовании этого режима платформа не удаляет старые записи, а выполняет точечные SQL-запросы класса UPDATE только для тех колонок, значения которых действительно изменились (в нашем случае — колонка Продавец), сопоставляя записи по ключевым полям Регистратор и НомерСтроки. Это повышает скорость записи в десятки раз и минимизирует блокировки таблиц СУБД.

Применим этот режим в коде записи:


// Пример оптимизированной записи для платформ 8.3.26+
НаборЗаписей.ОбменДанными.Загрузка = Истина;
НаборЗаписей.Записать(Истина, РежимЗамещения.Обновление);

2. Порционность и транзакционность

Никогда не выполняйте обработку огромного массива документов в рамках одной транзакции. Разделите обработку на фиксированные порции (например, по 1000 документов или по периодам в одну неделю) и фиксируйте транзакции для каждого пакета отдельно. Это убережет базу от раздувания лога транзакций СУБД.


// Шаблон порционной обработки с фиксацией транзакций
НачатьТранзакцию();
СчетчикДокументов = 0;

Пока Выборка.Следующий() Цикл
    
    // [Тут располагается логика чтения и изменения набора записей]
    
    СчетчикДокументов = СчетчикДокументов + 1;
    
    // Каждые 1000 документов фиксируем транзакцию и открываем новую
    Если СчетчикДокументов % 1000 = 0 Тогда
        ЗафиксироватьТранзакцию();
        НачатьТранзакцию();
    КонецЕсли;
    
КонецЦикла;

Если ТранзакцияАктивна() Тогда
    ЗафиксироватьТранзакцию();
EndЕсли;

3. Асинхронное выполнение через Фоновые Задания

Запуск длительных обработок в интерактивном режиме гарантированно приведет к зависанию клиентского приложения. Заполнение исторических данных следует выносить в фоновые задания на сервере приложений. Более того, при гигантских объемах данных этот процесс можно распараллелить — например, запустить 5 параллельных фоновых заданий, каждое из которых обрабатывает свой интервал дат или свой пул документов по уникальному идентификатору (GUID).

4. Обход даты запрета изменения данных

При работе в закрытых периодах (например, в прошлых кварталах или годах) даже свойство ОбменДанными.Загрузка = Истина в типовых конфигурациях (на базе БСП) может натолкнуться на блокировки со стороны механизмов проверки дат запрета редактирования.

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

5. Альтернатива: нужно ли физическое измерение в регистре?

Прежде чем приступать к масштабной процедуре дополнения движений, давайте зададимся важным архитектурным вопросом: **действительно ли нам необходимо физическое измерение в регистре накопления?**

Если продавец всегда однозначно совпадает с реквизитом шапки документа (например, Ответственный или Менеджер), то физически хранить это измерение в каждой строке регистра — нерационально с точки зрения объема базы данных и избыточно.

В этом случае в отчетах информацию о продавце можно получать динамически, связав виртуальную таблицу регистра с таблицей документов-регистраторов с помощью левого соединения по полю Регистратор.

Физическое измерение в регистре действительно необходимо только в двух случаях:

  1. В одном документе в разных строках табличной части могут быть указаны разные продавцы (как справедливо заметил участник форума в Сообщении 5).
  2. Требуется предельно высокая скорость построения аналитических отчетов СУБД с жесткой фильтрацией по продавцам на огромных массивах данных (физическое индексированное поле в таблице регистра работает быстрее, чем динамическое соединение с таблицами документов).

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

← На главную