Одной из наиболее распространенных проблем производительности в 1С является выполнение запросов внутри цикла. Особенно часто это встречается в задачах распределения — когда нам нужно взять список товаров из табличной части документа и для каждой строки найти остатки по партиям в регистре накопления. В этой статье мы подробно разберем, почему так делать нельзя и как правильно переписать код, используя возможности языка запросов 1С, на примере конфигурации "Управление торговлей 10.3".
Проанализируем ситуацию: если в документе 100 строк, то при запросе в цикле система 100 раз обратится к СУБД (SQL Server или PostgreSQL). Каждое обращение — это сетевая задержка, время на парсинг запроса, построение плана выполнения и блокировку таблиц. Если же пользователей много, база начинает "тормозить", а транзакции проведения документов отваливаются по таймауту. Наша задача — выполнить один запрос, получить все необходимые данные разом и обработать их в памяти.
Самый простой способ исправить ситуацию — это выгрузить всю номенклатуру из табличной части в массив и передать его в параметры запроса. Рассмотрим, как это реализовать. Вместо поиска остатка для каждой позиции отдельно, мы просим систему выдать нам остатки по всем товарам сразу.
Для того чтобы удобно сгруппировать данные по товарам, мы будем использовать конструкцию ИТОГИ. Это позволит нам в одном результате запроса получить и общую сумму стоимости по товару (для расчета пропорции), и детализацию по конкретным партиям.
// 1. Подготавливаем список номенклатуры
МассивНоменклатуры = СписокРаспределения.ВыгрузитьКолонку("Номенклатура");
// 2. Формируем запрос
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ПартииОстатки.Номенклатура КАК Номенклатура,
| ПартииОстатки.ДокументОприходования КАК ДокументОприходования,
| ПартииОстатки.Склад КАК Склад,
| ПартииОстатки.СтоимостьОстаток КАК Стоимость
|ИЗ
| РегистрНакопления.ПартииТоваровНаСкладах.Остатки(&ДатаАктуальности, Номенклатура В (&МассивНоменклатуры)) КАК ПартииОстатки
|ИТОГИ
| СУММА(Стоимость)
|ПО
| Номенклатура";
Запрос.УстановитьПараметр("ДатаАктуальности", Дата);
Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
РезультатЗапроса = Запрос.Выполнить();
Обратите внимание на секцию ИТОГИ СУММА(Стоимость) ПО Номенклатура. Она создает иерархическую структуру в результате запроса. На верхнем уровне (группировка) мы сразу видим общую стоимость всех партий товара, а на нижнем (детальные записи) — конкретные склады и документы.
Если нам нужно не просто получить остатки, а сопоставить их с данными из табличной части (например, с суммой, которую нужно распределить), лучше использовать временные таблицы. Этот метод более надежен при больших объемах данных.
Разберем по шагам логику построения такого запроса:
ВТСписокРаспределения. Это позволит нам выполнять соединения прямо на стороне сервера СУБД.ЛЕВОЕ СОЕДИНЕНИЕ этой временной таблицы с виртуальной таблицей остатков регистра.ИТОГИ для удобного обхода результата.
// Выгружаем ТЧ в таблицу значений для передачи в запрос
ТаблицаДляЗапроса = СписокРаспределения.Выгрузить();
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.Текст =
"ВЫБРАТЬ
| ТЧ.Номенклатура КАК Номенклатура,
| ТЧ.СуммаРаспределения КАК СуммаРаспределения
|ПОМЕСТИТЬ ВТСписокРаспределения
|ИЗ
| &ТаблицаДляЗапроса КАК ТЧ
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ.Номенклатура КАК Номенклатура,
| ВТ.СуммаРаспределения КАК СуммаРаспределения,
| ЕСТЬNULL(Остатки.ДокументОприходования, ЗНАЧЕНИЕ(Документ.ПоступлениеТоваровУслуг.ПустаяСсылка)) КАК ДокументОприходования,
| ЕСТЬNULL(Остатки.Склад, ЗНАЧЕНИЕ(Справочник.Склады.ПустаяСсылка)) КАК Склад,
| ЕСТЬNULL(Остатки.СтоимостьОстаток, 0) КАК СтоимостьПартии
|ИЗ
| ВТСписокРаспределения КАК ВТ
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ПартииТоваровНаСкладах.Остатки(&ДатаАктуальности,
| Номенклатура В (ВЫБРАТЬ Номенклатура ИЗ ВТСписокРаспределения)) КАК Остатки
| ПО ВТ.Номенклатура = Остатки.Номенклатура
|ИТОГИ
| МАКСИМУМ(СуммаРаспределения),
| СУММА(СтоимостьПартии)
|ПО
| Номенклатура";
Запрос.УстановитьПараметр("ТаблицаДляЗапроса", ТаблицаДляЗапроса);
Запрос.УстановитьПараметр("ДатаАктуальности", Дата);
ВыборкаНоменклатура = Запрос.Выполнить().Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Когда мы распределяем сумму пропорционально стоимости, возникает проблема округления. Если просто умножать долю на сумму в каждой строке, итоговая сумма может не совпасть с исходной из-за сотых долей копейки. Рассмотрим классический алгоритм решения этой задачи при обходе выборки.
Проанализируем ситуацию: мы будем накапливать уже распределенную сумму и в последней строке партии просто вычтем накопленное из общего итога. Таким образом, баланс всегда будет идеальным.
Пока ВыборкаНоменклатура.Следующий() Цикл
// Проверка на наличие стоимости (избегаем деления на ноль)
ИтоговаяСтоимостьПоТовару = ВыборкаНоменклатура.СтоимостьПартии;
Если ИтоговаяСтоимостьПоТовару = 0 Тогда
Продолжить;
КонецЕсли;
СуммаДляРаспределения = ВыборкаНоменклатура.СуммаРаспределения;
РаспределеноВсего = 0;
ВыборкаДетали = ВыборкаНоменклатура.Выбрать();
КоличествоСтрок = ВыборкаДетали.Количество();
ТекущаяСтрока = 0;
Пока ВыборкаДетали.Следующий() Цикл
ТекущаяСтрока = ТекущаяСтрока + 1;
Если ТекущаяСтрока < КоличествоСтрок Тогда
// Расчет пропорции для всех строк, кроме последней
Доля = ВыборкаДетали.СтоимостьПартии / ИтоговаяСтоимостьПоТовару;
ТекущаяДобавка = Окр(Доля * СуммаДляРаспределения, 2);
РаспределеноВсего = РаспределеноВсего + ТекущаяДобавка;
Иначе
// В последнюю строку записываем остаток
ТекущаяДобавка = СуммаДляРаспределения - РаспределеноВсего;
КонецЕсли;
// Здесь формируем движение по регистру
Движение = Движения.ПартииТоваровНаСкладах.ДобавитьРасход();
// ... заполнение измерений (Номенклатура, Склад, ДокументОприходования) ...
Движение.Стоимость = ТекущаяДобавка;
КонецЦикла;
КонецЦикла;
При реализации данного подхода следует учитывать несколько критически важных моментов, которые помогут избежать ошибок в промышленной эксплуатации:
1. Момент времени и границы. При получении остатков в модуле проведения документа крайне важно правильно указать дату. Если вы используете просто Дата, движения текущего документа могут не учитываться или учитываться некорректно. В типовых решениях рекомендуется использовать объект Граница с видом МоментВремени.
2. Проверка на нулевые остатки. Всегда добавляйте проверку Если ИтоговаяСтоимостьПоТовару = 0. Если товар числится на складе в количестве, но его стоимость по регистру партий равна нулю (такое бывает при ошибках учета), ваш код вызовет исключение "Деление на 0", и документ не проведется.
3. Индексация временных таблиц. Если в вашем документе могут быть тысячи строк, обязательно добавляйте индексацию временной таблицы: ИНДЕКСИРОВАТЬ ПО Номенклатура. Это значительно ускорит операцию соединения таблиц в оперативной памяти SQL-сервера.
4. Использование ЕСТЬNULL. При левом соединении с таблицей остатков, если по какой-то номенклатуре остатков не нашлось, поля регистра вернут NULL. Обязательно обрабатывайте их функцией ЕСТЬNULL(..., 0), иначе математические операции приведут к ошибкам или пустым результатам.
5. Избыточность данных. В запросе старайтесь выбирать только те поля, которые реально нужны для формирования движений. Не стоит выбирать все ресурсы регистра, если вам нужна только СтоимостьОстаток. Это снижает нагрузку на сеть и сервер БД.
Рассмотренные нами методы позволяют превратить медленный, неоптимизированный код в профессиональное решение, соответствующее стандартам разработки "1С:Совместимо". Использование пакетных запросов и итогов — это база, которую должен знать каждый разработчик для обеспечения высокой производительности системы.