При работе в высоконагруженных розничных базах данных, особенно на базе конфигурации «1С:Управление торговлей 10.3», администраторы и программисты часто сталкиваются с критическим падением производительности в конце рабочего дня. Кассиры пытаются закрыть смену, система начинает архивировать или физически удалять сотни и тысячи документов Чек ККМ, база «зависает», возникают транзакционные блокировки, а пользователи других сеансов не могут продолжать работу. В этой статье мы подробно разберем, почему стандартные механизмы очистки движений работают медленно, и как решить эту проблему с помощью различных оптимизационных методик.
Основная идея, которая часто возникает у разработчиков: почему бы не очистить движения по регистру накопления (например, Товары на складах или Продажи) не по одному документу, а сразу целой «пачкой»? К сожалению, здесь мы упираемся в фундаментальное ограничение платформы «1С:Предприятие».
Объект НаборЗаписей для любого типа регистра требует, чтобы в отборе по регистратору использовался только один конкретный документ. Платформа жестко контролирует это условие на уровне ядра. Мы не можем установить вид сравнения В списке для отбора по регистратору в наборе записей:
// Такой код вызовет ошибку выполнения, так как платформа не поддерживает списки в отборе набора записей
НаборЗаписей = РегистрыНакопления.ТоварыНаСкладах.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.ВидСравнения = ВидСравнения.ВСписке; // Ошибка!
НаборЗаписей.Отбор.Регистратор.Значение = СписокЧеков;
Поскольку массовый отбор невозможен, разработчикам приходится прибегать к циклическому обходу документов и последовательной записи пустого набора записей для каждого регистратора. Если документов более 200–300, этот процесс начинает занимать минуты, парализуя работу всей СУБД. Давайте проанализируем, почему это происходит и как мы можем кардинально ускорить данный процесс.
Выясним причину, почему запись пустого набора записей в цикле для 300 документов может выполняться несколько минут. В типовых конфигурациях при записи движений регистров срабатывают многочисленные подписки на события, а также тяжелый код в модулях самих регистров (процедуры ПередЗаписью и ПриЗаписи). Там могут выполняться дополнительные запросы к базе данных, проверки остатков, расчеты себестоимости и другие ресурсоемкие операции.
Чтобы обойти выполнение этих тяжелых обработчиков, мы можем использовать свойство ОбменДанными.Загрузка = Истина. Установив этот флаг для очищаемого набора записей, мы сообщаем платформе, что запись производится в режиме загрузки данных, и все прикладные проверки и подписки на события выполнять не нужно. Это абсолютно безопасно, если мы просто очищаем движения (записываем пустой набор).
Рассмотрим пример кода, реализующего этот подход:
Процедура ОчиститьДвиженияЧековБыстро(МассивЧеков)
Для Каждого ТекущийЧек Из МассивЧеков Цикл
// Создаем пустой набор записей для нужного регистра
НаборЗаписей = РегистрыНакопления.ТоварыНаСкладах.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Recorder.Установить(ТекущийЧек);
// Включаем режим загрузки для отключения обработчиков и подписок
НаборЗаписей.ОбменДанными.Загрузка = Истина;
Попытка
НаборЗаписей.Записать(); // Записывает пустой набор движений
Исключение
ЗаписьЖурналаРегистрации("ОчисткаДвижений", УровеньЖурналаРегистрации.Ошибка,,,
"Не удалось очистить движения для документа: " + ТекущийЧек + ". Ошибка: " + КраткоеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
КонецЦикла;
КонецПроцедуры
Использование этого флага снижает нагрузку на СУБД в десятки раз, так как исключает выполнение неоптимального кода, написанного в модулях конфигурации (подобного тому, что представлен в сообщении 14 темы форума).
Даже при отключении обработчиков событий СУБД продолжает выполнять колоссальный объем работы при записи каждого пустого набора записей. Причина кроется в пересчете таблиц итогов регистра накопления. Платформа 1С для каждой транзакции пытается обновить текущие итоги на конец периода и итоги за актуальный месяц.
Если мы массово очищаем движения сотен документов, мы можем временно отключить использование итогов для конкретных регистров, выполнить пакетную очистку, а затем включить итоги обратно. В этом случае пересчет итогов произойдет один раз в конце всей процедуры, что сэкономит огромное количество дискового времени и процессорных ресурсов сервера.
Разберем этот алгоритм по шагам:
ОбменДанными.Загрузка = Истина записываем пустые наборы записей.Пример программной реализации этого метода:
Процедура ПакетнаяОчисткаСОтключениемИтогов(МассивЧеков)
// Шаг 1. Отключаем итоги для целевых регистров
РегистрыНакопления.ТоварыНаСкладах.УстановитьИспользованиеИтогов(Ложь);
РегистрыНакопления.Продажи.УстановитьИспользованиеИтогов(Ложь);
Попытка
// Шаг 2. Очищаем движения в цикле
Для Каждого ТекущийЧек Из МассивЧеков Цикл
// Очищаем движения по первому регистру
НаборТовары = РегистрыНакопления.ТоварыНаСкладах.СоздатьНаборЗаписей();
НаборТовары.Отбор.Регистратор.Установить(ТекущийЧек);
НаборТовары.ОбменДанными.Загрузка = Истина;
НаборТовары.Записать();
// Очищаем движения по второму регистру
НаборПродажи = РегистрыНакопления.Продажи.СоздатьНаборЗаписей();
НаборПродажи.Отбор.Регистратор.Установить(ТекущийЧек);
НаборПродажи.ОбменДанными.Загрузка = Истина;
НаборПродажи.Записать();
КонецЦикла;
Исключение
// В случае ошибки обязательно включаем итоги назад, чтобы не нарушить работу пользователей
РегистрыНакопления.ТоварыНаСкладах.УстановитьИспользованиеИтогов(Истина);
РегистрыНакопления.Продажи.УстановитьИспользованиеИтогов(Истина);
ВызватьИсключение;
КонецПопытки;
// Шаг 3. Включаем итоги и запускаем их пересчет
РегистрыНакопления.ТоварыНаСкладах.УстановитьИспользованиеИтогов(Истина);
РегистрыНакопления.Продажи.УстановитьИспользованиеИтогов(Истина);
КонецПроцедуры
В исходной теме автор упоминает проблему конфликта блокировок при попытке получить данные документов (сообщение 23). В СУБД при выполнении длительных транзакций (например, при удалении 300 чеков в одной транзакции) мелкие блокировки строк могут перерастать в блокировку всей таблицы (Lock Escalation). Это парализует работу касс на других точках.
Чтобы избежать этой проблемы, необходимо разделять обработку данных на небольшие порции (батчи). Каждая порция должна обрабатываться в своей собственной транзакции. Оптимальный размер порции составляет от 20 до 50 документов.
Проанализируем пример пакетной обработки документов с ограничением транзакций по времени и количеству:
Процедура ОчиститьДвиженияПорциями(МассивЧеков)
РазмерПорции = 50;
ВсегоДокументов = МассивЧеков.Количество();
ТекущийИндекс = 0;
Пока ТекущийИндекс < ВсегоДокументов Цикл
НачатьТранзакцию();
Попытка
Для Шаг = 1 По РазмерПорции Цикл
Если ТекущийИндекс >= ВсегоДокументов Тогда
Прервать;
КонецЕсли;
ТекущийЧек = МассивЧеков[ТекущийИндекс];
// Очищаем движения
Набор = РегистрыНакопления.ТоварыНаСкладах.СоздатьНаборЗаписей();
Набор.Отбор.Регистратор.Установить(ТекущийЧек);
Набор.ОбменДанными.Загрузка = Истина;
Набор.Записать();
ТекущийИндекс = ТекущийИндекс + 1;
КонецЦикла;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
// Записываем лог и продолжаем работу со следующей порцией
ЗаписьЖурналаРегистрации("ОчисткаПорциями", УровеньЖурналаРегистрации.Предупреждение,,,
"Ошибка при обработке порции документов начиная с индекса " + ТекущийИндекс);
// Сдвигаем индекс, чтобы пропустить проблемную порцию или обработать ее индивидуально
ТекущийИндекс = ТекущийИндекс + 1;
КонецПопытки;
КонецЦикла;
КонецПроцедуры
В процессе обсуждения на форуме (сообщения 19, 22) прозвучала важная методологическая рекомендация: не выполнять удаление и тяжелую архивацию документов непосредственно в момент закрытия смены кассиром.
Когда кассир нажимает кнопку «Закрыть смену», его основная задача — быстро получить Z-отчет на фискальном регистраторе и дождаться проведения сводного документа Отчет о розничных продажах. Физическое удаление сотен чеков ККМ с помощью платформенного метода УдалитьОбъекты(МассивЧеков, Ложь) заставляет 1С проверять ссылочную целостность по всей базе данных для каждой ссылки, что занимает колоссальное время.
Рекомендуемый сценарий оптимизации процесса закрытия смены:
Рассмотрим структуру кода для регистрации чеков к отложенному удалению:
// Код вызывается в момент закрытия смены (быстрая операция)
Процедура ЗарегистрироватьЧекиДляНочногоУдаления(МассивЧеков)
// Записываем ссылки на чеки в специально созданный регистр сведений "ОчередьОчисткиОбъектов"
Для Каждого ТекущийЧек Из МассивЧеков Цикл
МенеджерЗаписи = РегистрыСведений.ОчередьОчисткиОбъектов.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Объект = ТекущийЧек;
МенеджерЗаписи.ДатаРегистрации = ТекущаяДата();
МенеджерЗаписи.Записать(Истина); // Быстрая запись без транзакционных блокировок таблиц регистров накопления
КонецЦикла;
КонецПроцедуры
Затем в регламентном задании, выполняемом ночью на сервере 1С, мы обрабатываем зарегистрированные данные:
// Вызывается регламентным заданием в фоне
Процедура ВыполнитьНочнуюОчисткуДвижений()
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| Очередь.Объект КАК ДокументСсылка
|ИЗ
| РегистрСведений.ОчередьОчисткиОбъектов КАК Очередь";
РезультатЗапроса = Запрос.Выполнить();
Если РезультатЗапроса.Пустой() Тогда
Возврат;
КонецЕсли;
Выборка = РезультатЗапроса.Выбрать();
МассивЧеков = Новый Массив;
Пока Выборка.Следующий() Цикл
МассивЧеков.Добавить(Выборка.ДокументСсылка);
КонецЦикла;
// Используем ранее написанную процедуру с порционной очисткой и отключением обработчиков
ОчиститьДвиженияПорциями(МассивЧеков);
// После успешной очистки удаляем ссылки из очереди
Для Каждого ОбработанныйДокумент Из МассивЧеков Цикл
МенеджерЗаписи = РегистрыСведений.ОчередьОчисткиОбъектов.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Объект = ОбработанныйДокумент;
МенеджерЗаписи.Удалить();
КонецЦикла;
КонецПроцедуры
Для радикального ускорения работы с движениями группы документов при закрытии смены в 1С необходимо отказаться от попыток прямого группового удаления движений через стандартные формы или динамические списки, так как это невозможно из-за ограничений платформы. Вместо этого используйте комплексный подход:
ОбменДанными.Загрузка = Истина, чтобы отключить выполнение тяжелых обработчиков при записи наборов.Эти проверенные методы позволят сократить время закрытия смены с нескольких минут до нескольких секунд и сделают работу розничных магазинов стабильной и быстрой.