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

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

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

В чем заключается суть проблемы и ограничение платформы 1С

Основная идея, которая часто возникает у разработчиков: почему бы не очистить движения по регистру накопления (например, Товары на складах или Продажи) не по одному документу, а сразу целой «пачкой»? К сожалению, здесь мы упираемся в фундаментальное ограничение платформы «1С:Предприятие».

Объект НаборЗаписей для любого типа регистра требует, чтобы в отборе по регистратору использовался только один конкретный документ. Платформа жестко контролирует это условие на уровне ядра. Мы не можем установить вид сравнения В списке для отбора по регистратору в наборе записей:


// Такой код вызовет ошибку выполнения, так как платформа не поддерживает списки в отборе набора записей
НаборЗаписей = РегистрыНакопления.ТоварыНаСкладах.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.ВидСравнения = ВидСравнения.ВСписке; // Ошибка!
НаборЗаписей.Отбор.Регистратор.Значение = СписокЧеков;

Поскольку массовый отбор невозможен, разработчикам приходится прибегать к циклическому обходу документов и последовательной записи пустого набора записей для каждого регистратора. Если документов более 200–300, этот процесс начинает занимать минуты, парализуя работу всей СУБД. Давайте проанализируем, почему это происходит и как мы можем кардинально ускорить данный процесс.

Решение 1. Использование механизма «ОбменДанными.Загрузка»

Выясним причину, почему запись пустого набора записей в цикле для 300 документов может выполняться несколько минут. В типовых конфигурациях при записи движений регистров срабатывают многочисленные подписки на события, а также тяжелый код в модулях самих регистров (процедуры ПередЗаписью и ПриЗаписи). Там могут выполняться дополнительные запросы к базе данных, проверки остатков, расчеты себестоимости и другие ресурсоемкие операции.

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

Рассмотрим пример кода, реализующего этот подход:


Процедура ОчиститьДвиженияЧековБыстро(МассивЧеков)
    
    Для Каждого ТекущийЧек Из МассивЧеков Цикл
        // Создаем пустой набор записей для нужного регистра
        НаборЗаписей = РегистрыНакопления.ТоварыНаСкладах.СоздатьНаборЗаписей();
        НаборЗаписей.Отбор.Recorder.Установить(ТекущийЧек);
        
        // Включаем режим загрузки для отключения обработчиков и подписок
        НаборЗаписей.ОбменДанными.Загрузка = Истина;
        
        Попытка
            НаборЗаписей.Записать(); // Записывает пустой набор движений
        Исключение
            ЗаписьЖурналаРегистрации("ОчисткаДвижений", УровеньЖурналаРегистрации.Ошибка,,, 
                "Не удалось очистить движения для документа: " + ТекущийЧек + ". Ошибка: " + КраткоеПредставлениеОшибки(ИнформацияОбОшибке()));
        КонецПопытки;
    КонецЦикла;
    
КонецПроцедуры

Использование этого флага снижает нагрузку на СУБД в десятки раз, так как исключает выполнение неоптимального кода, написанного в модулях конфигурации (подобного тому, что представлен в сообщении 14 темы форума).

Решение 2. Программное управление итогами регистров накопления

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

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

Разберем этот алгоритм по шагам:

  1. Отключаем использование итогов для регистров, по которым будут удаляться движения.
  2. В цикле с использованием свойства ОбменДанными.Загрузка = Истина записываем пустые наборы записей.
  3. Включаем использование итогов обратно.
  4. Выполняем пересчет итогов за период проведения документов.

Пример программной реализации этого метода:


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

Решение 3. Попорционные транзакции и обход блокировок СУБД

В исходной теме автор упоминает проблему конфликта блокировок при попытке получить данные документов (сообщение 23). В СУБД при выполнении длительных транзакций (например, при удалении 300 чеков в одной транзакции) мелкие блокировки строк могут перерастать в блокировку всей таблицы (Lock Escalation). Это парализует работу касс на других точках.

Чтобы избежать этой проблемы, необходимо разделять обработку данных на небольшие порции (батчи). Каждая порция должна обрабатываться в своей собственной транзакции. Оптимальный размер порции составляет от 20 до 50 документов.

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


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

