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