Как ускорить получение остатков в 1С на разные даты без использования запросов в цикле?

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

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

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

Почему запрос в цикле — это антипаттерн?

Когда мы выполняем метод Запрос.Выполнить() внутри цикла, платформа 1С на каждой итерации совершает следующие ресурсоемкие действия:

  1. Передает текст запроса и параметры из клиентского сеанса или сервера приложений 1С на сервер СУБД.
  2. СУБД производит компиляцию запроса и строит план его выполнения.
  3. СУБД выполняет поиск по индексам таблиц остатков.
  4. Результат передается обратно на сервер приложений 1С, где создается новый объект выборки.

Если в вашей таблице 1000 строк, эти действия повторятся 1000 раз. В итоге большая часть времени тратится не на сам расчет остатка, а на сетевые задержки (Network Roundtrips) и накладные расходы СУБД. Наша цель — передать все параметры на сервер СУБД за один раз и получить итоговый результат одним пакетом.

---

Решение 1. Использование виртуальной таблицы "ОстаткиИОбороты" и временных таблиц

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

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

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

Рассмотрим пример реализации такого запроса:


// Предположим, у нас есть ТЗ с исходными данными: РезультатСЗ
// Помещаем её в менеджер временных таблиц

МВТ = Новый МенеджерВременныхТаблиц;
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МВТ;

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

Запрос.УстановитьПараметр("ТаблицаИсходник", РезультатСЗ);
РезультатЗапроса = Запрос.Выполнить().Выгрузить();

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

---

Решение 2. Расчет остатков методом нарастающих итогов (высокая производительность)

Если в исходной таблице много строк, а диапазон дат достаточно широк, классическое соединение "больше или равно" (ПО ВТ.Период >= Движения.Период) может вызвать перегрузку СУБД из-за квадратичного роста количества сравниваемых строк. В этом случае более эффективным является математический метод нарастающего итога.

Формула расчета проста:

Остаток на целевую дату = Начальный остаток на начало всего интервала + Сумма всех оборотов с начала интервала до целевой даты.

Разберем реализацию этого метода по шагам:

  1. В коде 1С находим минимальную (МинДата) и максимальную (МаксДата) даты из исходной таблицы.
  2. Одним быстрым запросом получаем начальный остаток на МинДата по всем требуемым товарам и складам.
  3. Этим же запросом получаем обороты с детализацией до дня или до регистратора в интервале между МинДата и МаксДата.
  4. Соединяем эти данные в запросе и рассчитываем нарастающий итог для каждой строки таблицы.

Посмотрим на пример запроса для расчета нарастающим итогом:


// Сначала определим границы периода в коде
ГраницаНачала = РезультатСЗ.Итог("Период", "Минимум");
ГраницаКонца = РезультатСЗ.Итог("Период", "Максимум");

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

---

Решение 3. Группировка дат и пакетный запрос (Гибридный метод)

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

Выясним логику этого метода:

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

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


ТаблицаДат = РезультатСЗ.Скопировать();
ТаблицаДат.Свернуть("Период");

МассивНоменклатуры = РезультатСЗ.ВыгрузитьКолонку("Номенклатура");
МассивСкладов = РезультатСЗ.ВыгрузитьКолонку("Склад");

ЗапросПакет = Новый Запрос;
ТекстЗапроса = "";

Для Индекс = 0 По ТаблицаДат.Количество() - 1 Цикл
	ИмяПараметраДаты = "Дата_" + Строка(Индекс);
	ЗапросПакет.УстановитьПараметр(ИмяПараметраДаты, ТаблицаДат[Индекс].Период);
	
	ТекстЗапроса = ТекстЗапроса + 
	"ВЫБРАТЬ
	|	&" + ИмяПараметраДаты + " КАК Период,
	|	Остатки.Номенклатура КАК Номенклатура,
	|	Остатки.Склад КАК Склад,
	|	Остатки.КоличествоОстаток КАК ОстатокСклада
	|ИЗ
	|	РегистрНакопления.ДвижениеТМЦ.Остатки(&" + ИмяПараметраДаты + ", Номенклатура В (&МассивНоменклатуры) И Склад В (&МассивСкладов)) КАК Остатки
	|";
	
	Если Индекс < ТаблицаДат.Количество() - 1 Тогда
		ТекстЗапроса = ТекстЗапроса + "ОБЪЕДИНИТЬ ВСЕ" + Символы.ПС;
	КонецЕсли;