Решение 4. Архивация вместо физического удаления и асинхронный отложенный запуск

В процессе обсуждения на форуме (сообщения 19, 22) прозвучала важная методологическая рекомендация: не выполнять удаление и тяжелую архивацию документов непосредственно в момент закрытия смены кассиром.

Когда кассир нажимает кнопку «Закрыть смену», его основная задача — быстро получить Z-отчет на фискальном регистраторе и дождаться проведения сводного документа Отчет о розничных продажах. Физическое удаление сотен чеков ККМ с помощью платформенного метода УдалитьОбъекты(МассивЧеков, Ложь) заставляет 1С проверять ссылочную целостность по всей базе данных для каждой ссылки, что занимает колоссальное время.

Рекомендуемый сценарий оптимизации процесса закрытия смены:

  1. В настройках параметров учета 1С:УТ 10.3 выберите режим «Архивировать чеки ККМ» вместо их непосредственного удаления. При архивации чеки просто снимаются с проведения и помечаются специальным признаком, что происходит в разы быстрее, чем физическое удаление объекта из базы данных.
  2. Для удаления архивных чеков или полной очистки их движений используйте механизм отложенной обработки. Зарегистрируйте ссылки на удаляемые чеки на специальном узле плана обмена или запишите их во временный регистр сведений.
  3. Настройте регламентное (фоновое) задание, которое будет запускаться автоматически в ночное время (например, в 02:00), когда нагрузка на сервер минимальна. Это задание выберет накопленный пул документов и выполнит их очистку/удаление порциями без создания помех пользователям.

Рассмотрим структуру кода для регистрации чеков к отложенному удалению:


// Код вызывается в момент закрытия смены (быстрая операция)
Процедура ЗарегистрироватьЧекиДляНочногоУдаления(МассивЧеков)
    
    // Записываем ссылки на чеки в специально созданный регистр сведений "ОчередьОчисткиОбъектов"
    Для Каждого ТекущийЧек Из МассивЧеков Цикл
        МенеджерЗаписи = РегистрыСведений.ОчередьОчисткиОбъектов.СоздатьМенеджерЗаписи();
        МенеджерЗаписи.Объект = ТекущийЧек;
        МенеджерЗаписи.ДатаРегистрации = ТекущаяДата();
        МенеджерЗаписи.Записать(Истина); // Быстрая запись без транзакционных блокировок таблиц регистров накопления
    КонецЦикла;
    
КонецПроцедуры

Затем в регламентном задании, выполняемом ночью на сервере 1С, мы обрабатываем зарегистрированные данные:


// Вызывается регламентным заданием в фоне
Процедура ВыполнитьНочнуюОчисткуДвижений()
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ РАЗЛИЧНЫЕ
        |   Очередь.Объект КАК ДокументСсылка
        |ИЗ
        |   РегистрСведений.ОчередьОчисткиОбъектов КАК Очередь";
        
    РезультатЗапроса = Запрос.Выполнить();
    Если РезультатЗапроса.Пустой() Тогда
        Возврат;
    КонецЕсли;
    
    Выборка = РезультатЗапроса.Выбрать();
    МассивЧеков = Новый Массив;
    
    Пока Выборка.Следующий() Цикл
        МассивЧеков.Добавить(Выборка.ДокументСсылка);
    КонецЦикла;
    
    // Используем ранее написанную процедуру с порционной очисткой и отключением обработчиков
    ОчиститьДвиженияПорциями(МассивЧеков);
    
    // После успешной очистки удаляем ссылки из очереди
    Для Каждого ОбработанныйДокумент Из МассивЧеков Цикл
        МенеджерЗаписи = РегистрыСведений.ОчередьОчисткиОбъектов.СоздатьМенеджерЗаписи();
        МенеджерЗаписи.Объект = ОбработанныйДокумент;
        МенеджерЗаписи.Удалить();
    КонецЦикла;
    
КонецПроцедуры

Подведем итоги

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

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

← На главную