При разработке учетных механизмов в 1С часто возникает задача скорректировать плановые или нормативные показатели на фактически совершенные операции. Типичный пример — расчет доступного остатка блюд в столовой или ресторане, когда из первоначального лимита меню требуется вычесть уже оформленные за день заказы.
Разберем основные подходы к решению этой задачи на уровне языка запросов 1С: через объединение таблиц с агрегацией и через соединение временных таблиц.
Наиболее лаконичный и универсальный способ математического вычитания двух наборов данных заключается в использовании конструкции ОБЪЕДИНИТЬ ВСЕ с последующей группировкой по ключевым полям и суммированием. В ветке запроса, содержащей вычитаемые данные, числовое поле умножается на -1 (или берется с отрицательным знаком).
Рассмотрим базовую схему запроса:
ВЫБРАТЬ
ВТ_Итог.Блюдо КАК Блюдо,
СУММА(ВТ_Итог.Количество) КАК ОстатокВНаличии
ИЗ
(ВЫБРАТЬ
Меню.Номенклатура КАК Блюдо,
Меню.Количество КАК Количество
ИЗ
Документ.КХВМеню.Обед КАК Меню
ГДЕ
Меню.Ссылка.Дата МЕЖДУ &ДатаНач И &ДатаКон
И Меню.Ссылка.Проведен
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
Заказы.Номенклатура,
-Заказы.КоличествоОборот
ИЗ
РегистрНакопления.КУБЗаявки.Обороты(&ДатаНач, &ДатаКон, , Склад = &Склад И Площадка = &НомерМеню) КАК Заказы) КАК ВТ_Итог
СГРУППИРОВАТЬ ПО
ВТ_Итог.Блюдо
Проанализируем важные технические нюансы этого способа:
"", СУБД вернет ошибку несовместимости типов или сформирует составной тип. Пустые числовые поля всегда следует инициализировать нулем: 0 КАК ВНаличии или ВЫРАЗИТЬ(0 КАК ЧИСЛО(15, 3)).НАЧАЛОПЕРИОДА(Документ.Ссылка.Дата, ДЕНЬ) в секции ГДЕ блокирует использование индекса по полю Дата, приводя к полному сканированию таблицы (Table Scan). Всегда передавайте границы периода параметрами через условие МЕЖДУ &ДатаНач И &ДатаКон.Для высоконагруженных баз данных и сложных запросов со срезами цен и реквизитами номенклатуры предпочтительнее использовать пакетный запрос с временными таблицами. Это позволяет СУБД построить оптимальный план выполнения и применить операцию Hash Match или Merge Join.
Посмотрим на реализацию пакетного запроса по шагам:
ЛЕВОЕ СОЕДИНЕНИЕ и вычисляем разницу.
ВЫБРАТЬ
Меню.Номенклатура КАК Номенклатура,
Меню.Номенклатура.Родитель КАК Группа,
Меню.Количество КАК ЛимитМеню,
Цены.Цена КАК Цена
ПОМЕСТИТЬ ВТ_ЛимитыМеню
ИЗ
Документ.КХВМеню.Обед КАК Меню
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&ДатаКон, ТипЦен = &ТипЦен) КАК Цены
ПО Меню.Номенклатура = Цены.Номенклатура
ГДЕ
Меню.Ссылка.Дата МЕЖДУ &ДатаНач И &ДатаКон
И Меню.Ссылка.Проведен
ИНДЕКСИРОВАТЬ ПО
Номенклатура
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Заказы.Номенклатура КАК Номенклатура,
СУММА(Заказы.КоличествоОборот) КАК Заказано
ПОМЕСТИТЬ ВТ_Заказано
ИЗ
РегистрНакопления.КУБЗаявки.Обороты(&ДатаНач, &ДатаКон, , Склад = &Склад И Площадка = &НомерМеню) КАК Заказы
СГРУППИРОВАТЬ ПО
Заказы.Номенклатура
ИНДЕКСИРОВАТЬ ПО
Номенклатура
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Лимиты.Номенклатура КАК Блюдо,
Лимиты.Группа КАК Группа,
Лимиты.Цена КАК Цена,
Лимиты.ЛимитМеню КАК ВсегоВМеню,
ЕСТЬNULL(Заказы.Заказано, 0) КАК ЗаказаноКоличество,
Лимиты.ЛимитМеню - ЕСТЬNULL(Заказы.Заказано, 0) КАК ОстатокВНаличии
ИЗ
ВТ_ЛимитыМеню КАК Лимиты
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Заказано КАК Заказы
ПО Лимиты.Номенклатура = Заказы.Номенклатура
Критически важная деталь: При выполнении левого соединения для позиций, по которым еще не было заказов за день, поле Заказы.Заказано вернет значение NULL. Любая арифметическая операция с NULL (например, 10 - NULL) в языке запросов 1С возвращает NULL, в результате чего строка пропадет из итоговой выборки доступных блюд. Обязательно используйте функцию ЕСТЬNULL(Поле, 0) для всех вычитаемых колонок.
Если запросы выполняются в несвязанных модулях конфигурации или выгружаются в клиентские формы, расчет разницы можно произвести в коде 1С с предварительной индексацией:
ТаблицаМеню = ЗапросМеню.Выполнить().Выгрузить();
ТаблицаЗаказов = ЗапросЗаказов.Выполнить().Выгрузить();
// Индексируем таблицу заказов для мгновенного поиска
ТаблицаЗаказов.Индексы.Добавить("Номенклатура");
Для Каждого СтрМеню Из ТаблицаМеню Цикл
НайденныйЗаказ = ТаблицаЗаказов.Найти(СтрМеню.Номенклатура, "Номенклатура");
КоличествоЗаказано = ?(НайденныйЗаказ = Неопределено, 0, НайденныйЗаказ.Количество);
СтрМеню.ВНаличии = СтрМеню.Лимит - КоличествоЗаказано;
КонецЦикла;
Подводя итог: для простых расчетов отлично подходит объединение с инверсией знака, а для построения сложных отчетов с множеством аналитик надежнее использовать пакетные запросы с временными таблицами и обязательной проверкой на ЕСТЬNULL.