Разработчики в 1С часто сталкиваются с задачей, когда для некоторого набора данных (например, строк в таблице значений) необходимо получить остатки товаров или взаиморасчетов. Ситуация усложняется, когда у каждой строки таблицы свой собственный склад, своя номенклатура и, главное, свой индивидуальный период (дата), на который этот остаток нужно рассчитать.
Самое очевидное, но крайне неэффективное решение — запустить цикл по строкам таблицы и в каждой итерации вызывать запрос к виртуальной таблице остатков регистра накопления. Разберем по шагам, почему этот подход работает медленно, и рассмотрим профессиональные альтернативные архитектурные решения, позволяющие получить остатки за одно обращение к базе данных.
Когда мы выполняем метод Запрос.Выполнить() внутри цикла, платформа 1С на каждой итерации совершает следующие ресурсоемкие действия:
Если в вашей таблице 1000 строк, эти действия повторятся 1000 раз. В итоге большая часть времени тратится не на сам расчет остатка, а на сетевые задержки (Network Roundtrips) и накладные расходы СУБД. Наша цель — передать все параметры на сервер СУБД за один раз и получить итоговый результат одним пакетом.
---
Этот способ основан на идее сопоставления целевых дат из нашей таблицы с реальными движениями в регистре. Вместо прямого обращения к таблице остатков на конкретную дату, мы получаем срез последних движений регистра, предшествующих нашей целевой дате.
Проанализируем алгоритм реализации этого подхода в одном запросе:
ВТ_ИсходныеДанные) и индексируем ключевые поля.ОстаткиИОбороты по нужным аналитикам. При этом накладываем условие, что период движения регистра должен быть меньше или равен целевому периоду из нашей строки.ВТ_СрезПоследних.ВТ_СрезПоследних обратно с таблицей ОстаткиИОбороты, чтобы получить конечный остаток на этот найденный максимальный период движения.Рассмотрим пример реализации такого запроса:
// Предположим, у нас есть ТЗ с исходными данными: РезультатСЗ
// Помещаем её в менеджер временных таблиц
МВТ = Новый МенеджерВременныхТаблиц;
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МВТ;
Запрос.Текст =
"ВЫБРАТЬ
| Исходник.Номенклатура КАК Номенклатура,
| Исходник.Склад КАК Склад,
| Исходник.Период КАК Период
|ПОМЕСТИТЬ ВТ_ИсходныеДанные
|ИЗ
| &ТаблицаИсходник КАК Исходник
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ.Номенклатура КАК Номенклатура,
| ВТ.Склад КАК Склад,
| ВТ.Период КАК Период,
| МАКСИМУМ(Движения.Период) КАК ПериодОстатка
|ПОМЕСТИТЬ ВТ_БлижайшиеДаты
|ИЗ
| ВТ_ИсходныеДанные КАК ВТ
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ДвижениеТМЦ.ОстаткиИОбороты(, , Запись, , ) КАК Движения
| ПО ВТ.Номенклатура = Движения.Номенклатура
| И ВТ.Склад = Движения.Склад
| И ВТ.Период >= Движения.Период
|ГРУППИРОВАТЬ ПО
| ВТ.Номенклатура,
| ВТ.Склад,
| ВТ.Период
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_Даты.Номенклатура КАК Номенклатура,
| ВТ_Даты.Склад КАК Склад,
| ВТ_Даты.Период КАК Период,
| ЕСТЬNULL(Движения.КоличествоКонечныйОстаток, 0) КАК ОстатокСклада
|ИЗ
| ВТ_БлижайшиеДаты КАК ВТ_Даты
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ДвижениеТМЦ.ОстаткиИОбороты(, , Запись, , ) КАК Движения
| ПО ВТ_Даты.Номенклатура = Движения.Номенклатура
| И ВТ_Даты.Склад = Движения.Склад
| И ВТ_Даты.ПериодОстатка = Движения.Период";
Запрос.УстановитьПараметр("ТаблицаИсходник", РезультатСЗ);
РезультатЗапроса = Запрос.Выполнить().Выгрузить();
Важное примечание: Если в одну и ту же секунду (или в рамках одного регистратора) произошло несколько движений по регистру, простого сравнения по дате Период может быть недостаточно. Для точного позиционирования необходимо использовать сравнение по моменту времени регистратора.
---
Если в исходной таблице много строк, а диапазон дат достаточно широк, классическое соединение "больше или равно" (ПО ВТ.Период >= Движения.Период) может вызвать перегрузку СУБД из-за квадратичного роста количества сравниваемых строк. В этом случае более эффективным является математический метод нарастающего итога.
Формула расчета проста:
Остаток на целевую дату = Начальный остаток на начало всего интервала + Сумма всех оборотов с начала интервала до целевой даты.
Разберем реализацию этого метода по шагам:
МинДата) и максимальную (МаксДата) даты из исходной таблицы.МинДата по всем требуемым товарам и складам.МинДата и МаксДата.Посмотрим на пример запроса для расчета нарастающим итогом:
// Сначала определим границы периода в коде
ГраницаНачала = РезультатСЗ.Итог("Период", "Минимум");
ГраницаКонца = РезультатСЗ.Итог("Период", "Максимум");
ЗапросНарастающий = Новый Запрос;
ЗапросНарастающий.Текст =
"ВЫБРАТЬ
| Исходник.Номенклатура КАК Номенклатура,
| Исходник.Склад КАК Склад,
| Исходник.Период КАК Период
|ПОМЕСТИТЬ ВТ_Критерии
|ИЗ
| &ТаблицаИсходник КАК Исходник
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ОстаткиИОбороты.Номенклатура КАК Номенклатура,
| ОстаткиИОбороты.Склад КАК Склад,
| ОстаткиИОбороты.Период КАК ПериодДвижения,
| ОстаткиИОбороты.КоличествоНачальныйОстаток КАК НачальныйОстаток,
| ОстаткиИОбороты.КоличествоОборот КАК Оборот
|ПОМЕСТИТЬ ВТ_ДвиженияРегистра
|ИЗ
| РегистрНакопления.ДвижениеТМЦ.ОстаткиИОбороты(&НачалоПериода, &КонецПериода, День, , ) КАК ОстаткиИОбороты
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_Критерии.Номенклатура КАК Номенклатура,
| ВТ_Критерии.Склад КАК Склад,
| ВТ_Критерии.Период КАК Период,
| МАКСИМУМ(ЕСТЬNULL(ВТ_ДвиженияРегистра.НачальныйОстаток, 0)) + СУММА(ЕСТЬNULL(ВТ_ДвиженияРегистра.Оборот, 0)) КАК ОстатокСклада
|ИЗ
| ВТ_Критерии КАК ВТ_Критерии
| ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ДвиженияРегистра КАК ВТ_ДвиженияРегистра
| ПО ВТ_Критерии.Номенклатура = ВТ_ДвиженияРегистра.Номенклатура
| И ВТ_Критерии.Склад = ВТ_ДвиженияРегистра.Склад
| И ВТ_Критерии.Период >= ВТ_ДвиженияРегистра.ПериодДвижения
|ГРУППИРОВАТЬ ПО
| ВТ_Критерии.Номенклатура,
| ВТ_Критерии.Склад,
| ВТ_Критерии.Период";
---
Часто на практике оказывается, что строк в таблице много (например, 10 000 строк), но уникальных дат среди них всего несколько (например, остатки нужно получить на конец прошлых трех месяцев или на даты пяти ключевых документов). В таком случае нет необходимости строить сложные виртуальные соединения.
Выясним логику этого метода:
РезультатСЗ.Скопировать() и последующего Свернуть("Период") получаем список только уникальных дат.Остатки с фильтром по этой дате и общему массиву номенклатуры.Запрос.ВыполнитьПакет().Рассмотрим пример кода, реализующего этот алгоритм:
ТаблицаДат = РезультатСЗ.Скопировать();
ТаблицаДат.Свернуть("Период");
МассивНоменклатуры = РезультатСЗ.ВыгрузитьКолонку("Номенклатура");
МассивСкладов = РезультатСЗ.ВыгрузитьКолонку("Склад");
ЗапросПакет = Новый Запрос;
ТекстЗапроса = "";
Для Индекс = 0 По ТаблицаДат.Количество() - 1 Цикл
ИмяПараметраДаты = "Дата_" + Строка(Индекс);
ЗапросПакет.УстановитьПараметр(ИмяПараметраДаты, ТаблицаДат[Индекс].Период);
ТекстЗапроса = ТекстЗапроса +
"ВЫБРАТЬ
| &" + ИмяПараметраДаты + " КАК Период,
| Остатки.Номенклатура КАК Номенклатура,
| Остатки.Склад КАК Склад,
| Остатки.КоличествоОстаток КАК ОстатокСклада
|ИЗ
| РегистрНакопления.ДвижениеТМЦ.Остатки(&" + ИмяПараметраДаты + ", Номенклатура В (&МассивНоменклатуры) И Склад В (&МассивСкладов)) КАК Остатки
|";
Если Индекс < ТаблицаДат.Количество() - 1 Тогда
ТекстЗапроса = ТекстЗапроса + "ОБЪЕДИНИТЬ ВСЕ" + Символы.ПС;
КонецЕсли;
КонецЦикла;
ЗапросПакет.Текст = ТекстЗапроса;
ЗапросПакет.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
ЗапросПакет.УстановитьПараметр("МассивСкладов", МассивСкладов);
ТаблицаОстатков = ЗапросПакет.Выполнить().Выгрузить();
// Теперь у нас есть единая таблица остатков на все уникальные даты,
// и мы можем быстро сопоставить данные с исходной таблицей.
---
Для файловых баз данных или высоконагруженных систем сложные SQL-запросы со связями по условиям сравнения дат могут выполняться медленнее, чем обработка в оперативной памяти сервера приложений 1С. Мы можем перенести тяжелые расчеты на сторону сервера приложений, используя индексацию таблиц значений.
Проанализируем этот сценарий по шагам:
ТЗ_Движения на сервере приложений.ТЗ_Движения по колонкам Номенклатура, Склад. Это позволит методу НайтиСтроки() работать со скоростью прямого обращения к памяти.РезультатСЗ, находим соответствующие строки движений в проиндексированной таблице и быстро суммируем остаток в памяти 1С.Пример кода обработки данных в оперативной памяти:
// Предположим, мы получили таблицу ТЗ_Движения, содержащую колонки:
// Номенклатура, Склад, ПериодДвижения, НачальныйОстаток, Оборот
ТЗ_Движения.Индексы.Добавить("Номенклатура, Склад");
Для Каждого СтрокаИсходная Из РезультатСЗ Цикл
ПараметрыОтбора = Новый Структура("Номенклатура, Склад", СтрокаИсходная.Номенклатура, СтрокаИсходная.Склад);
НайденныеСтроки = ТЗ_Движения.НайтиСтроки(ПараметрыОтбора);
ТекущийОстаток = 0;
ПервыйПроход = Истина;
Для Каждого СтрокаДвижения Из НайденныеСтроки Цикл
Если СтрокаДвижения.ПериодДвижения <= СтрокаИсходная.Период Тогда
Если ПервыйПроход Тогда
ТекущийОстаток = СтрокаДвижения.НачальныйОстаток;
ПервыйПроход = Ложь;
КонецЕсли;
ТекущийОстаток = ТекущийОстаток + СтрокаДвижения.Оборот;
КонецЕсли;
КонецЦикла;
СтрокаИсходная.ОстатокСклада = ТекущийОстаток;
КонецЦикла;
---
Если ваша задача — вывести эти данные в отчет или в форму, идеальным и самым простым решением будет использование Системы компоновки данных (СКД). Платформа 1С содержит встроенный оптимизатор связей наборов данных.
Разберем, как это настроить в СКД:
РезультатСЗ). В нем будут поля: Номенклатура, Склад, Период.
ВЫБРАТЬ
Остатки.КоличествоОстаток КАК ОстатокСклада
ИЗ
РегистрНакопления.ДвижениеТМЦ.Остатки(&ПериодГраница, Номенклатура = &НоменклатураПараметр И Склад = &СкладПараметр) КАК Остатки
Номенклатура и Склад.Период из Набора 1 должно передаваться в параметр &ПериодГраница Набора 2.Почему СКД работает быстро? Платформа 1С на уровне СУБД автоматически группирует передаваемые параметры. Вместо выполнения отдельного SQL-запроса на каждую строку, СКД формирует пакетные запросы (обычно передавая по 1000 строк в одном пакете), минимизируя нагрузку на сервер СУБД.
---
Каждый из рассмотренных методов оптимален для своих условий: