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