Как в 1С получить срез последних значений на несколько дат?

Программист
← На главную

В разработке на платформе 1С часто возникает задача: получить актуальные данные из периодического регистра сведений не на одну дату, а сразу на целый список дат. Например, для формирования табеля учета рабочего времени нужно знать должность и ставку сотрудника на каждый день месяца. Стандартная виртуальная таблица СрезПоследних принимает только один параметр-дату, что делает ее неудобной для таких задач. Рассмотрим, как решить эту проблему с помощью пакетного запроса.

Проанализируем типичную ситуацию. У нас есть регистр с датами (например, производственный календарь) и регистр сведений, хранящий историю должностей сотрудников. Цель — для каждой даты из календаря получить последнюю актуальную должность сотрудника.

Неправильный подход и его анализ

Начинающий разработчик может попытаться решить задачу одним запросом с левым соединением и группировкой. Посмотрим на пример такого запроса, который приводит к некорректному результату.

Исходная проблема: есть список дат из регистра ДанныеПроизводственногоКалендаря. Нужно для каждой даты из этого списка получить последнюю по времени запись о должности сотрудника из регистра СведенияОСотрудниках.

Первоначальный вариант запроса:


ВЫБРАТЬ
    ДанныеПроизводственногоКалендаря.Дата КАК Дата,
    СведенияОСотрудниках.Сотрудник КАК Сотрудник,
    СведенияОСотрудниках.Должность КАК Должность,
    МАКСИМУМ(СведенияОСотрудниках.Период) КАК Период
ИЗ
    РегистрСведений.ДанныеПроизводственногоКалендаря КАК ДанныеПроизводственногоКалендаря
        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СведенияОСотрудниках КАК СведенияОСотрудниках
        ПО (СведенияОСотрудниках.Период <= ДанныеПроизводственногоКалендаря.Дата)
СГРУППИРОВАТЬ ПО
    ДанныеПроизводственногоКалендаря.Дата,
    СведенияОСотрудниках.Сотрудник,
    СведенияОСотрудниках.Должность

УПОРЯДОЧИТЬ ПО
    Дата

Почему этот запрос не работает как ожидалось?

Причина кроется в поле Должность в секции СГРУППИРОВАТЬ ПО. Из-за этого группировка происходит по уникальным комбинациям (Дата, Сотрудник, Должность). В результате на одну и ту же дату для одного сотрудника запрос выведет несколько строк — по одной для каждой должности, которую он когда-либо занимал до этой даты. Агрегатная функция МАКСИМУМ(Период) в этом случае просто вернет дату записи для каждой из этих должностей, а не единственную самую последнюю.

Правильное решение: пакетный запрос с временной таблицей

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

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

Разберем этот метод по шагам.

Шаг 1: Находим максимальные периоды

Первый запрос пакета предназначен исключительно для определения "точек среза". Мы соединяем наш список дат с регистром сведений по условию, что период записи должен быть не больше даты среза. Затем группируем результат по дате и измерениям регистра (Сотрудник), вычисляя максимальный период.

Ключевой момент: в группировку не должны входить ресурсы регистра (Должность, Ставка и т.д.).


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

СГРУППИРОВАТЬ ПО
    ДанныеПроизводственногоКалендаря.Дата,
    СведенияОСотрудниках.Сотрудник

После выполнения этого запроса мы получим временную таблицу ВТ_ПериодыСреза примерно такого вида:

| Дата | Сотрудник | Период | |---|---|---| | 01.01.2023 | Иванов И.И. | 15.12.2022 | | 02.01.2023 | Иванов И.И. | 15.12.2022 | | ... | ... | ... | | 10.02.2023 | Иванов И.И. | 01.02.2023 | | 11.02.2023 | Иванов И.И. | 01.02.2023 |

Как видно, до 10 февраля для сотрудника Иванова последней записью была та, что от 15.12.2022. А начиная с 10 февраля (предположим, 1 февраля был перевод на новую должность), последней актуальной записью становится та, что от 01.02.2023.

Шаг 2: Получаем данные по найденным периодам

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


ВЫБРАТЬ
    ВТ.Дата КАК Дата,
    ВТ.Сотрудник КАК Сотрудник,
    СведенияОСотрудниках.Должность КАК Должность,
    СведенияОСотрудниках.Ставка КАК Ставка // и другие необходимые ресурсы
ИЗ
    ВТ_ПериодыСреза КАК ВТ
        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СведенияОСотрудниках КАК СведенияОСотрудниках
        ПО ВТ.Сотрудник = СведенияОСотрудниках.Сотрудник
            И ВТ.Период = СведенияОСотрудниках.Период

Это соединение гарантированно вернет нам одну строку для каждой строки из временной таблицы, содержащую именно те данные, которые были актуальны на найденный максимальный период.

Разбор типичной ошибки при реализации

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


// Неправильный первый запрос
ВЫБРАТЬ
    ДанныеПроизводственногоКалендаря.Дата КАК Дата,
    СведенияОСотрудниках.Сотрудник КАК Сотрудник,
    МАКСИМУМ(СведенияОСотрудниках.Период) КАК Период
ПОМЕСТИТЬ ВТ
ИЗ
    РегистрСведений.ДанныеПроизводственногоКалендаря КАК ДанныеПроизводственногоКалендаря
        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СведенияОСотрудниках КАК СведенияОСотрудниках
        ПО (СведенияОСотрудниках.Период <= ДанныеПроизводственногоКалендаря.Дата)
СГРУППИРОВАТЬ ПО
    ДанныеПроизводственногоКалендаря.Дата,
    СведенияОСотрудниках.Сотрудник,
    СведенияОСотрудниках.Должность; // <-- ОШИБКА ЗДЕСЬ

////////////////////////////////////////////////////////////////////////
// Второй запрос, который получает неверные данные из ВТ
ВЫБРАТЬ
    СведенияОСотрудниках.Должность КАК Должность,
    ВТ.Дата КАК Дата,
    ВТ.Сотрудник КАК Сотрудник
ИЗ
    ВТ КАК ВТ
        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СведенияОСотрудниках КАК СведенияОСотрудниках
        ПО СведенияОСотрудниках.Сотрудник = ВТ.Сотрудник
            И СведенияОСотрудниках.Период = ВТ.Период

Ошибка осталась прежней: в первом запросе в секции СГРУППИРОВАТЬ ПО все еще присутствует поле СведенияОСотрудниках.Должность. Это сводит на нет всю идею первого шага. Временная таблица `ВТ` будет содержать избыточные строки, и итоговый результат не изменится. Запомните: на первом этапе мы ищем только периоды, поэтому группировка должна быть только по измерениям регистра и полям из таблицы дат.

Производительность и альтернативы

Предложенный метод с временной таблицей является "золотым стандартом" и хорошо показывает себя на больших объемах данных. Однако стоит знать и о других нюансах.

В заключение, метод с пакетным запросом и разделением логики на два шага является надежным, производительным и легко читаемым способом получения среза последних на массив дат. Главное — четко понимать, какую задачу решает каждый из подзапросов, и не допускать ошибок в полях группировки.

← На главную