Почему не работает условие на пустую дату в параметрах виртуальной таблицы СрезПоследних?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

Разработчики в среде 1С часто сталкиваются с ситуацией, когда фильтрация внутри параметров виртуальной таблицы работает не так, как ожидается. Классический пример — попытка отсечь уволенных сотрудников, наложив условие на реквизит или ресурс ДатаУвольнения непосредственно внутри параметров виртуальной таблицы СрезПоследних. Запрос при этом возвращает абсолютно всех сотрудников, игнорируя условия, хотя перенос этого же фильтра в секцию ГДЕ мгновенно решает проблему.

В этой статье мы подробно разберем, почему так происходит, заглянем во внутренние механизмы работы платформы 1С и СУБД, а также научимся правильно оптимизировать такие запросы без потери производительности.

В чем суть проблемы? Посмотрим на исходный запрос

Представим стандартную задачу: нам необходимо выгрузить список действующих сотрудников на определенную дату. Для этого мы пишем запрос к периодическому регистру сведений КадровыеСведения. Чтобы отсечь тех, кто уже уволен, мы накладываем условие: дата увольнения должна быть либо пустой (сотрудник работает), либо больше или равна дате нашего отчета (сотрудник увольняется в будущем).

Попытка реализовать это «оптимально» (внутри параметров виртуальной таблицы) выглядит следующим образом:


ВЫБРАТЬ
    КадровыеСведенияСрезПоследних.Сотрудник,
    КадровыеСведенияСрезПоследних.ДатаУвольнения
ИЗ
    РегистрСведений.КадровыеСведения.СрезПоследних(
            &Дата,
            Фирма = &Фирма 
            И СтатусРаботы = ЗНАЧЕНИЕ(Справочник.СтатусыРаботы.ОсновноеМестоРаботы)
            И (ДатаУвольнения = ДАТАВРЕМЯ(1, 1, 1, 0, 0, 0) ИЛИ ДатаУвольнения >= &Дата)
    ) КАК КадровыеСведенияСрезПоследних

При выполнении этого запроса система выдает всех сотрудников, включая давно уволенных! При этом у уволенных сотрудников поле ДатаУвольнения отображается как пустое (01.01.0001), хотя в физической таблице регистра там явно записана конкретная дата увольнения.

Однако, стоит нам переписать запрос и вынести фильтр в секцию ГДЕ:


ВЫБРАТЬ
    КадровыеСведенияСрезПоследних.Сотрудник,
    КадровыеСведенияСрезПоследних.ДатаУвольнения
ИЗ
    РегистрСведений.КадровыеСведения.СрезПоследних(
            &Дата,
            Фирма = &Фирма 
            И СтатусРаботы = ЗНАЧЕНИЕ(Справочник.СтатусыРаботы.ОсновноеМестоРаботы)
    ) КАК КадровыеСведенияСрезПоследних
ГДЕ
    (КадровыеСведенияСрезПоследних.ДатаУвольнения = ДАТАВРЕМЯ(1, 1, 1, 0, 0, 0) 
    ИЛИ КадровыеСведенияСрезПоследних.ДатаУвольнения >= &Дата)

Как всё начинает работать абсолютно корректно. Выясним причину такого поведения системы.

Физический смысл работы СрезПоследних: как СУБД строит виртуальную таблицу

Чтобы понять логику платформы, проанализируем, как именно накладываются условия в параметрах виртуальной таблицы и в секции ГДЕ.

Представим историю записей в регистре для одного сотрудника — Иванова И.И.:

  1. 01.01.2020 — Прием на работу. Ресурс ДатаУвольнения = 01.01.0001 (пусто).
  2. 15.05.2023 — Увольнение. Ресурс ДатаУвольнения = 15.05.2023.

Мы хотим получить срез на дату &Дата = 01.06.2023.

Вариант 1: Условие наложено в параметрах виртуальной таблицы

Когда мы помещаем условие в параметры СрезПоследних(), СУБД выполняет фильтрацию строк физической таблицы ДО того, как начнет вычислять максимальный (последний) период для каждого сотрудника. Это ключевой момент!

Давайте пошагово проследим за логикой СУБД:

  1. СУБД берет физическую таблицу регистра и начинает проверять каждую запись Иванова на соответствие условию: (ДатаУвольнения = 01.01.0001 ИЛИ ДатаУвольнения >= 01.06.2023).
  2. Запись №2 (от 15.05.2023 с датой увольнения 15.05.2023) не проходит этот фильтр, так как 15.05.2023 не равно пустой дате и меньше, чем 01.06.2023. СУБД исключает эту строку из выборки.
  3. Запись №1 (от 01.01.2020 с пустой датой увольнения) успешно проходит фильтр, так как её ДатаУвольнения равна 01.01.0001. Она остается в наборе данных.
  4. Теперь СУБД приступает к построению «среза последних» по тем записям, которые остались. Для Иванова осталась всего одна запись — №1 от 01.01.2020. Она автоматически признается «самой последней актуальной» на дату 01.06.2023.
  5. СУБД возвращает Иванова в результат запроса с пустой датой увольнения.