КонецЦикла;

ЗапросПакет.Текст = ТекстЗапроса;
ЗапросПакет.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
ЗапросПакет.УстановитьПараметр("МассивСкладов", МассивСкладов);

ТаблицаОстатков = ЗапросПакет.Выполнить().Выгрузить();
// Теперь у нас есть единая таблица остатков на все уникальные даты,
// и мы можем быстро сопоставить данные с исходной таблицей.

---

Решение 4. Обработка в памяти через индексированную таблицу значений

Для файловых баз данных или высоконагруженных систем сложные SQL-запросы со связями по условиям сравнения дат могут выполняться медленнее, чем обработка в оперативной памяти сервера приложений 1С. Мы можем перенести тяжелые расчеты на сторону сервера приложений, используя индексацию таблиц значений.

Проанализируем этот сценарий по шагам:

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

Пример кода обработки данных в оперативной памяти:


// Предположим, мы получили таблицу ТЗ_Движения, содержащую колонки:
// Номенклатура, Склад, ПериодДвижения, НачальныйОстаток, Оборот

ТЗ_Движения.Индексы.Добавить("Номенклатура, Склад");

Для Каждого СтрокаИсходная Из РезультатСЗ Цикл
	ПараметрыОтбора = Новый Структура("Номенклатура, Склад", СтрокаИсходная.Номенклатура, СтрокаИсходная.Склад);
	НайденныеСтроки = ТЗ_Движения.НайтиСтроки(ПараметрыОтбора);
	
	ТекущийОстаток = 0;
	ПервыйПроход = Истина;
	
	Для Каждого СтрокаДвижения Из НайденныеСтроки Цикл
		Если СтрокаДвижения.ПериодДвижения <= СтрокаИсходная.Период Тогда
			Если ПервыйПроход Тогда
				ТекущийОстаток = СтрокаДвижения.НачальныйОстаток;
				ПервыйПроход = Ложь;
			КонецЕсли;
			ТекущийОстаток = ТекущийОстаток + СтрокаДвижения.Оборот;
		КонецЕсли;
	КонецЦикла;
	
	СтрокаИсходная.ОстатокСклада = ТекущийОстаток;
КонецЦикла;

---

Решение 5. Использование связей наборов данных в СКД

Если ваша задача — вывести эти данные в отчет или в форму, идеальным и самым простым решением будет использование Системы компоновки данных (СКД). Платформа 1С содержит встроенный оптимизатор связей наборов данных.

Разберем, как это настроить в СКД:

  1. Создаем Набор данных - запрос 1, который выбирает наши исходные строки (аналог вашей таблицы РезультатСЗ). В нем будут поля: Номенклатура, Склад, Период.
  2. Создаем Набор данных - запрос 2, который обращается к виртуальной таблице остатков с параметрами:
    
    ВЫБРАТЬ
    	Остатки.КоличествоОстаток КАК ОстатокСклада
    ИЗ
    	РегистрНакопления.ДвижениеТМЦ.Остатки(&ПериодГраница, Номенклатура = &НоменклатураПараметр И Склад = &СкладПараметр) КАК Остатки
    
  3. Переходим на вкладку Связи наборов данных и связываем Набор 1 (источник) с Набором 2 (приемник) по полям Номенклатура и Склад.
  4. В колонке Параметры связи указываем, что поле Период из Набора 1 должно передаваться в параметр &ПериодГраница Набора 2.

Почему СКД работает быстро? Платформа 1С на уровне СУБД автоматически группирует передаваемые параметры. Вместо выполнения отдельного SQL-запроса на каждую строку, СКД формирует пакетные запросы (обычно передавая по 1000 строк в одном пакете), минимизируя нагрузку на сервер СУБД.

---

Резюме: какую методику выбрать?

Каждый из рассмотренных методов оптимален для своих условий:

← На главную