Объединение нескольких информационных баз 1С в одну — стандартная задача для крупных холдингов и развивающихся компаний. Однако при переносе исторических данных или актуальных срезов на определенную дату разработчики часто сталкиваются с аппаратными ограничениями платформы. Срез цен на несколько миллионов позиций номенклатуры при стандартном подходе гарантированно вызывает ошибку «Недостаточно памяти» (Out of Memory), останавливая весь процесс миграции данных.
В этой статье мы подробно разберем, почему возникает эта ошибка при использовании конфигурации «1С:Конвертация данных 2.1» (КД 2.1), как правильно настроить кэширование объектов и как оптимизировать выгрузку с помощью разделения гигантских пакетов данных на небольшие порции.
Для понимания сути проблемы проанализируем ситуацию. При выгрузке среза цен объемом в 5 000 000 записей через штатную обработку выгрузки КД 2.1 процесс останавливается через 1–2 часа, успев обработать лишь малую часть данных (например, 100 000 объектов), а размер файла выгрузки при этом раздувается до нескольких гигабайт. В итоге платформа завершает работу аварийно.
Этому способствуют два ключевых фактора:
Рассмотрим подробнее три основных способа решения этой проблемы, начиная от настройки правил конвертации и заканчивая написанием оптимизированного алгоритма выгрузки в ПВД.
Прежде чем приступать к написанию сложного программного кода, необходимо правильно настроить параметры самих правил конвертации объектов (ПКО). Неверно выставленные флаги могут увеличить размер файла выгрузки в несколько раз.
Шаг 1. Использование флага «Не запоминать выгруженные объекты»
В настройках ПКО для записей регистра сведений (или документов установки цен) крайне важно установить флаг «Не запоминать выгруженные объекты». Это указывает платформе на то, что после записи очередного узла данных в XML-файл информацию о нем можно сразу же удалить из оперативной памяти. Это высвобождает ОЗУ в процессе работы и предотвращает падение системы.
Шаг 2. Двухэтапная стратегия переноса (Справочники отдельно, регистры отдельно)
Частая ошибка разработчиков — попытка выгрузить цены вместе с полной информацией о номенклатуре за один проход. При этом в XML-файл для каждого товара выгружается вся его структура: наименование, реквизиты, свойства, производители и т.д. Если у вас 1 миллион товаров, файл мгновенно вырастет до неприличных размеров.
Правильный подход выглядит следующим образом:
СправочникСсылка.Номенклатура, СправочникСсылка.Контрагенты, СправочникСсылка.ВидыЦен) без цен. Это позволит создать все объекты в базе-приемнике и сопоставить их GUID.Если выгружать 5 миллионов записей цен одним единым документом (или одной транзакцией регистра сведений), то база-приемник просто «задохнется» при попытке провести такой документ. Оптимальное решение — разбивать выгружаемые данные на порции (пакеты) документов объемом от 50 000 до 100 000 строк в каждом.
Разберем рабочий алгоритм, который пишется в обработчике ПВД (Правила выгрузки данных) «Перед обработкой». Наш скрипт будет собирать срез последних цен, группировать их по видам цен, подразделениям и контрагентам, а затем порционно формировать входящие данные для создания документов установки цен в приемнике.
Посмотрим на пример реализации данного алгоритма:
НаДату = ДатаОкончания;
Запрос = Новый Запрос;
ВалютаРегл = Константы.ВалютаРегламентированногоУчетаОрганизаций.Получить;
Запрос.УстановитьПараметр("ДатаСведений", КонецДня(НаДату));
Запрос.УстановитьПараметр("ПодразделениеКомпании", Справочники.ПодразделенияКомпании.НайтиПоКоду("111"));
Запрос.Текст =
"ВЫБРАТЬ
| ЦеныСрезПоследних.ТипЦен КАК ТипЦен,
| ЦеныСрезПоследних.Номенклатура КАК Номенклатура,
| ЦеныСрезПоследних.ХарактеристикаНоменклатуры КАК ХарактеристикаНоменклатуры,
| ЦеныСрезПоследних.ЕдиницаИзмерения КАК ЕдиницаИзмерения,
| ЦеныСрезПоследних.ПодразделениеКомпании КАК ПодразделениеКомпанииПолучатель,
| ЦеныСрезПоследних.Контрагент КАК Контрагент,
| ЦеныСрезПоследних.Цена КАК Цена
|ИЗ
| РегистрСведений.Цены.СрезПоследних(&ДатаСведений, ПодразделениеКомпании = &ПодразделениеКомпании) КАК ЦеныСрезПоследних
|ГДЕ
| ЦеныСрезПоследних.Цена > 0
|ИТОГИ ПО
| ТипЦен,
| ПодразделениеКомпанииПолучатель,
| Контрагент";
ТаблицаДанных = Новый ТаблицаЗначений;
ТаблицаДанных.Колонки.Добавить("Номенклатура");
ТаблицаДанных.Колонки.Добавить("ХарактеристикаНоменклатуры");
ТаблицаДанных.Колонки.Добавить("ЕдиницаИзмерения");
ТаблицаДанных.Колонки.Добавить("БазоваяЕдиницаИзмерения");
ТаблицаДанных.Колонки.Добавить("Коэффициент");
ТаблицаДанных.Колонки.Добавить("ПроцентНаценки");
ТаблицаДанных.Колонки.Добавить("СуммаНаценки");
ТаблицаДанных.Колонки.Добавить("Цена");
ТаблицаДанных.Колонки.Добавить("ЦенаБазовая");
ПодразделениеКомпании = ПараметрыСеанса.ПодразделениеКомпании;
Организация = ПараметрыСеанса.Организация;
Автор = ПараметрыСеанса.Пользователь;
ВыборкаТипЦен = Запрос.Выполнить.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаТипЦен.Следующий() Цикл
ТипЦен = ВыборкаТипЦен.ТипЦен;
ВыборкаПодразделениеКомпанииПолучатель = ВыборкаТипЦен.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаПодразделениеКомпанииПолучатель.Следующий() Цикл
ПодразделениеКомпанииПолучатель = ВыборкаПодразделениеКомпанииПолучатель.ПодразделениеКомпанииПолучатель;
ВыборкаКонтрагент = ВыборкаПодразделениеКомпанииПолучатель.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаКонтрагент.Следующий() Цикл
Контрагент = ВыборкаКонтрагент.Контрагент;
ТаблицаДанных.Очистить();
НомерЗаписи = 1;
Выборка = ВыборкаКонтрагент.Выбрать(ОбходРезультатаЗапроса.Прямой);
Пока Выборка.Следующий() Цикл
НоваяСтрока = ТаблицаДанных.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, Выборка);
// Заполняем базовую единицу измерения, если она пустая
Если (НЕ обЗначениеНеЗаполнено(НоваяСтрока.Номенклатура))
И (НЕ обЗначениеНеЗаполнено(НоваяСтрока.Номенклатура.ОсновнаяЕдиницаИзмерения))
И (обЗначениеНеЗаполнено(НоваяСтрока.ЕдиницаИзмерения)) Тогда
НоваяСтрока.ЕдиницаИзмерения = НоваяСтрока.Номенклатура.ОсновнаяЕдиницаИзмерения;
КонецЕсли;
Если НЕ обЗначениеНеЗаполнено(НоваяСтрока.ЕдиницаИзмерения) Тогда
НоваяСтрока.Коэффициент = НоваяСтрока.ЕдиницаИзмерения.Коэффициент;
КонецЕсли;
НомерЗаписи = НомерЗаписи + 1;
// Если превышен лимит строк в документе, выгружаем порцию
Если (НомерЗаписи > Параметры.КоличествоСтрокТабличнойЧастиДляПереноса) Тогда
НомерЗаписи = 1;
Если ТаблицаДанных.Количество() <> 0 Тогда
ВходящиеДанные = Новый Структура("Дата, Автор, БазовоеПодразделение, БазовыйТипЦен, ВалютаДокумента, ДатаНачалаДействия, Комментарий, Контрагент, КурсДокумента, ОкруглятьДо, Организация, ПодразделениеКомпании, ПодразделениеКомпанииПолучатель, Поставщик, ПроцентНаценки, ПроцентНаценкиИзНоменклатуры, РасчетЦенОт, СкладКомпании, СуммаДокумента, ТипЦен, ХозОперация, Товары");
ВходящиеДанные.Дата = НаДату;
ВходящиеДанные.Автор = Автор;
ВходящиеДанные.БазовыйТипЦен = ТипЦен;
ВходящиеДанные.ДатаНачалаДействия = НаДату;
ВходящиеДанные.ВалютаДокумента = ВалютаРегл;
ВходящиеДанные.Контрагент = Контрагент;
ВходящиеДанные.КурсДокумента = 1;
ВходящиеДанные.ОкруглятьДо = 0.01;
ВходящиеДанные.Организация = Организация;
ВходящиеДанные.ПодразделениеКомпании = ПодразделениеКомпании;
ВходящиеДанные.ПодразделениеКомпанииПолучатель = ПодразделениеКомпанииПолучатель;
ВходящиеДанные.Поставщик = Неопределено;
ВходящиеДанные.ПроцентНаценки = 0;
ВходящиеДанные.ПроцентНаценкиИзНоменклатуры = Ложь;
ВходящиеДанные.РасчетЦенОт = 0;
ВходящиеДанные.СкладКомпании = Неопределено;
ВходящиеДанные.СуммаДокумента = ТаблицаДанных.Итог("Цена");
ВходящиеДанные.ТипЦен = ТипЦен;
Если обЗначениеНеЗаполнено(Контрагент) Тогда
ВходящиеДанные.ХозОперация = Справочники.ХозОперации.УстановкаЦенКомпании;
Else
ВходящиеДанные.ХозОперация = Справочники.ХозОперации.УстановкаЦенКонтрагента;
КонецЕсли;
ВходящиеДанные.Комментарий = "Перенос цен (пакетный режим)";
ВходящиеДанные.Товары = ТаблицаДанных;
// Выгружаем накопленную таблицу по правилу конвертации документа
ВыгрузитьПоПравилу(,, ВходящиеДанные,, "ИзменениеЦенСрезПоследних");
ТаблицаДанных.Очистить();
КонецЕсли;
КонецЕсли;
КонецЦикла;
// Выгружаем остаток записей, которые не вошли в последний полный пакет
Если ТаблицаДанных.Количество() <> 0 Тогда
ВходящиеДанные = Новый Структура("Дата, Автор, БазовоеПодразделение, БазовыйТипЦен, ВалютаДокумента, ДатаНачалаДействия, Комментарий, Контрагент, КурсДокумента, ОкруглятьДо, Организация, ПодразделениеКомпании, ПодразделениеКомпанииПолучатель, Поставщик, ПроцентНаценки, ПроцентНаценкиИзНоменклатуры, РасчетЦенОт, СкладКомпании, СуммаДокумента, ТипЦен, ХозОперация, Товары");
ВходящиеДанные.Дата = НаДату;
ВходящиеДанные.Автор = Автор;
ВходящиеДанные.БазовыйТипЦен = ТипЦен;
ВходящиеДанные.ДатаНачалаДействия = НаДату;
ВходящиеДанные.ВалютаДокумента = ВалютаРегл;
ВходящиеДанные.Контрагент = Контрагент;
ВходящиеДанные.КурсДокумента = 1;
ВходящиеДанные.ОкруглятьДо = 0.01;
ВходящиеДанные.Организация = Организация;
ВходящиеДанные.ПодразделениеКомпании = ПодразделениеКомпании;
ВходящиеДанные.ПодразделениеКомпанииПолучатель = ПодразделениеКомпанииПолучатель;
ВходящиеДанные.Поставщик = Неопределено;
ВходящиеДанные.ПроцентНаценки = 0;
ВходящиеДанные.ПроцентНаценкиИзНоменклатуры = Ложь;
ВходящиеДанные.РасчетЦенОт = 0;
ВходящиеДанные.СкладКомпании = Неопределено;
ВходящиеДанные.СуммаДокумента = ТаблицаДанных.Итог("Цена");
ВходящиеДанные.ТипЦен = ТипЦен;
Если обЗначениеНеЗаполнено(Контрагент) Тогда
ВходящиеДанные.ХозОперация = Справочники.ХозОперации.УстановкаЦенКомпании;
Else
ВходящиеДанные.ХозОперация = Справочники.ХозОперации.УстановкаЦенКонтрагента;
КонецЕсли;
ВходящиеДанные.Комментарий = "Перенос цен (финальный пакет)";
ВходящиеДанные.Товары = ТаблицаДанных;
ВыгрузитьПоПравилу(,, ВходящиеДанные,, "ИзменениеЦенСрезПоследних");
ТаблицаДанных.Очистить();
КонецЕсли;
КонецЦикла;
КонецЦикла;
КонецЦикла;
Проанализируем преимущества представленного кода:
Параметры.КоличествоСтрокТабличнойЧастиДляПереноса (рекомендуется выставлять значение от 50 000 до 100 000 строк).ТаблицаДанных.Очистить() после каждого вызова ВыгрузитьПоПравилу() своевременно уничтожает большие массивы данных во временных переменных, предотвращая переполнение оперативной памяти.Если проектные сроки поджимают или производительности формата XML в рамках «Конвертации данных 2.1» все равно недостаточно, архитекторы и ведущие разработчики используют альтернативные технологии интеграции:
Вариант А. Прямое подключение через COM-соединение (V83.COMConnector)
Вместо промежуточной выгрузки в файл пишется внешняя обработка, которая запускается в базе-источнике, подключается к приемнику через V83.COMConnector и напрямую записывает документы установки цен порциями. Это полностью исключает этап разбора XML, сокращая накладные расходы на сериализацию данных и экономя гигабайты ОЗУ на обеих сторонах.
Вариант Б. Перенос через формат JSON
Если вы работаете на современных версиях платформы 1С (начиная с 8.3), то использование встроенных механизмов ЗаписьJSON и ЧтениеJSON будет гораздо эффективнее XML. JSON-сериализатор в 1С реализован на низком уровне С++ и работает в 3–5 раз быстрее стандартного парсера XML, а потребление памяти сводится к минимуму за счет потоковой записи на диск.
Вариант В. Прямой перенос на уровне СУБД (SQL)
Этот способ требует высокой квалификации и глубокого понимания структуры таблиц базы данных 1С. Схема состоит из следующих этапов:
bcp в MS SQL) данные о ценах копируются непосредственно из таблицы регистра источника (например, _InfoRgXXXX) в таблицу регистра приемника.Подводя итог, отметим: перенос больших массивов данных требует от разработчика понимания внутренних механизмов управления памятью платформы 1С. Комбинируя настройки кэширования в правилах конвертации и порционную выгрузку документов в ПВД, можно успешно решать задачи миграции объемом в миллионы записей даже на стандартном аппаратном обеспечении.