Как перенести миллионы записей цен в 1С без ошибки «Недостаточно памяти»?

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

Объединение нескольких информационных баз 1С в одну — стандартная задача для крупных холдингов и развивающихся компаний. Однако при переносе исторических данных или актуальных срезов на определенную дату разработчики часто сталкиваются с аппаратными ограничениями платформы. Срез цен на несколько миллионов позиций номенклатуры при стандартном подходе гарантированно вызывает ошибку «Недостаточно памяти» (Out of Memory), останавливая весь процесс миграции данных.

В этой статье мы подробно разберем, почему возникает эта ошибка при использовании конфигурации «1С:Конвертация данных 2.1» (КД 2.1), как правильно настроить кэширование объектов и как оптимизировать выгрузку с помощью разделения гигантских пакетов данных на небольшие порции.

Выясняем причину: почему падает 1С при переносе больших объемов данных?

Для понимания сути проблемы проанализируем ситуацию. При выгрузке среза цен объемом в 5 000 000 записей через штатную обработку выгрузки КД 2.1 процесс останавливается через 1–2 часа, успев обработать лишь малую часть данных (например, 100 000 объектов), а размер файла выгрузки при этом раздувается до нескольких гигабайт. В итоге платформа завершает работу аварийно.

Этому способствуют два ключевых фактора:

  1. Ограничения разрядности платформы (32-bit): Если выгрузка запускается в 32-битной версии клиента или сервера 1С, то один процесс физически не может использовать более 2 ГБ оперативной памяти (в редких случаях до 3–4 ГБ при использовании патчей и флага LAA — Large Address Aware). Как только этот лимит превышается, операционная система принудительно закрывает приложение 1С.
  2. Кэширование выгруженных объектов в КД 2.1: По умолчанию механизмы конвертации данных сохраняют в оперативной памяти информацию обо всех выгруженных объектах. Это необходимо платформе для сопоставления ссылок (УИД) при повторных обращениях. Если вы переносите миллионы записей регистра сведений, у которых нет уникального идентификатора ссылки (в отличие от справочников или документов), КД все равно пытается держать их структуры в ОЗУ, что лавинообразно расходует доступную память.

Рассмотрим подробнее три основных способа решения этой проблемы, начиная от настройки правил конвертации и заканчивая написанием оптимизированного алгоритма выгрузки в ПВД.

Решение 1. Оптимизация кэширования и выгрузки по ссылкам в КД 2.1

Прежде чем приступать к написанию сложного программного кода, необходимо правильно настроить параметры самих правил конвертации объектов (ПКО). Неверно выставленные флаги могут увеличить размер файла выгрузки в несколько раз.

Шаг 1. Использование флага «Не запоминать выгруженные объекты»

В настройках ПКО для записей регистра сведений (или документов установки цен) крайне важно установить флаг «Не запоминать выгруженные объекты». Это указывает платформе на то, что после записи очередного узла данных в XML-файл информацию о нем можно сразу же удалить из оперативной памяти. Это высвобождает ОЗУ в процессе работы и предотвращает падение системы.

Шаг 2. Двухэтапная стратегия переноса (Справочники отдельно, регистры отдельно)

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

Правильный подход выглядит следующим образом:

Решение 2. Разбиение выгрузки на пакеты документов в ПВД (с примером кода)

Если выгружать 5 миллионов записей цен одним единым документом (или одной транзакцией регистра сведений), то база-приемник просто «задохнется» при попытке провести такой документ. Оптимальное решение — разбивать выгружаемые данные на порции (пакеты) документов объемом от 50 000 до 100 000 строк в каждом.

Разберем рабочий алгоритм, который пишется в обработчике ПВД (Правила выгрузки данных) «Перед обработкой». Наш скрипт будет собирать срез последних цен, группировать их по видам цен, подразделениям и контрагентам, а затем порционно формировать входящие данные для создания документов установки цен в приемнике.

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


НаДату = ДатаОкончания;
Запрос = Новый Запрос;
ВалютаРегл = Константы.ВалютаРегламентированногоУчетаОрганизаций.Получить;

