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

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

Загрузка больших объемов данных, таких как прайс-листы поставщиков объемом в 1 000 000 строк и более, — классическая задача, с которой сталкиваются разработчики в крупных торговых компаниях. При работе с такими массивами стандартные методы платформы 1С:Предприятие часто дают сбой. Дисковая подсистема перегружается до 100%, возникают взаимные блокировки СУБД (deadlocks), а сеансы пользователей аварийно завершаются из-за нехватки оперативной памяти рабочих процессов.

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

Почему стандартные подходы не работают? Анализ узких мест

Для начала давайте проанализируем типичный алгоритм загрузки, который часто приводит к катастрофическому падению производительности сервера:

  1. Файл (CSV или MDB) считывается на стороне клиента в таблицу значений.
  2. Полученная таблица передается на сервер.
  3. Серверный код выбирает запросом текущие цены из регистра для сравнения.
  4. В цикле создаются документы УстановкаЦенНоменклатуры (например, по 10 000 строк в каждом) и программно проводятся.

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

Ниже мы рассмотрим пошаговые решения для устранения каждого из этих узких мест.

Метод 1. Фильтрация неизменившихся цен «на лету» (Экономия 90% ресурсов)

Самый большой резерв производительности при загрузке огромных прайс-листов — избегать записи лишних данных. На практике из 1 000 000 позиций в прайс-листе реально меняются цены лишь у 5–15% товаров. Попытка записать и провести все 100% строк создает колоссальную бесполезную нагрузку на сервер.

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

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


// Помещаем таблицу загрузки во временную таблицу для сопоставления в СУБД
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ВТ_Загрузка.Номенклатура КАК Номенклатура,
    |	ВТ_Загрузка.ВидЦены КАК ВидЦены,
    |	ВТ_Загрузка.Цена КАК НоваяЦена,
    |	ВТ_Загрузка.Валюта КАК Валюта
    |ПОМЕСТИТЬ ВТ_НовыеЦены
    |ИЗ
    |	&ТаблицаЗагрузки КАК ВТ_Загрузка
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |	ВТ_НовыеЦены.Номенклатура КАК Номенклатура,
    |	ВТ_НовыеЦены.ВидЦены КАК ВидЦены,
    |	ВТ_НовыеЦены.НоваяЦена КАК Цена,
    |	ВТ_НовыеЦены.Валюта КАК Валюта
    |ИЗ
    |	ВТ_НовыеЦены КАК ВТ_НовыеЦены
    |		ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, ) КАК ЦеныСрезПоследних
    |		ПО ВТ_НовыеЦены.Номенклатура = ЦеныСрезПоследних.Номенклатура
    |			И ВТ_НовыеЦены.ВидЦены = ЦеныСрезПоследних.ВидЦены
    |ГДЕ
    |	ЦеныСрезПоследних.Цена ЕСТЬ NULL
    |	ИЛИ ВТ_НовыеЦены.НоваяЦена <> ЦеныСрезПоследних.Цена
    |	ИЛИ ВТ_НовыеЦены.Валюта <> ЦеныСрезПоследних.Валюта";

Запрос.УстановитьПараметр("ТаблицаЗагрузки", ИсходнаяТаблицаЦен);
Запрос.УстановитьПараметр("Период", ТекущаяДата());

ТаблицаДляЗаписи = Запрос.Выполнить().Выгрузить();

Этот простой шаг позволяет сократить объем записываемых данных с 1 000 000 до, например, 50 000 строк. Нагрузка на СУБД и дисковую подсистему моментально снизится в разы.

Метод 2. Управление транзакциями СУБД (Борьба с просадкой диска)

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

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

Разберем по шагам, как правильно обернуть процесс создания документов в явную транзакцию:


КоличествоДокументовВТранзакции = 10;
СчетчикДокументов = 0;

НачатьТранзакцию();
Попытка
    Для Каждого ПакетДанных Из МассивПакетовДанных Цикл
        
        // Создаем и записываем документ
        ДокументОбъект = СоздатьДокументУстановкиЦен(ПакетДанных);
        ДокументОбъект.Записать(РежимЗаписиДокумента.Запись);
        
        СчетчикДокументов = СчетчикДокументов + 1;
        
        // Фиксируем транзакцию при достижении лимита и открываем новую
        Если СчетчикДокументов % КоличествоДокументовВТранзакции = 0 Тогда
            ЗафиксироватьТранзакцию();
            НачатьТранзакцию();
        КонецЕсли;
        
    КонецЦикла;
    
    // Фиксируем остаток транзакции
    Если ТранзакцияАктивна() Тогда
        ЗафиксироватьТранзакцию();
    КонецЕсли;
Исключение
    Если ТранзакцияАктивна() Тогда
        ОтменитьТранзакцию();
    КонецЕсли;
    ВызватьИсключение КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
КонецПопытки;

Метод 3. Сверхбыстрая запись через ОбменДанными.Загрузка = Истина

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

