При разработке сложных отчетов и обработок в системе 1С:Предприятие часто возникает задача: ограничить выборку данных динамическим периодом, который вычисляется на лету в этом же запросе. Например, сначала мы выбираем определенный набор документов или движений по какому-то специфическому фильтру (не по дате), а в следующем пакете нам необходимо получить связанные данные (курсы валют, цены, остатки или другие документы) строго в диапазоне дат «от самого первого до самого последнего» элемента из первого пакета.
Естественным желанием разработчика в этом случае является написание интуитивно понятной конструкции с использованием вложенных запросов в условиях фильтрации. Однако при попытке реализовать это напрямую мы сталкиваемся с жесткими ограничениями платформы.
Попробуем проанализировать, почему стандартная попытка написать условие фильтрации напрямую приводит к синтаксической ошибке. Предположим, мы написали следующий код:
ГДЕ
Док.Дата МЕЖДУ
(ВЫБРАТЬ МИНИМУМ(П1.Период) ИЗ ВТ_Пакет1 КАК П1)
И
(ВЫБРАТЬ МАКСИМУМ(П1.Период) ИЗ ВТ_Пакет1 КАК П1)
При попытке сохранить или выполнить такой запрос платформа 1С выдаст ошибку компиляции: «Ожидается "&"». Это связано с тем, что в языке запросов 1С существует синтаксическое ограничение: подзапросы нельзя использовать в качестве операндов для операторов сравнения (таких как >, <, >=, <=), а также внутри оператора МЕЖДУ.
Парсер языка запросов 1С в этих конструкциях ожидает увидеть только:
'00010101').&).Использование вложенных запросов в 1С разрешено только внутри оператора проверки на вхождение В (...), а также в секции ИЗ в качестве источника данных. Во всех остальных местах использование подзапросов вызовет ошибку синтаксиса. Давайте разберем по шагам, как красиво и эффективно обойти это ограничение без выгрузки данных во встроенный язык.
Самым простым, производительным и читаемым решением является предварительное вычисление границ периода во временную таблицу, состоящую ровно из одной строки. Затем мы просто связываем эту таблицу с нашей основной выборкой.
Рассмотрим этот алгоритм детально. Сначала мы берем исходный пакет данных (назовем его ВТ_ИсходныеДанные) и получаем из него минимальную и максимальную даты:
// ПАКЕТ 1: Вычисляем границы периода
ВЫБРАТЬ
МИНИМУМ(Исходные.Период) КАК МинДата,
МАКСИМУМ(Исходные.Период) КАК МаксДата
ПОМЕСТИТЬ ВТ_ГраницыПериода
ИЗ
ВТ_ИсходныеДанные КАК Исходные
;
// ПАКЕТ 2: Основной запрос с фильтрацией по периоду
ВЫБРАТЬ
ДокументыПродаж.Ссылка КАК ДокументСсылка,
ДокументыПродаж.Дата КАК ДатаДокумента
ИЗ
Документ.РеализацияТоваровУслуг КАК ДокументыПродаж
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ГраницыПериода КАК Границы
ПО ДокументыПродаж.Дата МЕЖДУ Границы.MинДата И Границы.МаксДата
Обратите внимание на то, как СУБД выполняет этот запрос. Поскольку временная таблица ВТ_ГраницыПериода не имеет группировок по разрезам, она гарантированно содержит **ровно одну строку** (или будет пустой, если исходных данных не было). При выполнении ВНУТРЕННЕГО СОЕДИНЕНИЯ СУБД легко сопоставит каждую строку таблицы документов с этой единственной строкой границ, не удваивая количество записей и эффективно используя индексы по полю Дата.
В языке запросов 1С можно связать таблицы без явного указания условий связи в секции ПО. Такой прием называется декартовым произведением. Мы можем перенести все условия фильтрации в стандартную секцию ГДЕ. Давайте посмотрим, как выглядит этот вариант:
ВЫБРАТЬ
ДокументыПродаж.Ссылка КАК ДокументСсылка,
ДокументыПродаж.Дата КАК ДатаДокумента
ИЗ
Документ.РеализацияТоваровУслуг КАК ДокументыПродаж,
ВТ_ГраницыПериода КАК Границы
ГДЕ
ДокументыПродаж.Дата МЕЖДУ Границы.МинДата И Границы.МаксДата
Этот способ абсолютно эквивалентен первому по производительности. Так как в таблице ВТ_ГраницыПериода находится только одна запись, оптимизатор СУБД автоматически преобразует такое перечисление таблиц в классическое соединение INNER JOIN. Вы можете использовать любой из этих двух вариантов в зависимости от стандартов разработки, принятых в вашей компании.
Рассмотрим альтернативный вариант, который упоминался разработчиками на форумах. Он заключается в создании двух раздельных временных таблиц: одна будет хранить только минимальную дату, а вторая — только максимальную. Для получения этих дат используется конструкция ВЫБРАТЬ ПЕРВЫЕ 1 с соответствующей сортировкой.
// Получаем минимальную дату
ВЫБРАТЬ ПЕРВЫЕ 1
Исходные.Период КАК МинПериод
ПОМЕСТИТЬ ВТ_Минимум
ИЗ
ВТ_ИсходныеДанные КАК Исходные
УПОРЯДОЧИТЬ ПО
Исходные.Период АСЦ
;
// Получаем максимальную дату
ВЫБРАТЬ ПЕРВЫЕ 1
Исходные.Период КАК МаксПериод
ПОМЕСТИТЬ ВТ_Максимум
ИЗ
ВТ_ИсходныеДанные КАК Исходные
УПОРЯДОЧИТЬ ПО
Исходные.Период УБЫВ
;
// Соединяем с основной таблицей
ВЫБРАТЬ
Док.Ссылка КАК Ссылка
ИЗ
Документ.РеализацияТоваровУслуг КАК Док
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Минимум КАК Т_Мин ПО Док.Дата >= Т_Мин.МинПериод
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Максимум КАК Т_Макс ПО Док.Дата <= Т_Макс.МаксПериод
Хотя данный метод является полностью рабочим, он считается более громоздким. Нам приходится создавать три пакета запроса вместо двух и дважды сортировать исходную таблицу данных. Сортировка больших объемов данных во временных таблицах — операция ресурсоемкая, поэтому первый способ с использованием функций МИНИМУМ и МАКСИМУМ является более предпочтительным.
Это самый важный архитектурный нюанс. Что делать, если нам нужно ограничить выборку не из физической таблицы документов, а из виртуальной таблицы регистра (например, получить обороты или остатки из РегистрНакопления.Продажи.Обороты)?
Критическое ограничение платформы: передать динамически вычисленные поля из временной таблицы напрямую в параметры виртуальной таблицы (такие как НачалоПериода или КонецПериода) внутри одного запроса **невозможно**. Эти параметры рассчитываются СУБД строго на этапе подготовки плана запроса, и они принимают только константы или готовые значения параметров (переданные через УстановитьПараметр()).
Давайте проанализируем три возможных пути решения этой задачи.
Если запрос пишется в общем или менеджерском модуле, лучшим решением с точки зрения производительности и стабильности будет разделение логики на два простых шага:
ЗапросГраниц = Новый Запрос;
ЗапросГраниц.Текст =
"ВЫБРАТЬ
| МИНИМУМ(Исходные.Период) КАК МинДата,
| МАКСИМУМ(Исходные.Период) КАК МаксДата
|ИЗ
| РегистрСведений.НекиеДанные КАК Исходные";
Выборка = ЗапросГраниц.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
МинДата = Выборка.МинДата;
МаксДата = Выборка.МаксДата;
КонецЕсли;
ВторойЗапрос = Новый Запрос;
ВторойЗапрос.Текст =
"ВЫБРАТЬ
| Продажи.Номенклатура,
| Продажи.КоличествоОборот
|ИЗ
| РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода) КАК Продажи";
ВторойЗапрос.УстановитьПараметр("НачалоПериода", МинДата);
ВторойЗапрос.УстановитьПараметр("КонецПериода", МаксДата);
Результат = ВторойЗапрос.Выполнить();
Этот подход гарантирует, что СУБД построит идеальный план запроса, прочитает только нужный диапазон индексов и отработает максимально быстро.
Если задача решается в отчете или динамическом списке на СКД, платформа предоставляет встроенный механизм связей наборов данных:
&НачалоПериода и &КонецПериода.Технически вы можете запустить виртуальную таблицу без указания параметров периода (то есть за всю историю базы данных), а затем соединить ее с временной таблицей границ:
// КРАЙНЕ НЕОПТИМАЛЬНЫЙ ВАРИАНТ! НЕ ИСПОЛЬЗОВАТЬ НА БОЛЬШИХ БАЗАХ!
ВЫБРАТЬ
Продажи.Номенклатура,
Продажи.КоличествоОборот
ИЗ
РегистрНакопления.Продажи.Обороты КАК Продажи
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ГраницыПериода КАК Границы
ПО Продажи.Период МЕЖДУ Границы.МинДата И Границы.МаксДата
Почему так делать категорически нельзя? Если в вашем регистре накопления хранятся миллионы записей за несколько лет, то при обращении к виртуальной таблице без параметров периода платформа 1С попытается рассчитать итоги и обороты за всю историю существования базы. СУБД потратит огромное количество оперативной памяти и времени, что приведет к зависанию системы или ошибке превышения времени ожидания (таймауту). Всегда ограничивайте виртуальные таблицы параметрами на этапе их вызова!
Для физических таблиц (документы, справочники, реальные таблицы регистров) идеальным решением является вычисление минимальной и максимальной дат в отдельную временную таблицу ВТ_ГраницыПериода из одной строки с последующим использованием ВНУТРЕННЕГО СОЕДИНЕНИЯ по условию МЕЖДУ. Для виртуальных таблиц наиболее надежным решением является разделение логики на два последовательных запроса во встроенном языке 1С с передачей вычисленных дат через параметры.