Запрос.УстановитьПараметр("ДатаСведений", КонецДня(НаДату));
Запрос.УстановитьПараметр("ПодразделениеКомпании", Справочники.ПодразделенияКомпании.НайтиПоКоду("111"));

Запрос.Текст = 
"ВЫБРАТЬ
|	ЦеныСрезПоследних.ТипЦен КАК ТипЦен,
|	ЦеныСрезПоследних.Номенклатура КАК Номенклатура,
|	ЦеныСрезПоследних.ХарактеристикаНоменклатуры КАК ХарактеристикаНоменклатуры,
|	ЦеныСрезПоследних.ЕдиницаИзмерения КАК ЕдиницаИзмерения,
|	ЦеныСрезПоследних.ПодразделениеКомпании КАК ПодразделениеКомпанииПолучатель,
|	ЦеныСрезПоследних.Контрагент КАК Контрагент,
|	ЦеныСрезПоследних.Цена КАК Цена
|ИЗ
|	РегистрСведений.Цены.СрезПоследних(&ДатаСведений, ПодразделениеКомпании = &ПодразделениеКомпании) КАК ЦеныСрезПоследних
|ГДЕ
|	ЦеныСрезПоследних.Цена > 0
|ИТОГИ ПО
|	ТипЦен,
|	ПодразделениеКомпанииПолучатель,
|	Контрагент";

ТаблицаДанных = Новый ТаблицаЗначений;
ТаблицаДанных.Колонки.Добавить("Номенклатура");
ТаблицаДанных.Колонки.Добавить("ХарактеристикаНоменклатуры");
ТаблицаДанных.Колонки.Добавить("ЕдиницаИзмерения");
ТаблицаДанных.Колонки.Добавить("БазоваяЕдиницаИзмерения");
ТаблицаДанных.Колонки.Добавить("Коэффициент");
ТаблицаДанных.Колонки.Добавить("ПроцентНаценки");
ТаблицаДанных.Колонки.Добавить("СуммаНаценки");
ТаблицаДанных.Колонки.Добавить("Цена");
ТаблицаДанных.Колонки.Добавить("ЦенаБазовая");

ПодразделениеКомпании = ПараметрыСеанса.ПодразделениеКомпании;
Организация = ПараметрыСеанса.Организация;
Автор = ПараметрыСеанса.Пользователь;

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

Проанализируем преимущества представленного кода:

Решение 3. Альтернативные варианты сверхбыстрого переноса данных

Если проектные сроки поджимают или производительности формата XML в рамках «Конвертации данных 2.1» все равно недостаточно, архитекторы и ведущие разработчики используют альтернативные технологии интеграции:

Вариант А. Прямое подключение через COM-соединение (V83.COMConnector)

Вместо промежуточной выгрузки в файл пишется внешняя обработка, которая запускается в базе-источнике, подключается к приемнику через V83.COMConnector и напрямую записывает документы установки цен порциями. Это полностью исключает этап разбора XML, сокращая накладные расходы на сериализацию данных и экономя гигабайты ОЗУ на обеих сторонах.

Вариант Б. Перенос через формат JSON

Если вы работаете на современных версиях платформы 1С (начиная с 8.3), то использование встроенных механизмов ЗаписьJSON и ЧтениеJSON будет гораздо эффективнее XML. JSON-сериализатор в 1С реализован на низком уровне С++ и работает в 3–5 раз быстрее стандартного парсера XML, а потребление памяти сводится к минимуму за счет потоковой записи на диск.

Вариант В. Прямой перенос на уровне СУБД (SQL)

Этот способ требует высокой квалификации и глубокого понимания структуры таблиц базы данных 1С. Схема состоит из следующих этапов:

  1. Справочник номенклатуры переносится стандартным образом через КД (для корректной синхронизации GUID-идентификаторов).
  2. С помощью прямого SQL-запроса (например, через Linked Server или утилиту bcp в MS SQL) данные о ценах копируются непосредственно из таблицы регистра источника (например, _InfoRgXXXX) в таблицу регистра приемника.
  3. В базе-приемнике программно запускается процедура пересчета итогов регистров сведений. Этот метод позволяет перенести 5 000 000 цен за считанные минуты, полностью минуя слой бизнес-логики 1С на этапе записи.

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

← На главную