В практике разработчика 1С часто возникает задача сравнения двух наборов данных. Типичный пример: у нас есть Таблица 1 с исходными данными (например, плановые показатели или товары в корзине) и Таблица 2 (фактические остатки или уже оформленные позиции). Нам нужно получить Таблицу 3, которая будет содержать разницу (ТЗ1 - ТЗ2 = ТЗ3). Рассмотрим подробнее несколько способов реализации этой задачи, проанализируем их преимущества и возможные подводные камни. Если расчет разницы требуется для контроля итогов между базами после миграции или доработок, используется программа для автоматической сверки проводок.
Это самый очевидный метод, который часто первым приходит на ум. Мы выгружаем обе таблицы в менеджер временных таблиц и соединяем их по ключевому полю (например, Номенклатура). Разберем по шагам, как это реализовать правильно.
Важно помнить, что при использовании ЛЕВОЕ СОЕДИНЕНИЕ мы можем столкнуться с ситуацией, когда товара нет во второй таблице. В этом случае в колонке второй таблицы будет значение NULL. Чтобы математическая операция вычитания сработала корректно, необходимо использовать функцию ЕСТЬNULL.
Проанализируем пример кода:
// Предположим, ТЗ1 и ТЗ2 уже заполнены и имеют колонки "Товар" и "Количество"
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
// Помещаем первую таблицу во временную
Запрос.Текст = "ВЫБРАТЬ * ПОМЕСТИТЬ ВТ_Таблица1 ИЗ &ТЗ1 КАК ТЗ1";
Запрос.УстановитьПараметр("ТЗ1", ТЗ1);
Запрос.Выполнить();
// Помещаем вторую таблицу во временную
Запрос.Текст = "ВЫБРАТЬ * ПОМЕСТИТЬ ВТ_Таблица2 ИЗ &ТЗ2 КАК ТЗ2";
Запрос.УстановитьПараметр("ТЗ2", ТЗ2);
Запрос.Выполнить();
// Выполняем вычитание
Запрос.Текст =
"ВЫБРАТЬ
| Т1.Товар КАК Товар,
| Т1.Количество - ЕСТЬNULL(Т2.Количество, 0) КАК Остаток
|ИЗ
| ВТ_Таблица1 КАК Т1
| ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Таблица2 КАК Т2
| ПО Т1.Товар = Т2.Товар";
ТЗ_Результат = Запрос.Выполнить().Выгрузить();
Важное замечание: если мы используем левое соединение, мы получим только те товары, которые были в первой таблице. Если во второй таблице есть товар, которого нет в первой, он будет проигнорирован. Если ваша задача требует учета новых позиций из второй таблицы, следует использовать ПОЛНОЕ СОЕДИНЕНИЕ и обрабатывать NULL для обеих сторон.
Этот способ считается более надежным и производительным, особенно если нужно найти именно разность ресурсов. Мы объединяем две таблицы в одну, при этом показатели второй таблицы берем с отрицательным знаком, после чего схлопываем результат с помощью группировки.
Выясним причину популярности этого метода: он исключает дублирование строк и корректно обрабатывает ситуации, когда ключи есть только в одной из таблиц. Посмотрим на пример запроса:
Запрос.Текст =
"ВЫБРАТЬ
| Выборка.Товар КАК Товар,
| СУММА(Выборка.Количество) КАК Количество
|ИЗ
| (ВЫБРАТЬ
| Т1.Товар КАК Товар,
| Т1.Количество КАК Количество
| ИЗ
| ВТ_Таблица1 КАК Т1
|
| ОБЪЕДИНИТЬ ВСЕ
|
| ВЫБРАТЬ
| Т2.Товар,
| -Т2.Количество
| ИЗ
| ВТ_Таблица2 КАК Т2) КАК Выборка
|
|СГРУППИРОВАТЬ ПО
| Выборка.Товар";
Такой подход позволяет за один проход получить итоговую разность. Если итоговое Количество равно нулю, значит, значения в обеих таблицах были идентичны.
Если вы не хотите использовать механизм запросов (например, таблицы небольшие или данные находятся в памяти в виде объектов), можно воспользоваться встроенным методом объекта ТаблицаЗначений. Алгоритм действий следующий:
Свернуть() по ключевым полям.Рассмотрим реализацию:
// ТЗ1 - исходная, ТЗ2 - вычитаемая
ТЗ_Разница = ТЗ1.Скопировать();
Для Каждого СтрокаТЗ2 Из ТЗ2 Цикл
НоваяСтрока = ТЗ_Разница.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаТЗ2);
НоваяСтрока.Количество = -СтрокаТЗ2.Количество;
КонецЦикла;
// Группируем по товару, суммируем количество
ТЗ_Разница.Свернуть("Товар", "Количество");
// Очистка нулевых итогов
Индекс = ТЗ_Разница.Количество() - 1;
Пока Индекс >= 0 Цикл
Если ТЗ_Разница[Индекс].Количество = 0 Тогда
ТЗ_Разница.Удалить(Индекс);
КонецЕсли;
Индекс = Индекс - 1;
КонецЦикла;
Нюанс: этот метод прост, но требует внимательности при работе с типами данных. Убедитесь, что колонки в обеих таблицах имеют числовой тип и одинаковые имена.
Если таблицы содержат десятки тысяч строк, методы Найти() или НайтиСтроки() в цикле будут работать крайне медленно (сложность O(n*m)). В таких случаях эффективнее использовать объект Соответствие, который работает по принципу хеш-таблицы.
Разберем этот алгоритм:
// Заполняем соответствие ключами из второй таблицы
КартаОстатков = Новый Соответствие;
Для Каждого Стр Из ТЗ2 Цикл
КартаОстатков.Вставить(Стр.Товар, Стр.Количество);
КонецЦикла;
// Обходим первую таблицу и вычитаем
Для Каждого Стр Из ТЗ1 Цикл
Остаток = КартаОстатков[Стр.Товар];
Если Остаток <> Неопределено Тогда
Стр.Количество = Стр.Количество - Остаток;
КонецЕсли;
КонецЦикла;
Этот метод значительно ускоряет обработку данных в оперативной памяти без обращения к СУБД.
Иногда данные для сравнения приходят из внешних систем через ADO. В таких случаях сначала необходимо преобразовать Recordset в ТаблицуЗначений, чтобы применить вышеописанные методы. Посмотрим, как это сделать универсально, учитывая типы данных:
&НаСервере
Функция ADODB_Recordset_в_ТаблицуЗначений(adoRecordset)
тзДанные = Новый ТаблицаЗначений;
// Определяем колонки на основе типов ADO
Для Каждого fldX из adoRecordset.Fields Цикл
ТипКолонкиТЗ = Неопределено;
// Упрощенная логика определения типов:
Если fldX.Type = 3 Тогда // adInteger
ТипКолонкиТЗ = Новый ОписаниеТипов("Число");
ИначеЕсли fldX.Type = 202 Тогда // adVarWChar (текст)
ТипКолонкиТЗ = Новый ОписаниеТипов("Строка");
КонецЕсли;
тзДанные.Колонки.Добавить(fldX.Name, ТипКолонкиТЗ);
КонецЦикла;
// Заполняем данными
Пока не adoRecordset.EOF Цикл
НоваяСтрокаТЗ = тзДанные.Добавить();
Для Каждого ПолеТЗ из тзДанные.Колонки Цикл
НоваяСтрокаТЗ[ПолеТЗ.Имя] = adoRecordset.fields(ПолеТЗ.Имя).value;
КонецЦикла;
adoRecordset.MoveNext();
КонецЦикла;
Возврат тзДанные;
КонецФункции
Если в вашей конфигурации установлена БСП, стоит проверить наличие готовых инструментов в модуле ОбщегоНазначения. Например, функция ОбщегоНазначения.ТаблицыЗначенийСовпадают() позволяет быстро проверить идентичность двух таблиц перед тем, как начинать сложные вычисления разности. Хотя в базовой поставке БСП нет прямой функции "вычесть одну таблицу из другой", механизмы сравнения коллекций часто используются в подсистеме обновления информационной базы для сопоставления настроек "было" и "стало".
Проанализировав ситуацию, можно сделать следующие выводы:
ТаблицыЗначений в памяти, метод ОБЪЕДИНИТЬ ВСЕ в запросе является самым универсальным.ТЗ.Индексы.Добавить("Товар").Помните о типах данных: при сравнении ссылочных типов (Номенклатура, Склады) запрос работает корректно, но если в таблицах содержатся объекты (ДокументОбъект), их нужно сначала привести к ссылочному типу или строковому представлению.