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