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