Как посчитать количество свернутых строк и найти дубликаты в Таблице Значений 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При работе с коллекциями данных в платформе 1С:Предприятие разработчики часто сталкиваются с необходимостью анализа таблиц на наличие дублирующихся записей. Одной из типовых задач является подсчет количества исходных строк, которые вошли в ту или иную группу после свертки таблицы значений (ТЗ). Например, нам нужно узнать, сколько раз конкретный ИНН или комбинация «ИНН + Вид объекта» встречается в исходном массиве данных.

В этой статье мы подробно разберем, почему стандартный метод сворачивания ТЗ без предварительной подготовки возвращает нулевые значения, как правильно реализовать классический алгоритм подсчета свернутых строк, а также рассмотрим четыре альтернативных и более производительных подхода, которые часто применяются в реальной практике 1С-разработки.

Почему при обычном сворачивании в новой колонке получаются нули?

Рассмотрим типичную ошибку начинающего разработчика. Мы выгружаем данные, копируем таблицу, добавляем колонку Количество и пытаемся свернуть таблицу значений по ключевым полям, рассчитывая получить число строк в свернутых группах:


// Ошибочный вариант, который вернет 0
ПолнаяТЗ = Объект.ПолнаяТаблица.Выгрузить();
СгруппированнаяТЗ = ПолнаяТЗ.Скопировать();
СгруппированнаяТЗ.Колонки.Добавить("Количество");
СгруппированнаяТЗ.Свернуть("ИНН, ВидОбъекта", "Количество");

Выясним причину, почему в колонке Количество после выполнения этого кода везде будут нулевые значения. Метод Свернуть() работает по следующему принципу: он группирует строки по уникальным комбинациям значений в колонках группировки (первый параметр), а для колонок суммирования (второй параметр) рассчитывает математическую сумму значений по всем свернутым строкам.

Когда мы только добавили новую колонку с помощью метода Колонки.Добавить("Количество"), во всех строках этой колонки изначально содержатся пустые значения (по умолчанию для неопределенного типа это Неопределено или 0, если мы явно указали числовой тип). При сворачивании платформа 1С пытается просуммировать эти пустые значения. Сумма нулей всегда дает ноль. Соответственно, чтобы получить реальное количество свернутых строк, нам необходимо предварительно сообщить системе, что каждая строка до свертки представляет собой ровно одну единицу данных.

Для этого перед сворачиванием нужно принудительно заполнить добавленную колонку единицами.

Рассмотрим по шагам классический и полностью рабочий алгоритм решения этой задачи.

Классический метод: добавление вспомогательной колонки и заполнение единицами

Разобьем процесс подготовки и сворачивания таблицы значений на логические шаги:

  1. Создание копии таблицы: Чтобы не повредить исходные данные, которые могут понадобиться в дальнейшем алгоритме, мы создаем независимую копию таблицы значений с помощью метода Скопировать().
  2. Добавление служебной колонки: Добавляем новую колонку, в которой будет накапливаться количество. Желательно сразу явно ограничить её тип как Число, чтобы избежать неявного приведения типов платформой.
  3. Заполнение единицами: Используем быстрый платформенный метод ЗаполнитьЗначения(). Он работает в оперативной памяти практически мгновенно и заполняет указанную колонку во всех строках переданным значением (в нашем случае — числом 1).
  4. Сворачивание таблицы: Вызываем метод Свернуть(), передавая ключевые поля в качестве первого параметра, а нашу заполненную колонку количества — в качестве второго.

Посмотрим на практический пример реализации этого алгоритма в коде:


// 1. Копируем исходную таблицу значений
СгруппированнаяТЗ = Объект.ПолнаяТаблица.Выгрузить().Скопировать();

// 2. Добавляем колонку "Количество" с типом Число
СгруппированнаяТЗ.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число"));

// 3. Заполняем добавленную колонку единицами
СгруппированнаяТЗ.ЗаполнитьЗначения(1, "Количество");

// 4. Сворачиваем таблицу по ключевым полям и суммируем единицы
СгруппированнаяТЗ.Свернуть("ИНН, ВидОбъекта", "Количество");

Проанализируем полученный результат. В свернутой таблице значений СгруппированнаяТЗ колонка Количество теперь содержит корректные данные.

Важно понимать: значение, равное 1, указывает на то, что данная строка в исходной таблице была уникальной и не имела дублей по полям «ИНН» и «ВидОбъекта». Значение больше единицы (например, 2, 5, 10) наглядно показывает, сколько именно раз данная комбинация полей дублировалась в исходной таблице значений.

Этот способ прост в реализации и отлично подходит для небольших и средних таблиц значений. Однако у него есть существенный недостаток — он деструктивен (требует изменения структуры таблицы или создания копии всей ТЗ в оперативной памяти). Если исходная таблица содержит сотни тысяч строк, создание копии и заполнение колонок может привести к избыточному потреблению памяти и снижению производительности.

Поэтому рассмотрим более эффективные и профессиональные альтернативы.

Альтернатива 1. Использование запроса и временной таблицы (Рекомендуемый подход)

