Загрузка больших объемов данных, таких как прайс-листы поставщиков объемом в 1 000 000 строк и более, — классическая задача, с которой сталкиваются разработчики в крупных торговых компаниях. При работе с такими массивами стандартные методы платформы 1С:Предприятие часто дают сбой. Дисковая подсистема перегружается до 100%, возникают взаимные блокировки СУБД (deadlocks), а сеансы пользователей аварийно завершаются из-за нехватки оперативной памяти рабочих процессов.
В этой статье мы подробно разберем, почему простые решения (например, банальное уменьшение порций документов) часто делают загрузку еще медленнее, и рассмотрим 6 эффективных архитектурных и программных методов оптимизации, которые позволят загружать миллионные прайсы за минуты, не нарушая стабильность работы пользователей.
Для начала давайте проанализируем типичный алгоритм загрузки, который часто приводит к катастрофическому падению производительности сервера:
УстановкаЦенНоменклатуры (например, по 10 000 строк в каждом) и программно проводятся.При запуске такого алгоритма на миллионном объеме система упирается в три ключевых барьера:
УстановкаЦенНоменклатуры платформа блокирует диапазоны таблиц регистра сведений ЦеныНоменклатуры. Если одновременно выполняется запись нескольких документов или загрузка идет в несколько потоков, разделенных некорректно (например, по типам цен), транзакции начинают ожидать друг друга, что приводит к ошибкам «Превышено время ожидания предоставления блокировки».Ниже мы рассмотрим пошаговые решения для устранения каждого из этих узких мест.
Самый большой резерв производительности при загрузке огромных прайс-листов — избегать записи лишних данных. На практике из 1 000 000 позиций в прайс-листе реально меняются цены лишь у 5–15% товаров. Попытка записать и провести все 100% строк создает колоссальную бесполезную нагрузку на сервер.
Выясним, как реализовать фильтрацию. Нам необходимо сравнить загружаемые цены с уже имеющимися в базе данных с помощью левого соединения в запросе и оставить только измененные строки.
Рассмотрим пример кода, реализующий этот подход:
// Помещаем таблицу загрузки во временную таблицу для сопоставления в СУБД
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ВТ_Загрузка.Номенклатура КАК Номенклатура,
| ВТ_Загрузка.ВидЦены КАК ВидЦены,
| ВТ_Загрузка.Цена КАК НоваяЦена,
| ВТ_Загрузка.Валюта КАК Валюта
|ПОМЕСТИТЬ ВТ_НовыеЦены
|ИЗ
| &ТаблицаЗагрузки КАК ВТ_Загрузка
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_НовыеЦены.Номенклатура КАК Номенклатура,
| ВТ_НовыеЦены.ВидЦены КАК ВидЦены,
| ВТ_НовыеЦены.НоваяЦена КАК Цена,
| ВТ_НовыеЦены.Валюта КАК Валюта
|ИЗ
| ВТ_НовыеЦены КАК ВТ_НовыеЦены
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, ) КАК ЦеныСрезПоследних
| ПО ВТ_НовыеЦены.Номенклатура = ЦеныСрезПоследних.Номенклатура
| И ВТ_НовыеЦены.ВидЦены = ЦеныСрезПоследних.ВидЦены
|ГДЕ
| ЦеныСрезПоследних.Цена ЕСТЬ NULL
| ИЛИ ВТ_НовыеЦены.НоваяЦена <> ЦеныСрезПоследних.Цена
| ИЛИ ВТ_НовыеЦены.Валюта <> ЦеныСрезПоследних.Валюта";
Запрос.УстановитьПараметр("ТаблицаЗагрузки", ИсходнаяТаблицаЦен);
Запрос.УстановитьПараметр("Период", ТекущаяДата());
ТаблицаДляЗаписи = Запрос.Выполнить().Выгрузить();
Этот простой шаг позволяет сократить объем записываемых данных с 1 000 000 до, например, 50 000 строк. Нагрузка на СУБД и дисковую подсистему моментально снизится в разы.
Если мы разбиваем документы на слишком мелкие порции (например, по 1 000 строк), общее время выполнения операции может вырасти в несколько раз (как и произошло на тестовой базе автора темы). Это связано с накладными расходами на открытие и фиксацию транзакций СУБД для каждого отдельного документа.
Чтобы нивелировать этот эффект, мы должны объединять запись нескольких документов в одну транзакцию. СУБД запишет данные в буфер лога и физически сбросит их на диск один раз для всей группы, а не для каждого документа отдельно.
Разберем по шагам, как правильно обернуть процесс создания документов в явную транзакцию:
КоличествоДокументовВТранзакции = 10;
СчетчикДокументов = 0;
НачатьТранзакцию();
Попытка
Для Каждого ПакетДанных Из МассивПакетовДанных Цикл
// Создаем и записываем документ
ДокументОбъект = СоздатьДокументУстановкиЦен(ПакетДанных);
ДокументОбъект.Записать(РежимЗаписиДокумента.Запись);
СчетчикДокументов = СчетчикДокументов + 1;
// Фиксируем транзакцию при достижении лимита и открываем новую
Если СчетчикДокументов % КоличествоДокументовВТранзакции = 0 Тогда
ЗафиксироватьТранзакцию();
НачатьТранзакцию();
КонецЕсли;
КонецЦикла;
// Фиксируем остаток транзакции
Если ТранзакцияАктивна() Тогда
ЗафиксироватьТранзакцию();
КонецЕсли;
Исключение
Если ТранзакцияАктивна() Тогда
ОтменитьТранзакцию();
КонецЕсли;
ВызватьИсключение КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
КонецПопытки;
При стандартном проведении документа платформа 1С запускает тяжелую бизнес-логику: подписки на события, проверку заполнения реквизитов, проверку прав доступа и расчеты на сервере. При массовом импорте это создает колоссальные накладные расходы.
Напрямую провести документ с флагом ОбменДанными.Загрузка = Истина платформа запрещает. Но мы можем применить высокоэффективный обходной путь:
Проведен = Истина прямо в объекте.ОбменДанными.Загрузка = Истина.Записать(РежимЗаписиДокумента.Запись). Документ мгновенно запишется в базу данных и будет иметь статус проведенного, но не создаст движений по регистру.ЦеныНоменклатуры с отбором по созданному документу-регистратору.ОбменДанными.Загрузка = Истина и записываем его методом Записать().Давайте посмотрим на программный пример реализации этой схемы:
НовыйДокумент = Документы.УстановкаЦенНоменклатуры.СоздатьДокумент();
НовыйДокумент.Дата = ТекущаяДата();
НовыйДокумент.Ответственный = ПараметрыСеанса.ТекущийПользователь;
НовыйДокумент.Согласован = Истина;
// Включаем признак проведения без выполнения обработчиков проведения
НовыйДокумент.Проведен = Истина;
НовыйДокумент.ОбменДанными.Загрузка = Истина;
НовыйДокумент.Записать(РежимЗаписиДокумента.Запись);
// Записываем движения напрямую в регистр
НаборДвижений = РегистрыСведений.ЦеныНоменклатуры.СоздатьНаборЗаписей();
НаборДвижений.Отбор.Регистратор.Установить(НовыйДокумент.Ссылка);
Для Каждого СтрокаЦены Из ТаблицаПакета Цикл
Движение = НаборДвижений.Добавить();
Движение.Период = НовыйДокумент.Дата;
Движение.Регистратор = НовыйДокумент.Ссылка;
Движение.Номенклатура = СтрокаЦены.Номенклатура;
Движение.ВидЦены = СтрокаЦены.ВидЦены;
Движение.Цена = СтрокаЦены.Цена;
Движение.Валюта = СтрокаЦены.Валюта;
КонецЦикла;
НаборДвижений.ОбменДанными.Загрузка = Истина;
НаборДвижений.Записать();
Этот способ обходит все тяжелые обработчики конфигурации, увеличивая скорость записи в базу данных до максимально возможного платформенного предела.
При передаче таблицы значений на 1 000 000 строк между клиентом и сервером система тратит огромное количество времени на сериализацию данных, а память рабочего процесса переполняется. Чтобы решить эту проблему, чтение файла должно происходить исключительно на сервере.
Вместо того чтобы читать весь файл целиком в память, мы рекомендуем читать его последовательно порциями. Для файлов формата CSV это легко реализовать с помощью объекта ЧтениеТекста:
// Передаем файл из временного хранилища на сервер и сохраняем во временный файл
ПутьКФайлу = ПолучитьИмяВременногоФайла("csv");
ДвоичныеДанныеФайла = ПолучитьИзВременногоХранилища(АдресВХранилище);
ДвоичныеДанныеФайла.Записать(ПутьКФайлу);
ЧтениеCSV = Новый ЧтениеТекста;
ЧтениеCSV.Открыть(ПутьКФайлу, КодировкаТекста.UTF8);
БуферДанных = Новый ТаблицаЗначений;
БуферДанных.Колонки.Добавить("Артикул", Новый ОписаниеТипов("Строка"));
БуферДанных.Колонки.Добавить("Цена", Новый ОписаниеТипов("Число"));
РазмерПорции = 20000;
СчетчикСтрок = 0;
СтрокаФайла = ЧтениеCSV.ПрочитатьСтроку();
Пока СтрокаФайла <> Неопределено Цикл
// Парсим строку (пример для разделителя ";")
МассивПодстрок = СтроковыеФункцииКлиентСервер.РазложитьСтрокуВМассивПодстрок(СтрокаФайла, ";");
Если МассивПодстрок.Количество() >= 2 Тогда
НоваяСтрока = БуферДанных.Добавить();
НоваяСтрока.Артикул = СокрЛП(МассивПодстрок[0]);
НоваяСтрока.Цена = Число(СокрЛП(МассивПодстрок[1]));
СчетчикСтрок = СчетчикСтрок + 1;
КонецЕсли;
// При накоплении порции обрабатываем ее и очищаем буфер
Если СчетчикСтрок >= РазмерПорции Тогда
ЗагрузитьПорциюВБазу(БуферДанных);
БуферДанных.Очистить();
СчетчикСтрок = 0;
КонецЕсли;
СтрокаФайла = ЧтениеCSV.ПрочитатьСтроку();
КонецЦикла;
// Дозаписываем остаток строк
Если БуферДанных.Количество() > 0 Тогда
ЗагрузитьПорциюВБазу(БуферДанных);
КонецЕсли;
ЧтениеCSV.Закрыть();
УдалитьФайлы(ПутьКФайлу);
В обсуждениях на форумах часто упоминается деление потоков по типам цен. Однако попытка записывать разные типы цен параллельно для одних и тех же товаров неминуемо приведет к взаимным блокировкам (deadlocks) в СУБД, так как СУБД накладывает блокировку на физическую страницу или диапазон записей таблицы регистра цен.
Если вы хотите запустить загрузку в несколько потоков через фоновые задания, разделять потоки нужно строго по диапазонам Номенклатуры (например, по уникальному идентификатору GUID, первой букве кода или по родительским группам справочника).
ЦеныНоменклатуры обычно строится с ведущим измерением Номенклатура.Если загрузка прайс-листа выполняется раз в месяц в выделенное технологическое окно, имеет смысл привлечь системного администратора СУБД (MS SQL Server или PostgreSQL).
При вставке 1 000 000 записей СУБД тратит колоссальное количество ресурсов на динамическое перестроение индексов таблицы регистра. Администратор базы данных может временно перевести некластеризованные индексы таблицы регистра цен в статус DISABLE перед началом загрузки, запустить загрузку в 1С на максимальной скорости, а после окончания вернуть индексы в исходное состояние с помощью команды REBUILD. Это позволяет сократить общее время физической записи данных СУБД в несколько раз.
Подведем итог. Чтобы оптимизировать загрузку прайс-листа объемом 1 000 000 строк, используйте следующий комплекс мер:
ЧтениеТекста) порциями, чтобы не перегружать оперативную память сервера.Проведен = Истина и флагом ОбменДанными.Загрузка = Истина, а движения пишите в регистр напрямую через набор записей.