При сопоставлении данных из двух источников, сверке документов или корректировке регистров часто возникает задача: избавиться от парных строк с одинаковыми аналитическими реквизитами, но противоположными знаками числовых показателей (например, +100 и -100). Нередко разработчики искусственно умножают числовые поля одной из выборок на -1, объединяют таблицы и пытаются очистить полученный массив.
Разберем по шагам, какие подводные камни таит в себе этот подход, как правильно использовать метод Свернуть() встроенного языка, как отсечь нулевые остатки в запросе и как реализовать строгое попарное схлопывание записей.
Перед выбором алгоритма проанализируем структуру исходных данных. При простом математическом сложении сумм могут возникнуть две типовые логические коллизии:
+80, +10 и -90 по одной и той же аналитике, их сумма даст 0. Метод свертки полностью удалит эти строки, хотя фактически в исходных источниках не было прямой встречной пары для -90.-100 и одна строка с +100. После агрегации останется одна строка со значением -100. Если задача состояла в поиске несбалансированных операций, этот результат верен. Однако если требовалось строго отслеживать идентификаторы строк или первичные документы, связь между исходными записями будет безвозвратно потеряна.Если для бизнес-логики важно только итоговое сальдо по аналитике, применяются типовая свертка или группировка в запросе. Если же требуется исключить строго пары "один к одному", необходим попарный поиск с флагами сопоставления.
Рассмотрим классический вариант на уровне встроенного языка. Алгоритм состоит из двух шагов: схлопывание числовых полей по набору группировочных колонок и удаление тех строк, у которых итоговая сумма стала равна нулю.
Посмотрим на пример реализации:
// Шаг 1. Сворачиваем таблицу значений по ключевым колонкам
// Первым параметром передаем список измерений, вторым — список суммируемых ресурсов
ТЗ.Свернуть("Контрагент, Договор, Номенклатура", "Количество, Сумма");
// Шаг 2. Удаляем строки, сальдо по которым обнулилось
// При удалении строк по индексу цикл ВСЕГДА выполняем в обратном порядке
Для Индекс = ТЗ.Количество() - 1 По 0 Цикл
ТекущаяСтрока = ТЗ[Индекс];
// Учитываем возможную погрешность вычислений с плавающей точкой
Если Окр(ТекущаяСтрока.Количество, 3) = 0 И Окр(ТекущаяСтрока.Сумма, 2) = 0 Тогда
ТЗ.Удалить(Индекс);
КонецЕсли;
КонецЦикла;
Важный нюанс при удалении строк в цикле: никогда не используйте конструкцию Для Каждого Стр Из ТЗ Цикл ТЗ.Удалить(Стр). При вызове Удалить() внутри прямого обхода индексы оставшихся элементов сдвигаются влево, из-за чего итератор пропускает следующую за удаленной строку. Альтернативный безопасный вариант — предварительно собрать удаляемые строки в массив и обойти уже массив:
СтрокиКУдалению = Новый Массив;
Для Каждого Стр Из ТЗ Цикл
Если Окр(Стр.Сумма, 2) = 0 Тогда
СтрокиКУдалению.Добавить(Стр);
КонецЕсли;
КонецЦикла;
Для Каждого Стр Из СтрокиКУдалению Цикл
ТЗ.Удалить(Стр);
КонецЦикла;
Если данные изначально собираются запросом или исходная таблица передается во временную таблицу, операцию исключения нулевых сумм эффективнее выполнить на стороне СУБД. Это избавит от необходимости тратить память и время процессора сервера на ручные циклы обхода.
Разберем структуру запроса с секцией ИМЕЮЩИЕ:
ВЫБРАТЬ
ИсходныеДанные.Контрагент КАК Контрагент,
ИсходныеДанные.Номенклатура КАК Номенклатура,
СУММА(ИсходныеДанные.Сумма) КАК СуммаОстаток
ПОМЕСТИТЬ ВТ_Итоги
ИЗ
&ВнешняяТаблицаДанных КАК ИсходныеДанные
СГРУППИРОВАТЬ ПО
ИсходныеДанные.Контрагент,
ИсходныеДанные.Номенклатура
ИМЕЮЩИЕ
СУММА(ИсходныеДанные.Сумма) <> 0
;
ВЫБРАТЬ
ВТ_Итоги.Контрагент,
ВТ_Итоги.Номенклатура,
ВТ_Итоги.СуммаОстаток
ИЗ
ВТ_Итоги КАК ВТ_Итоги
Обратим внимание на работу с временными таблицами: если в запрос передается объект ТаблицаЗначений через параметр &ВнешняяТаблицаДанных, все колонки этой таблицы должны иметь жесткую типизацию (через объект ОписательТипов). Таблица значений с нетипизированными колонками вызовет ошибку исполнения запроса.
Выясним, как поступить, если нельзя просто складывать сальдо, а требуется исключать именно симметричные зеркальные пары. Например, при наличии строк +100, -100, -100 в результате должна остаться ровно одна строка -100.
Для быстрого поиска встречной пары на больших объемах данных обязательно создадим индекс по ключевым полям поиска:
// Добавляем служебную колонку для отметки сопоставленных строк
ТЗ.Колонки.Добавить("Сопоставлено", Новый ОписаниеТипов("Булево"));
// Создаем индекс для ускорения работы метода НайтиСтроки
ТЗ.Индексы.Добавить("Номенклатура, Сумма, Сопоставлено");
ПараметрыПоиска = Новый Структура("Номенклатура, Сумма, Сопоставлено");
ПараметрыПоиска.Сопоставлено = Ложь;
Для Каждого СтрПлюс Из ТЗ Цикл
Если СтрПлюс.Сопоставлено Или СтрПлюс.Сумма <= 0 Тогда
Продолжить;
КонецЕсли;
// Ищем парную строку с тем же ключом и строго противоположным знаком
ПараметрыПоиска.Номенклатура = СтрПлюс.Номенклатура;
ПараметрыПоиска.Сумма = -СтрПлюс.Сумма;
НайденныеМинусы = ТЗ.НайтиСтроки(ПараметрыПоиска);
Если НайденныеМинусы.Количество() > 0 Тогда
// Берем первую найденную встречную запись и помечаем обе строки
СтрМинус = НайденныеМинусы[0];
СтрПлюс.Сопоставлено = Истина;
СтрМинус.Сопоставлено = Истина;
КонецЕсли;
КонецЦикла;
// Удаляем все строки, нашедшие пару
СтрокиКУдалению = ТЗ.НайтиСтроки(Новый Структура("Сопоставлено", Истина));
Для Каждого Стр Из СтрокиКУдалению Цикл
ТЗ.Удалить(Стр);
КонецЦикла;
ТЗ.Колонки.Удалить("Сопоставлено");
Использование ТЗ.Индексы.Добавить() критически важно: без индекса метод НайтиСтроки() сканирует таблицу линейно, что превращает алгоритм в квадратичную сложность $O(N^2)$ и приводит к зависанию программы при количестве строк более нескольких тысяч.
При решении задач очистки таблиц от взаимоисключающих значений придерживаемся следующих правил:
ТЗ.Свернуть() с удалением строк с нулевым значением через обратный цикл, либо секцию ИМЕЮЩИЕ СУММА(...) <> 0 в запросе.НайтиСтроки() с обязательным предварительным индексированием таблицы.Окр(), чтобы избежать ошибок, связанных с представлением вещественных чисел в оперативной памяти.