Если таблица значений содержит внушительный объем информации, операции непосредственно в оперативной памяти могут выполняться медленно. Использование встроенного языка запросов 1С и СУБД для обработки временных таблиц — это стандарт де-факто для оптимизации тяжелых алгоритмов.

Мы можем передать таблицу значений в запрос в качестве параметра, поместить её во временную таблицу и выполнить эффективную группировку силами СУБД. Ниже представлены три варианта реализации запроса в зависимости от ваших целей.

Вариант А. Простая группировка с подсчетом общего количества

Если нам нужно сгруппировать таблицу и получить количество вхождений для каждой строки (аналог классического метода, но силами запроса):


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ВТ_Исходная.ИНН КАК ИНН,
    |	ВТ_Исходная.ВидОбъекта КАК ВидОбъекта,
    |	КОЛИЧЕСТВО(*) КАК Количество
    |ПОМЕСТИТЬ ВТ_Сгруппированная
    |ИЗ
    |	&АдресТЗ КАК ВТ_Исходная
    |
    |СГРУППИРОВАТЬ ПО
    |	ВТ_Исходная.ИНН,
    |	ВТ_Исходная.ВидОбъекта";

Запрос.УстановитьПараметр("АдресТЗ", ПолнаяТЗ);
// Перед выполнением запроса у ТЗ обязательно должны быть определены типы колонок!
РезультатЗапроса = Запрос.Выполнить().Выгрузить();

Вариант Б. Выборка исключительно дублирующихся строк (Конструкция ИМЕЮЩИЕ)

Зачастую уникальные строки (где количество равно 1) нам вообще не интересны, а нужны только проблемные дубли. В этом случае в запросе чрезвычайно эффективно использовать секцию ИМЕЮЩИЕ. Она отсечет лишние уникальные строки на уровне СУБД и вернет только те записи, которые дублируются:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ВТ_Исходная.ИНН КАК ИНН,
    |	ВТ_Исходная.ВидОбъекта КАК ВидОбъекта,
    |	КОЛИЧЕСТВО(*) КАК КоличествоДублей
    |ИЗ
    |	&АдресТЗ КАК ВТ_Исходная
    |
    |СГРУППИРОВАТЬ ПО
    |	ВТ_Исходная.ИНН,
    |	ВТ_Исходная.ВидОбъекта
    |
    |ИМЕЮЩИЕ
    |	КОЛИЧЕСТВО(*) > 1";

Запрос.УстановитьПараметр("АдресТЗ", ПолнаяТЗ);
ТаблицаДублей = Запрос.Выполнить().Выгрузить();

Вариант В. Лайфхак для сверхбыстрого поиска дублей на огромных объемах данных

Для ускорения обработки таблиц данных размером в сотни тысяч и миллионы строк существует продвинутый прием. Вместо ресурсоемкого подсчета точного количества через КОЛИЧЕСТВО(*) мы можем применить проверку несовпадения минимального и максимального номеров строк исходной ТЗ в группе. Это работает значительно быстрее:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ВТ_Исходная.ИНН КАК ИНН,
    |	ВТ_Исходная.ВидОбъекта КАК ВидОбъекта
    |ИЗ
    |	&АдресТЗ КАК ВТ_Исходная
    |
    |СГРУППИРОВАТЬ ПО
    |	ВТ_Исходная.ИНН,
    |	ВТ_Исходная.ВидОбъекта
    |
    |ИМЕЮЩИЕ
    |	НЕ МИНИМУМ(ВТ_Исходная.НомерСтроки) = МАКСИМУМ(ВТ_Исходная.НомерСтроки)";

Данный прием базируется на том, что СУБД оптимизирует выполнение групповых функций: как только алгоритм вычисления агрегатов обнаруживает разницу между минимальным и максимальным индексами строк в группе, условие сразу считается выполненным, что экономит процессорное время на больших выборках.

Альтернатива 2. Использование индексации и метода НайтиСтроки()

Когда перед нами стоит задача не полностью перекроить и свернуть таблицу значений, а оперативно проверять наличие дубликатов для конкретных строк в процессе работы алгоритма, отлично подходит механизм индексации таблиц значений.

Добавление индекса существенно ускоряет поиск по выбранным полям, делая его практически мгновенным.

Рассмотрим пример реализации:


// 1. Добавляем индекс по ключевым полям для ускорения поиска
ПолнаяТЗ.Индексы.Добавить("ИНН, ВидОбъекта");

// 2. Формируем структуру отбора по интересующим нас значениям
Отбор = Новый Структура;
Отбор.Вставить("ИНН", ИскомыйИНН);
Отбор.Вставить("ВидОбъекта", ИскомыйВид);

// 3. Находим все строки, соответствующие отбору
НайденныеСтроки = ПолнаяТЗ.НайтиСтроки(Отбор);

// 4. Получаем количество дублей
КоличествоДублей = НайденныеСтроки.Количество();

