При разработке сложных учетных документов разработчики часто сталкиваются с задачей синхронизации двух табличных частей. Типичный сценарий: первая таблица содержит сводный перечень номенклатуры с базовым количеством, а во второй таблице данные детализируются по складам, характеристикам или партиям. При росте количества строк алгоритмы пересчета с вложенными циклами начинают существенно замедлять работу формы документа. Разберем по шагам, почему возникает просадка производительности и какими способами можно оптимизировать данный процесс.
Проанализируем типовую ситуацию. Пусть в первой таблице содержится N строк, а во второй таблице — M строк. Если при каждом добавлении или изменении строки разработчик выполняет полный перебор второй таблицы для каждой строки первой таблицы, вычислительная сложность такого алгоритма составляет O(N * M). При динамическом увеличении строк время выполнения операции растет квадратично.
Ситуация усугубляется следующими типичными ошибками:
Справочники.Склады.НайтиПоНаименованию(...) заставляет платформу на каждой итерации выполнять отдельный SQL-запрос к СУБД.Рассмотрим первоначальный оптимизированный вариант обработки в памяти. Если расчет выполняется в обработчике формы, необходимо исключить любые повторяющиеся обращения к метаданным и выполнять поиск только по измененному ключу связи.
Посмотрим на пример правильной организации локального перебора строк:
// Кэшируем постоянные ссылки до входа в циклы
СсылкаОсновнойСклад = Справочники.Склады.НайтиПоНаименованию("Склад Основной");
// Создаем промежуточную таблицу для расчета дельты
ТЗ_Промежуточная = Новый ТаблицаЗначений;
ТЗ_Промежуточная.Колонки.Добавить("Склад", Новый ОписаниеТипов("СправочникСсылка.Склады"));
ТЗ_Промежуточная.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 3)));
// Ограничиваем выборку только связанными строками по конкретной номенклатуре
ПараметрыОтбора = Новый Структура("НоменклатураОСН", НоменклатураОСН);
НайденныеСтроки = Объект.ТЧВтор.НайтиСтроки(ПараметрыОтбора);
Если НайденныеСтроки.Количество() > 0 Тогда
Для Каждого СтрТЧ Из НайденныеСтроки Цикл
// Исключаем лишние склады, используя заранее полученную ссылку
Если СтрТЧ.Склад <> СсылкаОсновнойСклад Тогда
КоличествоКРаспределению = СтрТЧ.КоличествоИзДока - СтрТЧ.Количество;
Если КоличествоКРаспределению <> 0 Тогда
НоваяСтр = ТЗ_Промежуточная.Добавить();
НоваяСтр.Склад = СтрТЧ.Магазин;
НоваяСтр.Количество = КоличествоКРаспределению;
КонецЕсли;
КонецЕсли;
КонецЦикла;
КонецЕсли;
// Сворачиваем дубли по складам
ТЗ_Промежуточная.Свернуть("Склад", "Количество");
// Пакетно добавляем рассчитанные данные
Для Каждого СтрИтог Из ТЗ_Промежуточная Цикл
НоваяСтрокаТЧ = Объект.ТЧВтор.Добавить();
НоваяСтрокаТЧ.Номенклатура = Номенклатура;
НоваяСтрокаТЧ.НоменклатураОСН = НоменклатураОСН;
НоваяСтрокаТЧ.Склад = СтрИтог.Склад;
НоваяСтрокаТЧ.КоличествоИзДока = СтрИтог.Количество;
КонецЦикла;
Вынос метода НайтиПоНаименованию за пределы цикла сразу сокращает время выполнения на порядок. Однако при работе с большими объемами данных предпочтительнее использовать механизмы СУБД или индексацию коллекций.
Выясним причину медленной работы метода НайтиСтроки. По умолчанию метод выполняет последовательное сканирование всей таблицы. Чтобы поиск выполнялся мгновенно (за логарифмическое время), добавим индекс по ключевым полям.
Разберем пример создания индекса для локальной таблицы:
// Выгружаем табличную часть во временную ТЗ
ТЗ_Данные = Объект.ТЧВтор.Выгрузить();
// Добавляем индекс по полям поиска
ТЗ_Данные.Индексы.Добавить("НоменклатураОСН, Склад");
// Теперь поиск по этой структуре будет использовать хэш-индекс
Отбор = Новый Структура;
Отбор.Вставить("НоменклатураОСН", ВыбНоменклатура);
Отбор.Вставить("Склад", ВыбСклад);
Строки = ТЗ_Данные.НайтиСтроки(Отбор);
Если требуется сопоставить две таблицы один к одному или один ко многим, наиболее эффективным объектом в оперативной памяти является Соответствие. Поиск по ключу в объекте Соответствие имеет сложность O(1).
Рассмотрим наиболее масштабируемое решение: полную передачу расчетов в механизм запросов 1С. Запрос выполняется на стороне сервера, использует оптимизатор СУБД и производит группировку, соединение и расчет остатков за один проход.
Разберем реализацию серверной процедуры:
&НаСервереБезКонтекста
Функция ПересчитатьСвязанныеДанныеТаблиц(Таблица1, Таблица2, ОтборНоменклатура)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Т1.Номенклатура КАК Номенклатура,
| Т1.Количество КАК КоличествоПлан
|ПОМЕСТИТЬ ВТ_Таблица1
|ИЗ
| &Таблица1 КАК Т1
|ГДЕ
| Т1.Номенклатура = &ОтборНоменклатура
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Т2.Номенклатура КАК Номенклатура,
| Т2.Склад КАК Склад,
| СУММА(Т2.Количество) КАК КоличествоФакт
|ПОМЕСТИТЬ ВТ_Таблица2
|ИЗ
| &Таблица2 КАК Т2
|ГДЕ
| Т2.Номенклатура = &ОтборНоменклатура
| И Т2.Склад <> &ОсновнойСклад
|СГРУППИРОВАТЬ ПО
| Т2.Номенклатура,
| Т2.Склад
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_Таблица2.Номенклатура КАК Номенклатура,
| ВТ_Таблица2.Склад КАК Склад,
| ЕСТЬNULL(ВТ_Таблица1.КоличествоПлан, 0) - ВТ_Таблица2.КоличествоФакт КАК ОстатокКРаспределению
|ИЗ
| ВТ_Таблица2 КАК ВТ_Таблица2
| ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Таблица1 КАК ВТ_Таблица1
| ПО ВТ_Таблица2.Номенклатура = ВТ_Таблица1.Номенклатура";
Запрос.УстановитьПараметр("Таблица1", Таблица1);
Запрос.УстановитьПараметр("Таблица2", Таблица2);
Запрос.УстановитьПараметр("ОтборНоменклатура", ОтборНоменклатура);
Запрос.УстановитьПараметр("ОсновнойСклад", Справочники.Склады.ОсновнойСклад);
Возврат Запрос.Выполнить().Выгрузить();
КонецФункции
Если задача требует последовательного списания количества (уменьшения исходного остатка первой таблицы строками второй таблицы по порядку), оптимально применить расчет нарастающего итога.
Посмотрим на пример вычисления нарастающего итога с помощью оконных функций, доступных в современных версиях платформы 1С:Предприятие 8.3:
ВЫБРАТЬ
Т.НомерСтроки КАК НомерСтроки,
Т.Номенклатура КАК Номенклатура,
Т.Количество КАК КоличествоСтроки,
СУММА(Т.Количество) OVER (
ПАРТИЦИЯ ПО Т.Номенклатура
УПОРЯДОЧИТЬ ПО Т.НомерСтроки
МЕЖДУ НЕОГРАНИЧЕННО ПРЕДШЕСТВУЮЩИМИ И ТЕКУЩЕЙ СТРОКОЙ
) КАК НакопленныйИтог
ИЗ
&ТЧВторая КАК Т
Если версия платформы не поддерживает оконные функции, аналогичный результат достигается соединением таблицы самой с собой по условию Т1.НомерСтроки >= Т2.НомерСтроки с последующей группировкой.
&НаСервереБезКонтекста, передавая на сервер только необходимые колонки табличных частей.ПредопределенноеЗначение("Справочник.Склады.Основной").