При разработке интерфейсов и отчетов в 1С часто возникает задача отфильтровать строки в таблице: оставить только те позиции номенклатуры, у которых цена выше или равна заданному пользователем порогу, а остальные строки удалить. Разберем, как правильно решить эту задачу программно, а также выясним, почему попытка сначала заполнить строки, выполняя запросы в циклах, является грубой архитектурной ошибкой.
Посмотрим на частую ошибку начинающих разработчиков: обход справочника Номенклатура в цикле, внутри которого запускается цикл по типам цен, а внутри него — каждый раз создается и выполняется объект Запрос. Такой подход создает критическую нагрузку на СУБД из-за сотен и тысяч отдельных обращений к базе данных.
Кроме того, добавление строки в ТабличноеПоле или коллекцию ТаблицаЗначений до проверки условий с последующим удалением неэффективно. Правило разработки в 1С гласит: ненужные данные следует отсекать как можно раньше — непосредственно на этапе выполнения запроса к базе данных. Однако, если таблица уже сформирована в памяти и ее требуется отфильтровать, рассмотрим корректные способы удаления строк по условию.
Проанализируем стандартную проблему прямого цикла: если обходить строки от 0 до конца таблицы и удалять неподходящие строки методом Удалить(), индексы оставшихся элементов смещаются влево. В результате следующая после удаленной строка пропускается и не проверяется.
Чтобы избежать пропуска строк, разберем обход коллекции с конца к началу (обратный цикл). В этом случае удаление текущего элемента сдвигает только уже проверенные строки с большими индексами, не влияя на оставшиеся в начале таблицы элементы:
Индекс = ТаблицаЦен.Количество() - 1;
Пока Индекс >= 0 Цикл
Если ТаблицаЦен[Индекс].ЗакупочнаяЦена < ПолеВвода2 Тогда
ТаблицаЦен.Удалить(Индекс);
КонецЕсли;
Индекс = Индекс - 1;
КонецЦикла;
Рассмотрим альтернативный способ, который часто применяется для повышения читаемости кода. Сначала выполняется обход коллекции конструкцией Для Каждого, в процессе которого ссылки на неподходящие строки собираются в объект Массив. Затем строки удаляются отдельным циклом:
УдаляемыеСтроки = Новый Массив;
Для Каждого Стр Из ТаблицаЦен Цикл
Если Стр.ЗакупочнаяЦена < ПолеВвода2 Тогда
УдаляемыеСтроки.Добавить(Стр);
КонецЕсли;
КонецЦикла;
Для Каждого Стр Из УдаляемыеСтроки Цикл
ТаблицаЦен.Удалить(Стр);
КонецЦикла;
Этот метод исключает путаницу с индексами, так как метод Удалить() коллекции ТаблицаЗначений умеет принимать в качестве параметра не только числовой индекс, но и прямую строку таблицы (СтрокаТаблицыЗначений).
Если из большой таблицы требуется удалить значительное количество строк (например, более половины), постоянные вызовы метода Удалить() приводят к многократной перестройке внутреннего массива памяти. В такой ситуации эффективнее создать пустую таблицу с аналогичной структурой колонок и скопировать туда только нужные строки:
ИтоговаяТЗ = ТаблицаЦен.СкопироватьКолонки();
Для Каждого Стр Из ТаблицаЦен Цикл
Если Стр.ЗакупочнаяЦена >= ПолеВвода2 Тогда
ЗаполнитьЗначенияСвойств(ИтоговаяТЗ.Добавить(), Стр);
КонецЕсли;
КонецЦикла;
ТаблицаЦен = ИтоговаяТЗ;
Рассмотрим случай, когда таблица уже заполнена в памяти, но отбор требуется выполнить гибко. Ее можно передать в Запрос в качестве параметра и выполнить фильтрацию средствами механизма запросов платформы:
Запрос = Новый Запрос;
МВТ = Новый МенеджерВременныхТаблиц;
Запрос.МенеджерВременныхТаблиц = МВТ;
Запрос.Текст =
"ВЫБРАТЬ * ПОМЕСТИТЬ ВремТЗ ИЗ &ИсходнаяТЗ КАК ИсходнаяТЗ;
|ВЫБРАТЬ * ИЗ ВремТЗ ГДЕ ЗакупочнаяЦена >= &ПорогЦены";
Запрос.УстановитьПараметр("ИсходнаяТЗ", ТаблицаЦен);
Запрос.УстановитьПараметр("ПорогЦены", ПолеВвода2);
ТаблицаЦен = Запрос.Выполнить().Выгрузить();
Важный нюанс: для успешной передачи таблицы значений в запрос все колонки таблицы должны иметь строго типизированное описание типов. Нетипизированные колонки вызовут ошибку выполнения запроса.
Выясним, как с самого начала избежать проблемы удаления строк. Вместо перебора объектов Справочники.Номенклатура.Выбрать() и выполнения запросов в циклах, актуальные цены необходимо получать одним пакетным запросом к виртуальной таблице РегистрСведений.ЦеныНоменклатуры.СрезПоследних.
Разберем по шагам, как сгруппировать разные типы цен в одну строку по номенклатуре и сразу отсечь позиции с ценой ниже порога:
СрезПоследних, передавая параметры периода и условий отбора в параметры виртуальной таблицы (а не в блок ГДЕ). Это гарантирует получение среза актуальных цен, а не всей истории записей регистра.ВЫБОР КОГДА совместно с агрегатной функцией МАКСИМУМ для разворачивания типов цен в отдельные колонки строки.ИМЕЮЩИЕ для фильтрации итоговых значений, отсекая строки, где закупочная цена не достигает заданного значения.Ссылка, исключая соединение по текстовому полю Наименование.Посмотрим на пример правильного текста запроса:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Срез.Номенклатура КАК Наименование,
| МАКСИМУМ(ВЫБОР КОГДА Срез.ТипЦен = &ТипЦеныЗакупочная ТОГДА Срез.Цена ИНАЧЕ 0 КОНЕЦ) КАК ЗакупочнаяЦена,
| МАКСИМУМ(ВЫБОР КОГДА Срез.ТипЦен = &ТипЦеныРозничная ТОГДА Срез.Цена ИНАЧЕ 0 КОНЕЦ) КАК РозничнаяЦена
|ИЗ
| РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, ) КАК Срез
|СГРУППИРОВАТЬ ПО
| Срез.Номенклатура
|ИМЕЮЩИЕ
| МАКСИМУМ(ВЫБОР КОГДА Срез.ТипЦен = &ТипЦеныЗакупочная ТОГДА Срез.Цена ИНАЧЕ 0 КОНЕЦ) >= &НижнийПредел";
Запрос.УстановитьПараметр("Период", ТекущаяДата());
Запрос.УстановитьПараметр("ТипЦеныЗакупочная", Справочники.ТипыЦенНоменклатуры.НайтиПоНаименованию("Закупочная"));
Запрос.УстановитьПараметр("ТипЦеныРозничная", Справочники.ТипыЦенНоменклатуры.НайтиПоНаименованию("Розничная"));
Запрос.УстановитьПараметр("НижнийПредел", ПолеВвода2);
ТабличноеПоле1 = Запрос.Выполнить().Выгрузить();
Обратите внимание на ограничения встроенных функций:
ТаблицаЗначений.НайтиСтроки() принимает структуру отбора, которая выполняет поиск только по строгому равенству значений. Выполнить отбор по условиям «больше», «меньше» или «не равно» с помощью этого метода невозможно.ТабличноеПоле рекомендуется выгружать данные в объект ТаблицаЗначений, выполнять обработку в оперативной памяти и загружать результат обратно, чтобы избежать постоянной перерисовки графического интерфейса.