Напрямую провести документ с флагом ОбменДанными.Загрузка = Истина платформа запрещает. Но мы можем применить высокоэффективный обходной путь:

  1. Создаем документ, заполняем реквизиты шапки и принудительно устанавливаем признак Проведен = Истина прямо в объекте.
  2. Устанавливаем для объекта признак ОбменДанными.Загрузка = Истина.
  3. Записываем документ в режиме обычной записи: Записать(РежимЗаписиДокумента.Запись). Документ мгновенно запишется в базу данных и будет иметь статус проведенного, но не создаст движений по регистру.
  4. Формируем набор записей регистра сведений ЦеныНоменклатуры с отбором по созданному документу-регистратору.
  5. Напрямую заполняем этот набор записей движениями.
  6. Устанавливаем у набора записей ОбменДанными.Загрузка = Истина и записываем его методом Записать().

Давайте посмотрим на программный пример реализации этой схемы:


НовыйДокумент = Документы.УстановкаЦенНоменклатуры.СоздатьДокумент();
НовыйДокумент.Дата = ТекущаяДата();
НовыйДокумент.Ответственный = ПараметрыСеанса.ТекущийПользователь;
НовыйДокумент.Согласован = Истина;

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

// Записываем движения напрямую в регистр
НаборДвижений = РегистрыСведений.ЦеныНоменклатуры.СоздатьНаборЗаписей();
НаборДвижений.Отбор.Регистратор.Установить(НовыйДокумент.Ссылка);

Для Каждого СтрокаЦены Из ТаблицаПакета Цикл
    Движение = НаборДвижений.Добавить();
    Движение.Период = НовыйДокумент.Дата;
    Движение.Регистратор = НовыйДокумент.Ссылка;
    Движение.Номенклатура = СтрокаЦены.Номенклатура;
    Движение.ВидЦены = СтрокаЦены.ВидЦены;
    Движение.Цена = СтрокаЦены.Цена;
    Движение.Валюта = СтрокаЦены.Валюта;
КонецЦикла;

НаборДвижений.ОбменДанными.Загрузка = Истина;
НаборДвижений.Записать();

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

Метод 4. Потоковое чтение файлов строго на сервере

При передаче таблицы значений на 1 000 000 строк между клиентом и сервером система тратит огромное количество времени на сериализацию данных, а память рабочего процесса переполняется. Чтобы решить эту проблему, чтение файла должно происходить исключительно на сервере.

Вместо того чтобы читать весь файл целиком в память, мы рекомендуем читать его последовательно порциями. Для файлов формата CSV это легко реализовать с помощью объекта ЧтениеТекста:


// Передаем файл из временного хранилища на сервер и сохраняем во временный файл
ПутьКФайлу = ПолучитьИмяВременногоФайла("csv");
ДвоичныеДанныеФайла = ПолучитьИзВременногоХранилища(АдресВХранилище);
ДвоичныеДанныеФайла.Записать(ПутьКФайлу);

ЧтениеCSV = Новый ЧтениеТекста;
ЧтениеCSV.Открыть(ПутьКФайлу, КодировкаТекста.UTF8);

БуферДанных = Новый ТаблицаЗначений;
БуферДанных.Колонки.Добавить("Артикул", Новый ОписаниеТипов("Строка"));
БуферДанных.Колонки.Добавить("Цена", Новый ОписаниеТипов("Число"));

РазмерПорции = 20000;
СчетчикСтрок = 0;

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

// Дозаписываем остаток строк
Если БуферДанных.Количество() > 0 Тогда
    ЗагрузитьПорциюВБазу(БуферДанных);
КонецЕсли;

ЧтениеCSV.Закрыть();
УдалитьФайлы(ПутьКФайлу);

Метод 5. Многопоточность: деление по Номенклатуре, а не по Типам цен

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

Если вы хотите запустить загрузку в несколько потоков через фоновые задания, разделять потоки нужно строго по диапазонам Номенклатуры (например, по уникальному идентификатору GUID, первой букве кода или по родительским группам справочника).

Метод 6. Временное отключение некластеризованных индексов СУБД

Если загрузка прайс-листа выполняется раз в месяц в выделенное технологическое окно, имеет смысл привлечь системного администратора СУБД (MS SQL Server или PostgreSQL).

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

Резюме и чеклист разработчика

Подведем итог. Чтобы оптимизировать загрузку прайс-листа объемом 1 000 000 строк, используйте следующий комплекс мер:

  1. Не загружайте данные, которые не изменились. Отсекайте их левым соединением со срезом последних перед формированием документов.
  2. Читайте файлы только на сервере. Используйте потоковое чтение (например, ЧтениеТекста) порциями, чтобы не перегружать оперативную память сервера.
  3. Используйте ручное управление транзакциями. Оборачивайте запись группы документов (например, по 5-10 документов с 5 000 - 10 000 строк) в явную транзакцию.
  4. Записывайте движения напрямую. Записывайте документы в режиме записи с признаком Проведен = Истина и флагом ОбменДанными.Загрузка = Истина, а движения пишите в регистр напрямую через набор записей.
  5. Настраивайте многопоточность правильно. Распределяйте фоновые задания исключительно по диапазонам номенклатуры, чтобы избежать взаимных блокировок.
← На главную