В процессе разработки в среде 1С программисты регулярно сталкиваются с необходимостью постобработки массивов данных, представленных в виде коллекций в оперативной памяти. Типичный пример — это работа с объектом ТаблицаЗначений, в которой содержатся неуникальные строки, например, пары «Артикул — Цена». Наша задача — сгруппировать эту таблицу по артикулу и найти для каждого из них максимальное значение цены.
Многие начинающие разработчики сразу вспоминают про метод Свернуть(), однако стандартное его использование суммирует числовые показатели, что искажает результат, если нам требуется именно максимум, а не сумма. В этой статье мы подробно разберем четыре практических способа решения этой задачи: от классического запроса к временной таблице до высокопроизводительного алгоритма на базе соответствия и оригинального трюка с сортировкой.
Проанализируем ситуацию, выясним причины возможных ошибок типизации и разберем каждый подход по шагам.
Использование механизма запросов в 1С — самый гибкий и масштабируемый метод группировки данных. Однако напрямую передать таблицу значений в предложение ИЗ основного запроса нельзя (платформа выдаст ошибку). Данные необходимо предварительно поместить во временную таблицу.
Крайне важный нюанс: Чтобы передать таблицу значений в качестве параметра запроса, все её колонки должны иметь строго определенные типы данных. Если вы добавите колонку без указания типа (например, просто ТЗ.Колонки.Добавить("Цена")), при выполнении запроса вы неизбежно получите ошибку: «Содержимое объекта данных может быть выбрано только во временную таблицу» или «Несоответствие типов».
Рассмотрим подробнее реализацию этого метода:
// Шаг 1. Создаем описание типов для колонок таблицы значений
ОписаниеТипаСтрок = Новый ОписаниеТипов("Строка", Новый КвалификаторыСтроки(30));
ОписаниеТипЦены = Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2));
// Шаг 2. Инициализируем таблицу значений с жесткой типизацией
ТЗЦеныТоваров = Новый ТаблицаЗначений;
ТЗЦеныТоваров.Колонки.Добавить("Артикул", ОписаниеТипаСтрок);
ТЗЦеныТоваров.Колонки.Добавить("Цена", ОписаниеТипЦены);
// Шаг 3. Заполняем таблицу демонстрационными данными
НоваяСтрока = ТЗЦеныТоваров.Добавить();
НоваяСтрока.Артикул = "123";
НоваяСтрока.Цена = 15;
НоваяСтрока = ТЗЦеныТоваров.Добавить();
НоваяСтрока.Артикул = "123";
НоваяСтрока.Цена = 35; // Это максимальная цена для 123
НоваяСтрока = ТЗЦеныТоваров.Добавить();
НоваяСтрока.Артикул = "321";
НоваяСтрока.Цена = 10;
НоваяСтрока = ТЗЦеныТоваров.Добавить();
НоваяСтрока.Артикул = "321";
НоваяСтрока.Цена = 53; // Это максимальная цена для 321
// Шаг 4. Настраиваем запрос
Запрос = Новый Запрос;
// Передаем нашу таблицу значений в качестве параметра
Запрос.УстановитьПараметр("ВходящаяТЗ", ТЗЦеныТоваров);
// Текст запроса состоит из двух частей:
// 1. Помещение таблицы в менеджер временных таблиц
// 2. Группировка по артикулу с вычислением МАКСИМУМ(Цена)
Запрос.Текст =
"ВЫБРАТЬ
| ВходящаяТЗ.Артикул КАК Артикул,
| ВходящаяТЗ.Цена КАК Цена
|ПОМЕСТИТЬ ВременнаяТЗ
|ИЗ
| &ВходящаяТЗ КАК ВходящаяТЗ
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВременнаяТЗ.Артикул КАК Артикул,
| МАКСИМУМ(ВременнаяТЗ.Цена) КАК МаксимальнаяЦена
|ИЗ
| ВременнаяТЗ КАК ВременнаяТЗ
|
|СГРУППИРОВАТЬ ПО
| ВременнаяТЗ.Артикул";
// Выполняем запрос и выгружаем результат обратно в новую ТЗ
ТЗРезультат = Запрос.Выполнить().Выгрузить();
// Проверим результат обходом коллекции
Для Каждого СтрокаРезультата Из ТЗРезультат Цикл
Сообщить("Артикул: " + СтрокаРезультата.Артикул + " -> Макс. Цена: " + СтрокаРезультата.МаксимальнаяЦена);
КонецЦикла;
Этот способ хорош тем, что логика агрегации перекладывается на движок СУБД (или локальный SQL-процессор платформы), что гарантирует корректность работы со всеми типами чисел и исключает ручной перебор строк программным кодом.
Если ваша таблица значений обрабатывается исключительно в оперативной памяти (например, на клиенте в тонком клиенте или внутри быстрой серверной процедуры) и содержит десятки тысяч строк, обращение к механизму запросов может вызвать накладные расходы на сериализацию данных.
В этом случае оптимальным решением является использование объекта Соответствие. Данный алгоритм имеет вычислительную сложность O(N), то есть выполняется за один линейный проход по таблице.
Разберем этот алгоритм по шагам:
// Создаем соответствие для хранения уникальных артикулов и их максимальных цен
МаксимальныеЦеныMap = Новый Соответствие;
Для Каждого СтрокаТЗ Из ТЗЦеныТоваров Цикл
ТекущийАртикул = СтрокаТЗ.Артикул;
ТекущаяЦена = СтрокаТЗ.Цена;
// Пытаемся получить уже сохраненный максимум по этому артикулу
НайденныйМаксимум = МаксимальныеЦеныMap.Получить(ТекущийАртикул);
Если НайденныйМаксимум = Неопределено Или ТекущаяЦена > НайденныйМаксимум Тогда
// Если артикул встретился впервые или текущая цена больше сохраненного максимума
МаксимальныеЦеныMap.Вставить(ТекущийАртикул, ТекущаяЦена);
КонецЕсли;
КонецЦикла;
// Полученное соответствие можно легко преобразовать в новую результирующую Таблицу Значений
ТЗМаксимумы = Новый ТаблицаЗначений;
ТЗМаксимумы.Колонки.Добавить("Артикул", ОписаниеТипаСтрок);
ТЗМаксимумы.Колонки.Добавить("Цена", ОписаниеТипЦены);
Для Каждого ЭлементСоответствия Из МаксимальныеЦеныMap Цикл
НоваяСтр = ТЗМаксимумы.Добавить();
НоваяСтр.Артикул = ЭлементСоответствия.Ключ;
НоваяСтр.Цена = ЭлементСоответствия.Значение;
КонецЦикла;
Преимущества метода: Работает мгновенно, не требует вызова СУБД, не зависит от наличия жесткой типизации колонок в исходной таблице значений.
На форуме часто ведутся споры о том, можно ли использовать стандартный метод Свернуть() для поиска экстремумов. Напрямую — нет, так как метод складывает числа. Но существует красивый трюк, который позволяет обойти это ограничение без тяжелых запросов и ручного сбора новых таблиц.
Идея заключается в следующем:
Свернуть("Артикул", "Цена"). Метод просуммирует цены. Поскольку для каждого артикула во всех строках, кроме первой, теперь записаны нули, сумма будет равна значению первой строки — то есть нашему максимуму!Посмотрим на практическую реализацию этого трюка:
// 1. Сортируем: Артикул по возрастанию, Цена по убыванию
ТЗЦеныТоваров.Сортировать("Артикул Возр, Цена Убыв");
// 2. Обнуляем неуникальные строки, оставляя нетронутой только первую (максимальную)
КоличествоСтрок = ТЗЦеныТоваров.Количество();
Для Индекс = 1 По КоличествоСтрок - 1 Цикл
// Сравниваем артикул текущей строки с артикулом предыдущей
Если ТЗЦеныТоваров[Индекс].Артикул = ТЗЦеныТоваров[Индекс - 1].Артикул Тогда
// Обнуляем цену, так как она меньше максимальной, которая уже поднялась наверх группы
ТЗЦеныТоваров[Индекс].Цена = 0;
КонецЕсли;
КонецЦикла;
// 3. Теперь безопасно сворачиваем. Сумма с нулями даст верный максимум
ТЗЦеныТоваров.Свернуть("Артикул", "Цена");
Этот способ элегантен, не требует создания вспомогательных объектов баз данных и отлично подходит для лаконичного решения задачи «на месте» (in-place) в исходной таблице значений.
Если вам необходимо сгруппировать таблицу значений без отправки на сервер СУБД, но при этом вы хотите использовать лаконичный декларативный синтаксис агрегатных функций, можно прибегнуть к возможностям объекта ПостроительЗапроса. Построитель запроса использует локальный движок файловой СУБД платформы для обработки объектов в памяти.
Проанализируем пример использования этого объекта:
// Оборачиваем нашу таблицу в описание источника данных
ОписаниеИсточника = Новый ОписаниеИсточникаДанных(ТЗЦеныТоваров);
// Указываем, какую агрегатную функцию применить к числовому полю
ОписаниеИсточника.Колонки.Цена.Итог = "Максимум(Цена)";
// Настраиваем Построитель запроса
Построитель = Новый ПостроительЗапроса;
Построитель.ИсточникДанных = ОписаниеИсточника;
Построитель.ЗаполнитьНастройки();
// Задаем группировку по ключевому полю
Построитель.Группировки.Добавить("Артикул");
// Выполняем расчет и выгружаем результат
РезультатПостроителя = Построитель.Результат;
ТЗМаксимальныеЦены = РезультатПостроителя.Выгрузить();
Примечание: Несмотря на удобство работы в локальной памяти, данный способ в современной разработке на управляемых формах 1С 8.3 применяется редко и считается устаревшим. Однако он до сих пор незаменим при оптимизации legacy-кода в старых конфигурациях.
Посмотрим на типичные грабли, на которые наступают разработчики при решении подобных задач:
ТЗ = Новый ТаблицаЗначений; ТЗ.Колонки.Добавить("Артикул"); и попытались скормить её запросу, платформа упадет с ошибкой несовместимости типов данных. Всегда определяйте типы колонок через ОписаниеТипов при создании таблицы, если планируете передавать её в запросы.ПОМЕСТИТЬ НазваниеВТ. Прямая выборка из параметра-таблицы не поддерживается.СГРУППИРОВАТЬ ПО полностью сворачивает выборку и оставляет только агрегированные уникальные строки. Предложение ИТОГИ строит древовидную иерархическую структуру (выборку по группировкам с сохранением детальных записей). Для получения плоской результирующей таблицы используйте именно СГРУППИРОВАТЬ ПО.Мы рассмотрели все основные подходы к группировке таблиц значений с вычислением максимума в 1С. Выбирайте наиболее подходящий метод в зависимости от архитектурных ограничений вашей задачи (тонкий или толстый клиент, объем данных, требования к производительности).