Как вычислить разность между двумя таблицами значений в 1С

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

В практике разработчика 1С часто возникает задача сравнения двух наборов данных. Типичный пример: у нас есть Таблица 1 с исходными данными (например, плановые показатели или товары в корзине) и Таблица 2 (фактические остатки или уже оформленные позиции). Нам нужно получить Таблицу 3, которая будет содержать разницу (ТЗ1 - ТЗ2 = ТЗ3). Рассмотрим подробнее несколько способов реализации этой задачи, проанализируем их преимущества и возможные подводные камни. Если расчет разницы требуется для контроля итогов между базами после миграции или доработок, используется программа для автоматической сверки проводок.

Способ 1. Использование запроса с левым соединением

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

Важно помнить, что при использовании ЛЕВОЕ СОЕДИНЕНИЕ мы можем столкнуться с ситуацией, когда товара нет во второй таблице. В этом случае в колонке второй таблицы будет значение 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 для обеих сторон.

Способ 2. Объединение и группировка (универсальный метод)

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

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


Запрос.Текст = 
"ВЫБРАТЬ
|	Выборка.Товар КАК Товар,
|	СУММА(Выборка.Количество) КАК Количество
|ИЗ
|	(ВЫБРАТЬ
|		Т1.Товар КАК Товар,
|		Т1.Количество КАК Количество
|	ИЗ
|		ВТ_Таблица1 КАК Т1
|	
|	ОБЪЕДИНИТЬ ВСЕ
|	
|	ВЫБРАТЬ
|		Т2.Товар,
|		-Т2.Количество
|	ИЗ
|		ВТ_Таблица2 КАК Т2) КАК Выборка
|
|СГРУППИРОВАТЬ ПО
|	Выборка.Товар";

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

Способ 3. Программный метод с использованием «Свернуть»

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

  1. Создаем копию первой таблицы (будущий результат).
  2. Добавляем в нее все строки из второй таблицы.
  3. Для строк из второй таблицы инвертируем знак числа (умножаем количество на -1).
  4. Вызываем метод Свернуть() по ключевым полям.
  5. Удаляем строки, в которых итоговое значение стало равным 0.

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


// ТЗ1 - исходная, ТЗ2 - вычитаемая
ТЗ_Разница = ТЗ1.Скопировать();

Для Каждого СтрокаТЗ2 Из ТЗ2 Цикл
    НоваяСтрока = ТЗ_Разница.Добавить();
    ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаТЗ2);
    НоваяСтрока.Количество = -СтрокаТЗ2.Количество;
КонецЦикла;

// Группируем по товару, суммируем количество
ТЗ_Разница.Свернуть("Товар", "Количество");

// Очистка нулевых итогов
Индекс = ТЗ_Разница.Количество() - 1;
Пока Индекс >= 0 Цикл
    Если ТЗ_Разница[Индекс].Количество = 0 Тогда
        ТЗ_Разница.Удалить(Индекс);
    КонецЕсли;
    Индекс = Индекс - 1;
КонецЦикла;

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

Способ 4. Высокопроизводительный поиск через «Соответствие» (Map)

Если таблицы содержат десятки тысяч строк, методы Найти() или НайтиСтроки() в цикле будут работать крайне медленно (сложность O(n*m)). В таких случаях эффективнее использовать объект Соответствие, который работает по принципу хеш-таблицы.

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


// Заполняем соответствие ключами из второй таблицы
КартаОстатков = Новый Соответствие;
Для Каждого Стр Из ТЗ2 Цикл
    КартаОстатков.Вставить(Стр.Товар, Стр.Количество);
КонецЦикла;

// Обходим первую таблицу и вычитаем
Для Каждого Стр Из ТЗ1 Цикл
    Остаток = КартаОстатков[Стр.Товар];
    Если Остаток <> Неопределено Тогда
        Стр.Количество = Стр.Количество - Остаток;
    КонецЕсли;
КонецЦикла;

Этот метод значительно ускоряет обработку данных в оперативной памяти без обращения к СУБД.

Работа с внешними источниками (ADO Recordset)

Иногда данные для сравнения приходят из внешних систем через 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();
    КонецЦикла;
    
    Возврат тзДанные;
КонецФункции

Подходы Библиотеки Стандартных Подсистем (БСП)

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

Рекомендации по выбору метода

Проанализировав ситуацию, можно сделать следующие выводы:

Помните о типах данных: при сравнении ссылочных типов (Номенклатура, Склады) запрос работает корректно, но если в таблицах содержатся объекты (ДокументОбъект), их нужно сначала привести к ссылочному типу или строковому представлению.

← На главную