Как оптимизировать код выгрузки и свертки документов в 1С?

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

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

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

Анализ проблем исходного алгоритма

Посмотрим на исходный вариант решения: разработчик выполняет запрос к табличной части Документ.РеализацияТоваровУслуг.Товары с группировкой по множеству полей шапки и строки, после чего выгружает результат одновременно в объект Выборка и таблицу значений ТЗ, организуя сложный ручной цикл со сменой складов и заполнением строк.

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

  1. Логическая ошибка во вложенном цикле выборки. Метод СледующийПоЗначениюПоля("Контрагент") смещает указатель, но вложенный цикл Пока Выборка.Следующий() Цикл в линейной выборке (без предложения ИТОГИ) обходит строки подряд до самого конца всей выборки, не останавливаясь на смене контрагента. В результате первая итерация внешнего цикла прочтет абсолютно все записи, а последующие итерации по контрагентам просто не состоятся.
  2. Рассинхронизация индекса строк. Перед запуском вложенного цикла счетчик ПозТЗ указывает на нулевой элемент, который копируется в шапку документа. Далее в первой же итерации цикла по строкам берется тот же самый индекс ПозТЗ, что приводит к повторному включению данных первой строки.
  3. Неявные соединения (LEFT JOIN) через точку в запросе. Конструкции вида РеализацияТоваровУслугТовары.Ссылка.Контрагент или РеализацияТоваровУслугТовары.Ссылка.Склад в полях выборки и группировки заставляют СУБД создавать множественные левые соединения физической таблицы строк документа с таблицей шапки, что существенно замедляет выполнение запроса.
  4. Неэффективная фильтрация по дате. Условие ГДЕ РеализацияТоваровУслугТовары.Ссылка.Дата МЕЖДУ ... накладывается на ссылку из табличной части, лишая СУБД возможности быстро использовать стандартный кластерный индекс документов по периоду.
  5. Неоправданный расход оперативной памяти. Одновременный вызов методов РезультатЗапроса.Выгрузить() и РезультатЗапроса.Выбрать() дублирует массив информации в памяти. Кроме того, создание колонок со вложенными таблицами значений (НоваяСтрока.Товары = ТЧ.Скопировать()) при больших объемах переносимых документов приводит к переполнению оперативной памяти рабочего процесса 1С.
  6. Бухгалтерские риски при свертке. В запросе присутствуют группировки по договорам и валютам, но разделение документов в коде выполняется только по контрагенту и складу. Если у контрагента было несколько отгрузок по разным договорам, данные табличной части соберутся в один документ, а договор шапки заполнится случайным образом из первой попавшейся строки, что приведет к перекосу сальдо на счете 62 в конфигурации-приемнике.

Оптимизация запроса: временная таблица и секция ИТОГИ ПО

Наиболее чистое и производительное решение заключается в переносе логики группировки документов на сторону СУБД с помощью конструкции ИТОГИ ПО. Это позволяет получить готовую двухуровневую иерархию (шапка документа и его табличная часть) без необходимости вручную синхронизировать индексы и отслеживать смену складов.

Разберем по шагам построение оптимального запроса:

  1. Сначала отберем ссылки проведенных документов за период во временную таблицу по индексированным полям шапки.
  2. Свяжем временную таблицу с табличной частью документа через ВНУТРЕННЕЕ СОЕДИНЕНИЕ.
  3. Зададим иерархию итогов по всем полям, формирующим уникальную шапку документа (Организация, Контрагент, Договор, Склад, Валюта, параметры учета НДС).

Посмотрим на пример оптимизированного запроса:


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

Запрос.УстановитьПараметр("ДатаНачала", ДатаНачала);
Запрос.УстановитьПараметр("ДатаОкончания", ДатаОкончания);

РезультатЗапроса = Запрос.Выполнить();

Иерархический обход выборки с итогами

После применения конструкции ИТОГИ ПО отпадает необходимость вручную сравнивать значения реквизитов и использовать вспомогательные таблицы. Мы организуем обход верхнего уровня по группировкам, а затем вложенной выборкой получаем детализирующие строки текущего свернутого документа.

Рассмотрим, как выглядит обход результата:


