При работе с запросами в системе 1С часто возникает необходимость сравнивать данные, полученные из одной части запроса (например, из временной таблицы), с данными из другой части (основной таблицы или другого источника). Особенно часто такие задачи появляются при обработке дат, когда важно учесть только календарный день, без времени. Рассмотрим подробно, как правильно сравнивать даты, полученные из временной таблицы, в основном запросе 1С, проанализируем распространенные ошибки и предложим эффективные решения.
Представим ситуацию, когда нам необходимо получить "крайнюю дату поступления наличных" на основе даты реализации и ответственного, а затем использовать эту дату для фильтрации другого документа, например, "Расходный кассовый ордер". Изначальный подход может выглядеть следующим образом:
ВЫБРАТЬ
НАЧАЛОПЕРИОДА(ДОБАВИТЬКДАТЕ(РеализацияТоваровУслуг.Дата, ДЕНЬ, -2), ДЕНЬ) КАК КрайняяДатаПоступленияНаличных,
РеализацияТоваровУслуг.Ответственный
ПОМЕСТИТЬ ВТ_Реализация
ИЗ
Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг
ГДЕ
РеализацияТоваровУслуг.Ссылка = &ТекущийОбъект
;
В этом блоке мы создаем временную таблицу ВТ_Реализация, которая содержит вычисленную "крайнюю дату поступления наличных" и ответственного за реализацию. Далее автор попытался использовать эту временную таблицу в основном запросе:
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ ПЕРВЫЕ 1
РасходныйКассовыйОрдерРасшифровкаПлатежа.Ссылка
ИЗ
Документ.РасходныйКассовыйОрдер.РасшифровкаПлатежа КАК РасходныйКассовыйОрдерРасшифровкаПлатежа
ГДЕ
РасходныйКассовыйОрдерРасшифровкаПлатежа.СтатьяДвиженияДенежныхСредств.итд_ТорговыйПредставитель В
(ВЫБРАТЬ
ВТ_Реализация.Ответственный
ИЗ
ВТ_Реализация)
И НАЧАЛОПЕРИОДА(РасходныйКассовыйОрдерРасшифровкаПлатежа.Ссылка.Дата, ДЕНЬ) >
(ВЫБРАТЬ ПЕРВЫЕ 1
ВТ_Реализация.КрайняяДатаПоступленияНаличных
ИЗ
ВТ_Реализация)
УПОРЯДОЧИТЬ ПО
РасходныйКассовыйОрдерРасшифровкаПлатежа.Ссылка.Дата УБЫВ
ГДЕРассмотрим, почему начальный подход с подзапросом в секции ГДЕ вызывал ошибки при сравнении по оператору "больше" (>) и почему использование оператора В не давало ожидаемого результата.
Операторы сравнения, такие как >, <, >=, <=, ожидают в правой части выражения одно скалярное значение. Если подзапрос в правой части возвращает более одного значения (даже если он теоретически должен возвращать одно из-за ВЫБРАТЬ ПЕРВЫЕ 1, но на практике или из-за особенностей оптимизации запроса это не гарантируется в каждом случае), или если он возвращает некорректный тип данных, возникает ошибка. В данном контексте, даже когда подзапрос возвращает одно значение, система запросов 1С может интерпретировать его как потенциальный набор данных, а не как однозначное скалярное значение для прямого сравнения. Это приводит к синтаксическим или логическим ошибкам.
Оператор В (IN) предназначен для проверки вхождения значения в список. То есть, он сравнивает левую часть с каждым значением, возвращаемым подзапросом. Если нам нужно проверить, что дата больше определенной даты, оператор В не подходит для этой цели. Он лишь скажет, присутствует ли точное значение даты в списке дат из временной таблицы, что не соответствует условию "больше".
В 1С даты могут содержать не только день, месяц и год, но и время. Если нам необходимо сравнивать только календарные дни без учета времени, функция НАЧАЛОПЕРИОДА(<Дата>, ДЕНЬ) является ключевой. Она обнуляет временную часть даты до 00:00:00 для начала указанного периода (в данном случае, дня). Это позволяет избежать некорректных сравнений, когда, например, '01.01.2023 10:00:00' сравнивается с '01.01.2023 11:00:00' и одно из них оказывается "больше" другого, хотя календарно это тот же день. В нашем примере она используется верно для обеих сравниваемых дат.
Наиболее эффективным и типовым способом для сравнения и объединения данных из основной и временной таблиц в 1С-запросах является использование ВНУТРЕННЕЕ СОЕДИНЕНИЕ (INNER JOIN). Этот подход позволяет нам явно указать условия, по которым записи из двух таблиц должны быть сопоставлены.
Рассмотрим рабочее решение, которое автор темы успешно применил:
ВЫБРАТЬ
НАЧАЛОПЕРИОДА(ДОБАВИТЬКДАТЕ(РеализацияТоваровУслуг.Дата, ДЕНЬ, -20), ДЕНЬ) КАК КрайняяДатаПоступленияНаличных,
РеализацияТоваровУслуг.Ответственный
ПОМЕСТИТЬ ВТ_Реализация
ИЗ
Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг
ГДЕ
РеализацияТоваровУслуг.Ссылка = &ТекущийОбъект
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ ПЕРВЫЕ 1
РасходныйКассовыйОрдерРасшифровкаПлатежа.Ссылка
ИЗ
Документ.РасходныйКассовыйОрдер.РасшифровкаПлатежа КАК РасходныйКассовыйОрдерРасшифровкаПлатежа
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Реализация КАК ВТ_Реализация
ПО РасходныйКассовыйОрдерРасшифровкаПлатежа.Ссылка.Дата > ВТ_Реализация.КрайняяДатаПоступленияНаличных
И РасходныйКассовыйОрдерРасшифровкаПлатежа.СтатьяДвиженияДенежныхСредств.итд_ТорговыйПредставитель = ВТ_Реализация.Ответственный
УПОРЯДОЧИТЬ ПО
РасходныйКассовыйОрдерРасшифровкаПлатежа.Ссылка.Дата УБЫВ
Разберем это решение по шагам:
Первый блок запроса остается практически без изменений, за исключением того, что автор изменил смещение даты с -2 на -20 дней, что не влияет на принцип сравнения. Мы по-прежнему получаем КрайняяДатаПоступленияНаличных и Ответственный для конкретного документа реализации.
ВЫБРАТЬ
НАЧАЛОПЕРИОДА(ДОБАВИТЬКДАТЕ(РеализацияТоваровУслуг.Дата, ДЕНЬ, -20), ДЕНЬ) КАК КрайняяДатаПоступленияНаличных,
РеализацияТоваровУслуг.Ответственный
ПОМЕСТИТЬ ВТ_Реализация
ИЗ
Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг
ГДЕ
РеализацияТоваровУслуг.Ссылка = &ТекущийОбъект
;
Во втором блоке запроса мы используем ВНУТРЕННЕЕ СОЕДИНЕНИЕ (INNER JOIN) между таблицей Документ.РасходныйКассовыйОрдер.РасшифровкаПлатежа и нашей временной таблицей ВТ_Реализация. Это позволяет нам сопоставлять записи из обеих таблиц на основе заданных условий.
...
ИЗ
Документ.РасходныйКассовыйОрдер.РасшифровкаПлатежа КАК РасходныйКассовыйОрдерРасшифровкаПлатежа
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Реализация КАК ВТ_Реализация
...
В секции ПО мы указываем условия, по которым должны быть сопоставлены записи. Здесь мы видим два ключевых условия, объединенных оператором И:
РасходныйКассовыйОрдерРасшифровкаПлатежа.Ссылка.Дата > ВТ_Реализация.КрайняяДатаПоступленияНаличных: Это главное условие, которое и требовалось автору. Мы напрямую сравниваем дату расходного кассового ордера (ПКО) с "крайней датой поступления наличных" из временной таблицы. Поскольку теперь мы работаем с двумя таблицами в соединении, каждая строка ПКО будет сравниваться со строками из ВТ_Реализация по этому условию. Это корректно и позволяет использовать оператор >. Обратите внимание, что для корректного сравнения по дням, для даты ПКО также следует использовать НАЧАЛОПЕРИОДА(РасходныйКассовыйОрдерРасшифровкаПлатежа.Ссылка.Дата, ДЕНЬ). Хотя в примере автора это явно не указано для даты ПКО, по хорошему стилю, если мы обрезали дату во временной таблице, то и сравниваемую дату следует приводить к такому же формату начала дня.РасходныйКассовыйОрдерРасшифровкаПлатежа.СтатьяДвиженияДенежныхСредств.итд_ТорговыйПредставитель = ВТ_Реализация.Ответственный: Это дополнительное условие, которое связывает записи по ответственному (торговому представителю). Это важно, чтобы сопоставлять оплаты с реализациями, за которые ответственен один и тот же сотрудник.
...
ПО РасходныйКассовыйОрдерРасшифровкаПлатежа.Ссылка.Дата > ВТ_Реализация.КрайняяДатаПоступленияНаличных
И РасходныйКассовыйОрдерРасшифровкаПлатежа.СтатьяДвиженияДенежныхСредств.итд_ТорговыйПредставитель = ВТ_Реализация.Ответственный
...
Запрос возвращает ссылку на первый попавшийся расходный кассовый ордер, удовлетворяющий условиям, отсортированный по убыванию даты ПКО.
Таким образом, ВНУТРЕННЕЕ СОЕДИНЕНИЕ является мощным и гибким инструментом для решения подобных задач, позволяя объединять и фильтровать данные из различных источников (включая временные таблицы) на основе сложных условий сравнения.
В сообщении 8 был поднят важный вопрос: "А теперь представь, что ПКО у тебя на сумму одной реализации, а реализаций больше. 1 раз заплатил, и все, бери товара сколько хошь?". Этот сценарий указывает на потенциальные сложности, когда один документ оплаты может быть связан с несколькими реализациями, или наоборот. Прямое ВНУТРЕННЕЕ СОЕДИНЕНИЕ, как показано выше, может привести к дублированию строк или некорректной агрегации, если это не учесть.
Рассмотрим подходы к решению этой задачи:
Если для сравнения нам нужна одна "КрайняяДатаПоступленияНаличных" для каждого "Ответственного" (или другого ключевого поля), то временную таблицу ВТ_Реализация можно предварительно сгруппировать и агрегировать даты. Например, если нам нужна самая ранняя или самая поздняя "крайняя дата" для каждого ответственного:
ВЫБРАТЬ
МАКСИМУМ(НАЧАЛОПЕРИОДА(ДОБАВИТЬКДАТЕ(РеализацияТоваровУслуг.Дата, ДЕНЬ, -20), ДЕНЬ)) КАК КрайняяДатаПоступленияНаличных,
РеализацияТоваровУслуг.Ответственный
ПОМЕСТИТЬ ВТ_Реализация
ИЗ
Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг
ГДЕ
РеализацияТоваровУслуг.Ссылка = &ТекущийОбъект // Или другое условие, если нужно получить агрегат по нескольким реализациям
СГРУППИРОВАТЬ ПО
РеализацияТоваровУслуг.Ответственный
;
Такой подход обеспечит наличие одного агрегированного значения даты для каждого ключа (в данном случае, Ответственного), что упростит дальнейшее соединение и предотвратит дублирование.
Если важно видеть все ПКО, даже те, которые по каким-то причинам не нашли соответствия во временной таблице по условию даты, можно использовать ЛЕВОЕ СОЕДИНЕНИЕ (LEFT JOIN). Однако, это не решит проблему "одна оплата, несколько реализаций" без дополнительной логики. Если мы хотим сопоставить ПКО с *любой* подходящей реализацией, то ВНУТРЕННЕЕ СОЕДИНЕНИЕ с указанными условиями часто подходит. Если же нужно более сложное сопоставление (например, "один ко многим" с учетом сумм), то потребуются другие механизмы, возможно, с использованием вложенных запросов или более сложной логики.
В некоторых случаях может потребоваться уточнить условия соединения в секции ПО или добавить дополнительные фильтры в секцию ГДЕ после соединения, чтобы правильно связать документы. Например, если ПКО также имеет ссылку на реализацию, можно добавить условие И РасходныйКассовыйОрдерРасшифровкаПлатежа.ДокументОснование = ВТ_Реализация.Ссылка (если такое поле существует). Это сделает соединение более точным.
Хотя ВНУТРЕННЕЕ СОЕДИНЕНИЕ часто является предпочтительным, иногда можно вернуться к подзапросам, если требуется получить одно агрегированное значение. Например, если мы хотим найти максимальную крайнюю дату поступления для всех реализаций, связанных с текущим объектом, и сравнить с ней:
ВЫБРАТЬ ПЕРВЫЕ 1
РасходныйКассовыйОрдерРасшифровкаПлатежа.Ссылка
ИЗ
Документ.РасходныйКассовыйОрдер.РасшифровкаПлатежа КАК РасходныйКассовыйОрдерРасшифровкаПлатежа
ГДЕ
НАЧАЛОПЕРИОДА(РасходныйКассовыйОрдерРасшифровкаПлатежа.Ссылка.Дата, ДЕНЬ) >
(ВЫБРАТЬ
МАКСИМУМ(НАЧАЛОПЕРИОДА(ДОБАВИТЬКДАТЕ(РеализацияТоваровУслуг.Дата, ДЕНЬ, -20), ДЕНЬ))
ИЗ
Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг
ГДЕ
РеализацияТоваровУслуг.Ссылка = &ТекущийОбъект)
И РасходныйКассовыйОрдерРасшифровкаПлатежа.СтатьяДвиженияДенежныхСредств.итд_ТорговыйПредставитель В
(ВЫБРАТЬ
РеализацияТоваровУслуг.Ответственный
ИЗ
Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг
ГДЕ
РеализацияТоваровУслуг.Ссылка = &ТекущийОбъект)
УПОРЯДОЧИТЬ ПО
РасходныйКассовыйОрдерРасшифровкаПлатежа.Ссылка.Дата УБЫВ
Здесь мы используем агрегатную функцию МАКСИМУМ внутри подзапроса, чтобы гарантировать возвращение одного скалярного значения, что позволяет использовать оператор >. Однако, для более сложных условий и множественных связей, соединения остаются более гибкими и часто более производительными.
Для эффективной работы с датами в запросах 1С, помимо уже рассмотренных НАЧАЛОПЕРИОДА и ДОБАВИТЬКДАТЕ, существуют и другие полезные функции:
НАЧАЛОПЕРИОДА(<Дата>, <ТипПериода>): Мы уже выяснили, что эта функция обнуляет временную часть даты до начала указанного периода (ДЕНЬ, НЕДЕЛЯ, МЕСЯЦ, КВАРТАЛ, ГОД). Крайне важна для корректного сравнения дат без учета времени.КОНЕЦПЕРИОДА(<Дата>, <ТипПериода>): Аналогична НАЧАЛОПЕРИОДА, но устанавливает дату на конец указанного периода (например, 23:59:59.999 для ДЕНЬ). Часто используется в связке с НАЧАЛОПЕРИОДА для формирования диапазона дат.ДОБАВИТЬКДАТЕ(<Дата>, <ТипДобавления>, <Количество>): Позволяет прибавить или вычесть (если Количество отрицательное) определенное количество единиц (СЕКУНДА, МИНУТА, ЧАС, ДЕНЬ, НЕДЕЛЯ, МЕСЯЦ, ГОД) к дате. Это было использовано в нашем примере для расчета КрайняяДатаПоступленияНаличных.РАЗНОСТЬДАТ(<Дата1>, <Дата2>, <ТипРазности>): Возвращает разницу между двумя датами в указанных единицах (СЕКУНДА, МИНУТА, ЧАС, ДЕНЬ, НЕДЕЛЯ, МЕСЯЦ, ГОД). Например, РАЗНОСТЬДАТ(ДатаНачала, ДатаОкончания, ДЕНЬ) покажет количество дней между датами.ГОД(<Дата>), МЕСЯЦ(<Дата>), ДЕНЬ(<Дата>), ЧАС(<Дата>), МИНУТА(<Дата>), СЕКУНДА(<Дата>): Позволяют извлечь соответствующую часть из значения даты.ДАТАВРЕМЯ(<Год>, <Месяц>, <День>, <Час>, <Минута>, <Секунда>): Позволяет создать значение даты и времени из отдельных числовых компонентов. Полезна, когда нужно сформировать дату из числовых полей или литералов.Используя эти функции, мы можем гибко манипулировать датами в запросах 1С, создавая сложные условия фильтрации и связывания данных.
Мы проанализировали проблему сравнения дат из временной таблицы в запросах 1С, выяснили причины ошибок при использовании подзапросов с операторами сравнения и пришли к выводу, что наиболее эффективным и правильным решением является применение `ВНУТРЕННЕГО СОЕДИНЕНИЯ`. Этот метод не только позволяет корректно сравнивать даты и другие поля, но и обеспечивает гибкость в построении условий связи между таблицами. Дополнительно мы рассмотрели, как обрабатывать более сложные сценарии, такие как "одна оплата, несколько реализаций", и ознакомились с другими полезными функциями для работы с датами в запросах 1С.
Всегда стремитесь использовать соединения таблиц вместо подзапросов в секции ГДЕ для фильтрации по условиям, если подзапрос потенциально может вернуть несколько значений или если условия сравнения требуют операторов, отличных от В.