При получении кадровых данных на определенную дату (например, актуальной должности и подразделения сотрудника) разработчики в конфигурациях «Управление производственным предприятием» (УПП 1.3) или «Зарплата и Управление Персоналом» (ЗУП 2.5) часто сталкиваются с неожиданным поведением запросов. Простой запрос к виртуальной таблице СрезПоследних регистра сведений РаботникиОрганизаций может возвращать дублирующиеся строки для одного и того же сотрудника или «намертво» завешивать систему при попытке наложить фильтр по списку работников. В этой статье мы подробно разберем причины возникновения таких проблем и рассмотрим наиболее эффективные способы их решения.
Для того чтобы понять природу возникновения дублей, проанализируем, как платформа 1С:Предприятие работает с виртуальной таблицей СрезПоследних. Механизм среза выбирает наиболее позднюю запись на указанную дату в разрезе каждой уникальной комбинации измерений регистра.
Если мы обратимся к структуре измерений регистра сведений РаботникиОрганизаций, то увидим, что помимо очевидного измерения Сотрудник, там присутствуют и другие поля, например:
ОрганизацияУдалитьФизлицо (устаревшее измерение, оставленное для обратной совместимости)УдалитьПриказ (устаревшее измерение)Если в процессе ведения учета или переноса данных у одного и того же сотрудника в более старых записях было заполнено техническое измерение УдалитьФизлицо, а в новых записях это поле осталось пустым, СУБД воспримет эти строки как разные комбинации измерений. В результате стандартный СрезПоследних вернет обе записи, что приведет к дублированию данных в отчете или выборке.
Частой ошибкой при попытке ограничить выборку списком сотрудников является использование вложенного запроса непосредственно во втором параметре виртуальной таблицы. Рассмотрим пример такого неоптимального кода:
ВЫБРАТЬ
РаботникиОрганизацийСрезПоследних.ПодразделениеОрганизации,
РаботникиОрганизацийСрезПоследних.Должность,
РаботникиОрганизацийСрезПоследних.Сотрудник
ИЗ
РегистрСведений.РаботникиОрганизаций.СрезПоследних(&Дата, Сотрудник В (ВЫБРАТЬ Сотрудник ИЗ СписокСотр)) КАК РаботникиОрганизацийСрезПоследних
Подобный синтаксис заставляет СУБД вычислять вложенный подзапрос для каждой записи физической таблицы регистра при расчете среза последних. Это полностью отключает использование индексов СУБД, переводит операцию в режим сканирования всей таблицы (Table Scan) и приводит к зависанию («глубокой задумчивости») системы или к ошибкам нехватки памяти (Out of Memory) на больших базах данных.
Рассмотрим несколько правильных подходов к решению этой задачи.
Если список сотрудников формируется в коде на встроенном языке (например, это массив ссылок или список значений), самым простым и производительным решением будет передать его напрямую в качестве параметра виртуальной таблицы. В этом случае СУБД построит оптимальный план выполнения с использованием индекса по измерению Сотрудник.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| РаботникиОрганизацийСрезПоследних.Сотрудник КАК Сотрудник,
| РаботникиОрганизацийСрезПоследних.ПодразделениеОрганизации КАК ПодразделениеОрганизации,
| РаботникиОрганизацийСрезПоследних.Должность КАК Должность
|ИЗ
| РегистрСведений.РаботникиОрганизаций.СрезПоследних(&Дата, Сотрудник В (&СписокСотрудников)) КАК РаботникиОрганизацийСрезПоследних";
Запрос.УстановитьПараметр("Дата", КонецДня(ТекущаяДата()));
Запрос.УстановитьПараметр("СписокСотрудников", МассивСотрудников);
ТаблицаРезультатов = Запрос.Выполнить().Выгрузить();
В случаях, когда список сотрудников формируется динамически внутри самого запроса, правильным архитектурным решением будет предварительное создание индексированной временной таблицы и ее последующее левое соединение с виртуальной таблицей среза последних. Обратите внимание, что параметры виртуальной таблицы при этом остаются пустыми или содержат только фильтр по организации.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| Работники.Ссылка КАК Сотрудник
|ПОМЕСТИТЬ ВТ_СписокСотрудников
|ИЗ
| Справочник.СотрудникиОрганизаций КАК Работники
|ГДЕ
| НЕ Работники.ПометкаУдаления
|
|ИНДЕКСИРОВАТЬ ПО
| Сотрудник
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_СписокСотрудников.Сотрудник КАК Сотрудник,
| КадровыеДанные.ПодразделениеОрганизации КАК ПодразделениеОрганизации,
| КадровыеДанные.Должность КАК Должность
|ИЗ
| ВТ_СписокСотрудников КАК ВТ_СписокСотрудников
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.РаботникиОрганизаций.СрезПоследних(&Дата, ) КАК КадровыеДанные
| ПО ВТ_СписокСотрудников.Сотрудник = КадровыеДанные.Сотрудник";
Запрос.УстановитьПараметр("Дата", КонецДня(ТекущаяДата()));
ТаблицаРезультатов = Запрос.Выполнить().Выгрузить();
Если в вашей базе данных структура регистра сведений нарушена («замусорена» старыми измерениями) и стандартный срез последних продолжает возвращать дублирующие строки на одного сотрудника, необходимо обойти использование виртуальной таблицы СрезПоследних. Для этого мы самостоятельно найдем максимальную дату записи (период) по каждому сотруднику на физическом уровне таблиц, а затем соединим результат с данными регистра.
Реализуем этот надежный алгоритм:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| РаботникиОрганизаций.Сотрудник КАК Сотрудник,
| МАКСИМУМ(РаботникиОрганизаций.Период) КАК Период
|ПОМЕСТИТЬ ВТ_МаксимальныеДаты
|ИЗ
| РегистрСведений.РаботникиОрганизаций КАК РаботникиОрганизаций
|ГДЕ
| РаботникиОрганизаций.Период <= &Дата
|ГРУППИРОВАТЬ ПО
| РаботникиОрганизаций.Сотрудник
|
|ИНДЕКСИРОВАТЬ ПО
| Сотрудник,
| Период
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_МаксимальныеДаты.Сотрудник КАК Сотрудник,
| РаботникиОрганизаций.ПодразделениеОрганизации КАК ПодразделениеОрганизации,
| РаботникиОрганизаций.Должность КАК Должность
|ИЗ
| ВТ_МаксимальныеДаты КАК ВТ_МаксимальныеДаты
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.РаботникиОрганизаций КАК РаботникиОрганизаций
| ПО ВТ_МаксимальныеДаты.Сотрудник = РаботникиОрганизаций.Сотрудник
| И ВТ_МаксимальныеДаты.Период = РаботникиОрганизаций.Период";
Запрос.УстановитьПараметр("Дата", КонецДня(ТекущаяДата()));
ТаблицаРезультатов = Запрос.Выполнить().Выгрузить();
Кадровые регистры сведений в типовых конфигурациях 1С обладают еще одной важной спецификой — поддержкой временных изменений (например, временный перевод сотрудника на другую должность). Для этого используются реквизиты завершения:
ПериодЗавершенияДолжностьЗавершенияПодразделениеОрганизацииЗавершенияЕсли сотрудника перевели на другую должность временно, в регистр пишется одна строка. По окончании этого периода платформа не создает новую строку автоматически. Поэтому при получении среза последних на дату, которая больше, чем ПериодЗавершения, мы обязаны вернуть старые (исходные) значения. В запросе это реализуется с помощью конструкции ВЫБОР:
ВЫБРАТЬ
РаботникиОрганизацийСрезПоследних.Сотрудник КАК Сотрудник,
ВЫБОР
КОГДА РаботникиОрганизацийСрезПоследних.ПериодЗавершения <> ДАТАВРЕМЯ(1, 1, 1)
И РаботникиОрганизацийСрезПоследних.ПериодЗавершения <= &Дата
ТОГДА РаботникиОрганизацийСрезПоследних.ДолжностьЗавершения
ИНАЧЕ РаботникиОрганизацийСрезПоследних.Должность
КОНЕЦ КАК Должность,
ВЫБОР
КОГДА РаботникиОрганизацийСрезПоследних.ПериодЗавершения <> ДАТАВРЕМЯ(1, 1, 1)
И РаботникиОрганизацийСрезПоследних.ПериодЗавершения <= &Дата
ТОГДА РаботникиОрганизацийСрезПоследних.ПодразделениеОрганизацииЗавершения
ИНАЧЕ РаботникиОрганизацийСрезПоследних.ПодразделениеОрганизации
КОНЕЦ КАК ПодразделениеОрганизации
ИЗ
РегистрСведений.РаботникиОрганизаций.СрезПоследних(&Дата, ) КАК РаботникиОрганизацийСрезПоследних
Чтобы гарантированно избежать ошибок ручного написания сложных кадровых срезов с учетом увольнений, совместительства и временных переводов, в типовых конфигурациях УПП 1.3 и ЗУП 2.5 рекомендуется использовать встроенные методы общих модулей. Разработчики предусмотрели готовый программный интерфейс.
Для получения актуальных данных достаточно вызвать процедуру общего модуля:
// Создаем структуру запрашиваемых полей
СтруктураПараметров = Новый Структура("КадровыеДанные", "ПодразделениеОрганизации, Должность");
// Подготавливаем таблицу сотрудников (должна содержать колонку "Сотрудник")
ТаблицаСотрудников = Новый ТаблицаЗначений;
ТаблицаСотрудников.Колонки.Добавить("Сотрудник", Новый ОписаниеТипов("СправочникСсылка.СотрудникиОрганизаций"));
// Заполняем таблицу сотрудниками
// ...
// Вызываем типовой метод получения кадровых данных во временную таблицу
ПроцедурыУправленияПерсоналом.ПодготовитьВТКадровыеДанныеСотрудниковОрганизации(ТаблицаСотрудников, СтруктураПараметров, Истина, ДатаСреза);
Этот метод самостоятельно сформирует оптимизированный запрос, исключит уволенных сотрудников, учтет периоды завершения временных перемещений и вернет корректную временную таблицу, готовую для дальнейшего использования в ваших алгоритмах.