Разработчики в среде 1С часто сталкиваются с ситуацией, когда фильтрация внутри параметров виртуальной таблицы работает не так, как ожидается. Классический пример — попытка отсечь уволенных сотрудников, наложив условие на реквизит или ресурс ДатаУвольнения непосредственно внутри параметров виртуальной таблицы СрезПоследних. Запрос при этом возвращает абсолютно всех сотрудников, игнорируя условия, хотя перенос этого же фильтра в секцию ГДЕ мгновенно решает проблему.
В этой статье мы подробно разберем, почему так происходит, заглянем во внутренние механизмы работы платформы 1С и СУБД, а также научимся правильно оптимизировать такие запросы без потери производительности.
Представим стандартную задачу: нам необходимо выгрузить список действующих сотрудников на определенную дату. Для этого мы пишем запрос к периодическому регистру сведений КадровыеСведения. Чтобы отсечь тех, кто уже уволен, мы накладываем условие: дата увольнения должна быть либо пустой (сотрудник работает), либо больше или равна дате нашего отчета (сотрудник увольняется в будущем).
Попытка реализовать это «оптимально» (внутри параметров виртуальной таблицы) выглядит следующим образом:
ВЫБРАТЬ
КадровыеСведенияСрезПоследних.Сотрудник,
КадровыеСведенияСрезПоследних.ДатаУвольнения
ИЗ
РегистрСведений.КадровыеСведения.СрезПоследних(
&Дата,
Фирма = &Фирма
И СтатусРаботы = ЗНАЧЕНИЕ(Справочник.СтатусыРаботы.ОсновноеМестоРаботы)
И (ДатаУвольнения = ДАТАВРЕМЯ(1, 1, 1, 0, 0, 0) ИЛИ ДатаУвольнения >= &Дата)
) КАК КадровыеСведенияСрезПоследних
При выполнении этого запроса система выдает всех сотрудников, включая давно уволенных! При этом у уволенных сотрудников поле ДатаУвольнения отображается как пустое (01.01.0001), хотя в физической таблице регистра там явно записана конкретная дата увольнения.
Однако, стоит нам переписать запрос и вынести фильтр в секцию ГДЕ:
ВЫБРАТЬ
КадровыеСведенияСрезПоследних.Сотрудник,
КадровыеСведенияСрезПоследних.ДатаУвольнения
ИЗ
РегистрСведений.КадровыеСведения.СрезПоследних(
&Дата,
Фирма = &Фирма
И СтатусРаботы = ЗНАЧЕНИЕ(Справочник.СтатусыРаботы.ОсновноеМестоРаботы)
) КАК КадровыеСведенияСрезПоследних
ГДЕ
(КадровыеСведенияСрезПоследних.ДатаУвольнения = ДАТАВРЕМЯ(1, 1, 1, 0, 0, 0)
ИЛИ КадровыеСведенияСрезПоследних.ДатаУвольнения >= &Дата)
Как всё начинает работать абсолютно корректно. Выясним причину такого поведения системы.
Чтобы понять логику платформы, проанализируем, как именно накладываются условия в параметрах виртуальной таблицы и в секции ГДЕ.
Представим историю записей в регистре для одного сотрудника — Иванова И.И.:
ДатаУвольнения = 01.01.0001 (пусто).ДатаУвольнения = 15.05.2023.Мы хотим получить срез на дату &Дата = 01.06.2023.
Когда мы помещаем условие в параметры СрезПоследних(), СУБД выполняет фильтрацию строк физической таблицы ДО того, как начнет вычислять максимальный (последний) период для каждого сотрудника. Это ключевой момент!
Давайте пошагово проследим за логикой СУБД:
(ДатаУвольнения = 01.01.0001 ИЛИ ДатаУвольнения >= 01.06.2023).ДатаУвольнения равна 01.01.0001. Она остается в наборе данных.В итоге уволенный сотрудник Иванов отображается в отчете как работающий. Логика запроса полностью нарушена.
В этом случае СУБД действует принципиально иначе:
ГДЕ: проверяет, равна ли дата увольнения 15.05.2023 пустой дате или больше ли она 01.06.2023. Условие не выполняется.Для того чтобы избегать подобных логических ошибок при проектировании запросов, запомним простое правило распределения условий:
Сотрудник, Организация, Подразделение). Это позволяет СУБД мгновенно отсечь ненужные ветки индексов.ДатаУвольнения, Оклад, ВидЗанятости), если нам нужно проанализировать именно актуальное состояние данных.В реальных задачах мы редко получаем данные только из одного регистра. Зачастую к таблице сотрудников необходимо присоединить множество других таблиц через ЛЕВОЕ СОЕДИНЕНИЕ. Разработчик резонно спросит: «Если я наложу условие в секции ГДЕ, то СУБД сначала выберет вообще всех сотрудников (включая уволенных), соединит их с другими таблицами, и только в конце отфильтрует результат. Это же крайне неоптимально!»
Действительно, выполнять левое соединение к избыточному набору данных — плохая практика, особенно в СУБД PostgreSQL, где планировщик запросов может ошибиться со стратегией соединения. Чтобы решить эту проблему красиво и максимально производительно, рассмотрим правильный паттерн проектирования запросов с использованием Временных Таблиц.
Разберем это решение по шагам:
ГДЕ, и сразу же поместим этот результат во временную таблицу. На этом этапе мы получим чистый, максимально легкий список только реально работающих сотрудников.Посмотрим на пример готового кода:
// ШАГ 1: Выбираем только действующих сотрудников во временную таблицу
ВЫБРАТЬ
КадровыеСведенияСрезПоследних.Сотрудник КАК Сотрудник,
КадровыеСведенияСрезПоследних.Фирма КАК Фирма
ПОМЕСТИТЬ ВТ_ДействующиеСотрудники
ИЗ
РегистрСведений.КадровыеСведения.СрезПоследних(
&Дата,
Фирма = &Фирма
И СтатусРаботы = ЗНАЧЕНИЕ(Справочник.СтатусыРаботы.ОсновноеМестоРаботы)
) КАК КадровыеСведенияСрезПоследних
ГДЕ
(КадровыеСведенияСрезПоследних.ДатаУвольнения = ДАТАВРЕМЯ(1, 1, 1, 0, 0, 0)
ИЛИ КадровыеСведенияСрезПоследних.ДатаУвольнения >= &Дата)
ИНДЕКСИРОВАТЬ ПО
Сотрудник
;
// ШАГ 2: Соединяем отфильтрованный список сотрудников с другими данными
ВЫБРАТЬ
Действующие.Сотрудник КАК Сотрудник,
ТекущиеТелефоны.НомерТелефона КАК НомерТелефона,
ТекущиеАдреса.Адрес КАК Адрес
ИЗ
ВТ_ДействующиеСотрудники КАК Действующие
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КонтактнаяИнформация КАК ТекущиеТелефоны
ПО Действующие.Сотрудник = ТекущиеТелефоны.Объект
И ТекущиеТелефоны.Тип = ЗНАЧЕНИЕ(Перечисление.ТипыКонтактнойИнформации.Телефон)
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КонтактнаяИнформация КАК ТекущиеАдреса
ПО Действующие.Сотрудник = ТекущиеАдреса.Объект
И ТекущиеАдреса.Тип = ЗНАЧЕНИЕ(Перечисление.ТипыКонтактнойИнформации.Адрес)
При таком подходе СУБД отработает запрос молниеносно. Временная таблица ВТ_ДействующиеСотрудники будет содержать только целевые записи, а построение индекса по полю Сотрудник гарантирует мгновенное выполнение последующих левых соединений без избыточной нагрузки на сервер базы данных.