Если КоличествоДублей > 1 Тогда
    ОбщегоНазначенияКлиентСервер.СообщитьПользователю(
        СтрШаблон("Значение %1 дублируется %2 раз(а)!", ИскомыйИНН, КоличествоДублей)
    );
КонецЕсли;

Главный плюс метода: структура исходной таблицы не меняется, новые колонки не добавляются, а поиск по индексированным полям в оперативной памяти происходит моментально.

Альтернатива 3. Подсчет через коллекцию «Соответствие» (в один проход)

Иногда требуется произвести расчеты без обращения к базе данных и СУБД (например, в тонком клиенте, где недоступен класс Запрос) и без создания копий таблиц в памяти. В таких случаях идеальным инструментом является объект Соответствие (в других языках программирования известный как Map или ассоциативный массив).

Мы можем обойти исходную таблицу один раз в цикле и собрать статистику по дубликатам. В качестве ключа соответствия мы будем использовать уникальный строковый ключ, собранный из ключевых полей, а в качестве значения — счетчик вхождений.

Разберем пример реализации этого алгоритма:


// Создаем пустое соответствие для накопления счетчиков
СчетчикДублей = Новый Соответствие;

// Обходим таблицу в один проход
Для Каждого СтрокаТЗ Из ПолнаяТЗ Цикл
    // Формируем уникальный текстовый ключ комбинации полей
    КлючСтроки = СтрокаТЗ.ИНН + "_" + СтрокаТЗ.ВидОбъекта;
    
    // Проверяем, встречалась ли уже такая комбинация
    ТекущееКоличество = СчетчикДублей.Получить(КлючСтроки);
    
    Если ТекущееКоличество = Неопределено Тогда
        // Если комбинация встретилась впервые, записываем 1
        СчетчикДублей.Вставить(КлючСтроки, 1);
    Иначе
        // Если уже была в таблице, увеличиваем счетчик на 1
        СчетчикДублей.Вставить(КлючСтроки, ТекущееКоличество + 1);
    КонецЕсли;
КонецЦикла;

// Теперь мы можем быстро узнать количество дублей для любой строки
Для Каждого СтрокаТЗ Из ПолнаяТЗ Цикл
    КлючСтроки = СтрокаТЗ.ИНН + "_" + СтрокаТЗ.ВидОбъекта;
    Количество = СчетчикДублей.Получить(КлючСтроки);
    
    Если Количество > 1 Тогда
        // Строка дублируется, пишем логику обработки...
    КонецЕсли;
КонецЦикла;

Преимущества подхода: высокая скорость работы за счет прямого обращения по хэш-ключам (сложность алгоритма составляет O(N)), применимость на клиенте и полная сохранность исходной структуры данных.

Альтернатива 4. Сортировка и обход с конца (для удаления дублей)

Если ваша конечная цель состоит не только в получении цифры количества дублей, а в физической очистке таблицы значений от повторяющихся строк, то самым производительным и экономным по памяти является алгоритм сортировки с последующим удалением строк «обратным ходом».

Важное правило 1С: если удалять строки из таблицы значений обычным циклом «от первого к последнему», то индексы всех последующих строк будут смещаться при каждом удалении. Это приведет к ошибкам выхода за границы коллекции или к пропуску строк при обходе. Поэтому удаление строк из коллекции всегда нужно производить с конца (обратным циклом).

Рассмотрим алгоритм на практике:


// 1. Сортируем таблицу по ключевым колонкам. 
// Это выстроит все дублирующиеся строки друг за другом.
ПолнаяТЗ.Сортировать("ИНН, ВидОбъекта");

// 2. Вычисляем индекс последней строки
ВсегоСтрок = ПолнаяТЗ.Количество();

// 3. Запускаем цикл с конца таблицы к началу (до 1-го индекса, т.к. сравниваем с предыдущим)
Для Индекс = 1 - ВсегоСтрок По 0 Цикл
    ТекущийИндекс = -Индекс;
    
    // Пропускаем первую строку (индекс 0), так как перед ней нет элементов для сравнения
    Если ТекущийИндекс = 0 Тогда
        Продолжить;
    КонецЕсли;
    
    СтрокаТекущая = ПолнаяТЗ[ТекущийИндекс];
    СтрокаПредыдущая = ПолнаяТЗ[ТекущийИндекс - 1];
    
    // Сравниваем ключевые поля текущей строки с предыдущей
    Если СтрокаТекущая.ИНН = СтрокаПредыдущая.ИНН 
       И СтрокаТекущая.ВидОбъекта = СтрокаПредыдущая.ВидОбъекта Тогда
        
        // Обнаружен дубль! Фиксируем факт дублирования и удаляем текущую строку
        ПолнаяТЗ.Удалить(ТекущийИндекс);
        
    КонецЕсли;
КонецЦикла;

Этот способ позволяет избавиться от дублей прямо на месте (in-place), без копирования данных в новые таблицы значений и без выделения значительных объемов оперативной памяти под временные структуры.

Подведем итоги

Мы детально изучили методы подсчета количества свернутых строк в Таблице Значений 1С. Выбор конкретного инструмента зависит от масштаба ваших задач:

← На главную