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