В итоге уволенный сотрудник Иванов отображается в отчете как работающий. Логика запроса полностью нарушена.

Вариант 2: Условие наложено в секции ГДЕ

В этом случае СУБД действует принципиально иначе:

  1. Сначала СУБД находит действительно последнюю запись для Иванова на дату 01.06.2023 без учета каких-либо фильтров по ресурсам. Это запись №2 от 15.05.2023 (где указана реальная дата увольнения 15.05.2023).
  2. Затем СУБД формирует итоговую выборку среза. На этом этапе Иванов представлен одной-единственной актуальной строкой с датой увольнения 15.05.2023.
  3. После этого СУБД накладывает фильтр из секции ГДЕ: проверяет, равна ли дата увольнения 15.05.2023 пустой дате или больше ли она 01.06.2023. Условие не выполняется.
  4. Иванов полностью исключается из результата запроса. Всё работает правильно!

Золотое правило работы с виртуальными таблицами

Для того чтобы избегать подобных логических ошибок при проектировании запросов, запомним простое правило распределения условий:

Как оптимизировать запрос при левом соединении?

В реальных задачах мы редко получаем данные только из одного регистра. Зачастую к таблице сотрудников необходимо присоединить множество других таблиц через ЛЕВОЕ СОЕДИНЕНИЕ. Разработчик резонно спросит: «Если я наложу условие в секции ГДЕ, то СУБД сначала выберет вообще всех сотрудников (включая уволенных), соединит их с другими таблицами, и только в конце отфильтрует результат. Это же крайне неоптимально!»

Действительно, выполнять левое соединение к избыточному набору данных — плохая практика, особенно в СУБД PostgreSQL, где планировщик запросов может ошибиться со стратегией соединения. Чтобы решить эту проблему красиво и максимально производительно, рассмотрим правильный паттерн проектирования запросов с использованием Временных Таблиц.

Разберем это решение по шагам:

  1. Сначала получим срез последних по кадровым сведениям, применив отбор по ресурсу в секции ГДЕ, и сразу же поместим этот результат во временную таблицу. На этом этапе мы получим чистый, максимально легкий список только реально работающих сотрудников.
  2. Во втором пакете запроса будем выполнять левые соединения уже к этой легкой временной таблице.

Посмотрим на пример готового кода:


// ШАГ 1: Выбираем только действующих сотрудников во временную таблицу
ВЫБРАТЬ
    КадровыеСведенияСрезПоследних.Сотрудник КАК Сотрудник,
    КадровыеСведенияСрезПоследних.Фирма КАК Фирма
ПОМЕСТИТЬ ВТ_ДействующиеСотрудники
ИЗ
    РегистрСведений.КадровыеСведения.СрезПоследних(
            &Дата,
            Фирма = &Фирма 
            И СтатусРаботы = ЗНАЧЕНИЕ(Справочник.СтатусыРаботы.ОсновноеМестоРаботы)
    ) КАК КадровыеСведенияСрезПоследних
ГДЕ
    (КадровыеСведенияСрезПоследних.ДатаУвольнения = ДАТАВРЕМЯ(1, 1, 1, 0, 0, 0) 
    ИЛИ КадровыеСведенияСрезПоследних.ДатаУвольнения >= &Дата)

ИНДЕКСИРОВАТЬ ПО
    Сотрудник
;

// ШАГ 2: Соединяем отфильтрованный список сотрудников с другими данными
ВЫБРАТЬ
    Действующие.Сотрудник КАК Сотрудник,
    ТекущиеТелефоны.НомерТелефона КАК НомерТелефона,
    ТекущиеАдреса.Адрес КАК Адрес
ИЗ
    ВТ_ДействующиеСотрудники КАК Действующие
    
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КонтактнаяИнформация КАК ТекущиеТелефоны
        ПО Действующие.Сотрудник = ТекущиеТелефоны.Объект
        И ТекущиеТелефоны.Тип = ЗНАЧЕНИЕ(Перечисление.ТипыКонтактнойИнформации.Телефон)
        
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КонтактнаяИнформация КАК ТекущиеАдреса
        ПО Действующие.Сотрудник = ТекущиеАдреса.Объект
        И ТекущиеАдреса.Тип = ЗНАЧЕНИЕ(Перечисление.ТипыКонтактнойИнформации.Адрес)

При таком подходе СУБД отработает запрос молниеносно. Временная таблица ВТ_ДействующиеСотрудники будет содержать только целевые записи, а построение индекса по полю Сотрудник гарантирует мгновенное выполнение последующих левых соединений без избыточной нагрузки на сервер базы данных.

← На главную