// Создаем структуру результирующей таблицы
ТЗРез = Новый ТаблицаЗначений;
// Формируем колонки шапки
ТЗРез.Колонки.Добавить("Организация");
ТЗРез.Колонки.Добавить("Контрагент");
ТЗРез.Колонки.Добавить("ДоговорКонтрагента");
ТЗРез.Колонки.Добавить("Склад");
ТЗРез.Колонки.Добавить("ВалютаДокумента");
ТЗРез.Колонки.Добавить("УчитыватьНДС");
ТЗРез.Колонки.Добавить("СуммаВключаетНДС");
ТЗРез.Колонки.Добавить("Товары"); // Вложенная ТЗ для строк

// Задаем шаблон колонок для табличной части
ТЧШаблон = Новый ТаблицаЗначений;
ТЧШаблон.Колонки.Добавить("Номенклатура");
ТЧШаблон.Колонки.Добавить("ЕдиницаИзмерения");
ТЧШаблон.Колонки.Добавить("Количество");
ТЧШаблон.Колонки.Добавить("Цена");
ТЧШаблон.Колонки.Добавить("Сумма");
ТЧШаблон.Колонки.Добавить("СтавкаНДС");
ТЧШаблон.Колонки.Добавить("СуммаНДС");

// Выбираем только группировочные записи верхнего уровня
ВыборкаШапка = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);

Пока ВыборкаШапка.Следующий() Цикл
	
	СтрокаДокумента = ТЗРез.Добавить();
	ЗаполнитьЗначенияСвойств(СтрокаДокумента, ВыборкаШапка);
	
	// Создаем персональную таблицу строк для текущего документа
	ТЧДокумента = ТЧШаблон.СкопироватьКолонки();
	
	// Выбираем детальные строки, относящиеся исключительно к текущей группе
	ВыборкаСтроки = ВыборкаШапка.Выбрать();
	Пока ВыборкаСтроки.Следующий() Цикл
		НоваяСтрокаТЧ = ТЧДокумента.Добавить();
		ЗаполнитьЗначенияСвойств(НоваяСтрокаТЧ, ВыборкаСтроки);
	КонецЦикла;
	
	СтрокаДокумента.Товары = ТЧДокумента;
	
КонецЦикла;

ВыборкаДанных = ТЗРез;

Потоковая выгрузка без накопления в памяти

Если документов за период много, сохранение всех вложенных таблиц значений в общей таблице ВыборкаДанных способно вызвать ошибку нехватки памяти (Out of memory). Для конвертации данных эффективнее применить потоковый метод: отказаться от промежуточной таблицы ТЗРез и производить выгрузку каждого документа по отдельности непосредственно в цикле.

Для этого в обработчике «Перед обработкой правила выгрузки» (ПВД) устанавливают признак Отказ = Истина; и используют системную процедуру ВыгрузитьПоПравилу():


Отказ = Истина; // Отменяем стандартный сбор данных в память

ВыборкаШапка = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);

Пока ВыборкаШапка.Следующий() Цикл
	
	// Создаем структуру со свойствами документа для конвертации
	СтруктураДокумента = Новый Структура;
	СтруктураДокумента.Вставить("Организация", ВыборкаШапка.Организация);
	СтруктураДокумента.Вставить("Контрагент", ВыборкаШапка.Контрагент);
	СтруктураДокумента.Вставить("ДоговорКонтрагента", ВыборкаШапка.ДоговорКонтрагента);
	СтруктураДокумента.Вставить("Склад", ВыборкаШапка.Склад);
	
	ТаблицаТовары = ТЧШаблон.СкопироватьКолонки();
	
	ВыборкаСтроки = ВыборкаШапка.Выбрать();
	Пока ВыборкаСтроки.Следующий() Цикл
		НоваяСтрока = ТаблицаТовары.Добавить();
		ЗаполнитьЗначенияСвойств(НоваяСтрока, ВыборкаСтроки);
	КонецЦикла;
	
	СтруктураДокумента.Вставить("Товары", ТаблицаТовары);
	
	// Немедленно сериализуем один документ в поток выгрузки
	ВыгрузитьПоПравилу(СтруктураДокумента, , , , "РеализацияТоваровУслуг");
	
КонецЦикла;

Такой подход гарантирует стабильную работу переноса при любых объемах данных, устраняет дублирование информации в оперативной памяти и формирует корректные учетные документы в конфигурации-приемнике.

← На главную