При построении отчетов по заработной плате программисты часто сталкиваются со следующей задачей: необходимо вывести список сотрудников организации с детализацией по подразделениям и видам расчетов. При этом, если у конкретного сотрудника в выбранном периоде не было начислений по какому-то определенному виду расчета (например, отсутствовала премия или не начислялась доплата за ночные часы), данный вид расчета все равно должен присутствовать в строках отчета, но с нулевой суммой.
Попытки решить эту задачу «в лоб» через простое ЛЕВОЕ СОЕДИНЕНИЕ физической таблицы регистра с виртуальной таблицей оборотов обычно не приводят к успеху. В этой статье мы подробно разберем, почему возникает эта сложность, и рассмотрим три эффективных способа ее решения: с помощью декартова произведения в запросе, через механизмы Системы компоновки данных (СКД) и с использованием специализированного программного интерфейса (API) конфигурации ЗУП 3.1.
Рассмотрим типичную ошибку разработчика. В исходном запросе предпринимается попытка соединить таблицу уникальных видов расчетов с оборотами регистра накопления РегистрНакопления.НачисленияУдержанияПоСотрудникам по полю НачислениеУдержание. При этом накладывается отбор по сотрудникам или подразделениям в секции ГДЕ.
Такой подход некорректен по двум причинам:
Для правильного решения задачи нам необходимо сформировать полную координатную сетку (матрицу), где на пересечении каждого сотрудника и каждого вида начисления будет находиться соответствующая сумма (или 0, если данных в регистре нет).
Разберем по шагам, как построить правильный пакет запросов, который сформирует полную сетку всех возможных комбинаций сотрудников и видов расчетов. Для этого мы применим классический прием реляционных баз данных — декартово произведение (в терминах SQL — CROSS JOIN).
В языке запросов 1С декартово произведение получается крайне просто: достаточно перечислить две или более таблицы в секции ИЗ через запятую, не указывая при этом никаких условий связи в секции ПО или ГДЕ. СУБД автоматически соединит каждую строку первой таблицы с каждой строкой второй таблицы.
Рассмотрим пример реализации такого запроса:
// ШАГ 1. Получаем уникальный список сотрудников и их подразделений за период
ВЫБРАТЬ РАЗЛИЧНЫЕ
НачисленияУдержания.ФизическоеЛицо КАК ФизическоеЛицо,
НачисленияУдержания.Подразделение КАК Подразделение
ПОМЕСТИТЬ ВТ_Сотрудники
ИЗ
РегистрНакопления.НачисленияУдержанияПоСотрудникам КАК НачисленияУдержания
ГДЕ
НачисленияУдержания.Период МЕЖДУ НАЧАЛОПЕРИОДА(&Нач, ДЕНЬ) И КОНЕЦПЕРИОДА(&Кон, ДЕНЬ)
И (НачисленияУдержания.Подразделение В ИЕРАРХИИ (&Подразделение) ИЛИ НЕ &ОтборПоПодразделению)
И (НачисленияУдержания.ФизическоеЛицо = &ФизическоеЛицо ИЛИ НЕ &ОтборПоФизЛицу)
;
// ШАГ 2. Получаем уникальный список видов расчетов (начислений и удержаний)
ВЫБРАТЬ РАЗЛИЧНЫЕ
НачисленияУдержания.НачислениеУдержание КАК НачислениеУдержание,
НачисленияУдержания.ГруппаНачисленияУдержанияВыплаты КАК ГруппаНачисленияУдержанияВыплаты
ПОМЕСТИТЬ ВТ_Начисления
ИЗ
РегистрНакопления.НачисленияУдержанияПоСотрудникам КАК НачисленияУдержания
ГДЕ
НачисленияУдержания.Период МЕЖДУ НАЧАЛОПЕРИОДА(&Нач, ДЕНЬ) И КОНЕЦПЕРИОДА(&Кон, ДЕНЬ)
;
// ШАГ 3. Строим декартово произведение (Сетка: Сотрудник * Начисление)
ВЫБРАТЬ
ВТ_Сотрудники.ФизическоеЛицо КАК ФизическоеЛицо,
ВТ_Сотрудники.Подразделение КАК Подразделение,
ВТ_Начисления.НачислениеУдержание КАК НачислениеУдержание,
ВТ_Начисления.ГруппаНачисленияУдержанияВыплаты КАК ГруппаНачисленияУдержанияВыплаты
ПОМЕСТИТЬ ВТ_Сетка
ИЗ
ВТ_Сотрудники КАК ВТ_Сотрудники,
ВТ_Начисления КАК ВТ_Начисления
;
// ШАГ 4. Соединяем сетку с реальными оборотами регистра
ВЫБРАТЬ
ВТ_Сетка.Подразделение КАК Подразделение,
ВТ_Сетка.ФизическоеЛицо КАК ФизическоеЛицо,
ВТ_Сетка.ГруппаНачисленияУдержанияВыплаты КАК ГруппаНачисленияУдержанияВыплаты,
ВТ_Сетка.НачислениеУдержание КАК НачислениеУдержание,
ЕСТЬNULL(ОборотыРегистра.СуммаОборот, 0) КАК СуммаНУ
ИЗ
ВТ_Сетка КАК ВТ_Сетка
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.НачисленияУдержанияПоСотрудникам.Обороты(
НАЧАЛОПЕРИОДА(&Нач, ДЕНЬ),
КОНЕЦПЕРИОДА(&Кон, ДЕНЬ),
Авто,
(ФизическоеЛицо = &ФизическоеЛицо ИЛИ НЕ &ОтборПоФизЛицу)
И (Подразделение В ИЕРАРХИИ (&Подразделение) ИЛИ НЕ &ОтборПоПодразделению)
) КАК ОборотыРегистра
ПО ВТ_Сетка.ФизическоеЛицо = ОборотыРегистра.ФизическоеЛицо
И ВТ_Сетка.НачислениеУдержание = ОборотыРегистра.НачислениеУдержание
УПОРЯДОЧИТЬ ПО
Подразделение,
ФизическоеЛицо,
ГруппаНачисленияУдержанияВыплаты,
НачислениеУдержание
Преимущества данного подхода: Мы гарантированно получим строки по каждому сотруднику и каждому виду расчета, существовавшему в базе за этот период. Если у сотрудника не было конкретного начисления, функция ЕСТЬNULL вернет аккуратный 0 в колонке суммы.
Недостатки: На больших базах данных (например, тысячи сотрудников и десятки видов начислений) размер временной таблицы декартова произведения ВТ_Сетка может составить сотни тысяч строк, что приведет к замедлению работы сервера СУБД. Поэтому использовать чистое декартово произведение в запросах нужно осмотрительно.
Если конечной целью является вывод данных в виде печатной формы или отчета, то выполнять ресурсоемкие операции по перемножению таблиц в самом запросе не требуется. СКД предоставляет встроенный, оптимизированный на уровне платформы механизм для вывода пустых значений и кросс-таблиц.
Проанализируем алгоритм настройки отчета в СКД:
Подразделение и ФизическоеЛицо.НачислениеУдержание.NULL.NULL) в отчете отображался красивый ноль, перейдите в свойства выбранного ресурса на вкладке «Ресурсы» и укажите выражение расчета как: ЕСТЬNULL(Сумма(СуммаОборот), 0).СуммаОборот НЕ ЗАПОЛНЕНО, а в свойствах формата указываем текст: «0».Этот способ является наиболее предпочтительным с точки зрения быстродействия системы, так как всю работу по сборке матрицы берет на себя процессор компоновки данных 1С, оптимизируя запросы к базе данных.
В современных конфигурациях, работающих на платформе 1С:Предприятие 8.3 (таких как ЗУП 3.1, ERP или Комплексная автоматизация), прямое обращение к физическим таблицам регистров кадров и расчета крайне не рекомендуется. Это связано со сложной архитектурой учета периодов действия начислений, вытеснениями и частыми обновлениями метаданных со стороны фирмы «1С».
Вместо написания собственных сложных запросов к регистрам расчетов следует использовать готовый программный интерфейс общего назначения (API), предоставляемый разработчиками конфигурации. В ЗУП 3.1 для этих целей реализованы специальные функции в общих модулях.
Рассмотрим, как получить готовые временные таблицы кадровых данных и начислений сотрудников с помощью программного вызова:
// Создаем менеджер временных таблиц, который будет хранить результаты работы API
МенеджерВТ = Новый МенеджерВременныхТаблиц;
// 1. Получаем кадровые данные сотрудников (включая подразделения, должности, оклады)
КадровыеДанные = КадровыйУчет.СоздатьВТКадровыеДанныеСотрудников(
МенеджерВТ,
Истина,
"ФизическоеЛицо, Подразделение, Должность",
&Кон
);
// 2. Получаем плановые начисления сотрудников и показатели тарифных ставок
ПоказателиРасчетаЗарплатыКадры.СоздатьВТНачисленияСПоказателямиТарифныхСтавок(
МенеджерВТ,
Истина,
"Оклад, ТарифнаяСтавка",
&Нач,
&Кон
);
После вызова этих методов в вашем распоряжении (внутри объекта МенеджерВТ) появятся готовые, оптимизированные временные таблицы с кадровой историей сотрудников на нужные даты и их плановыми начислениями. Вам останется лишь написать результирующий запрос, который левым соединением свяжет эти готовые таблицы с оборотами расчетов. Такой подход полностью защищает вашу разработку от сбоев при последующих обновлениях конфигурации 1С.
Проанализируем еще одну критическую ошибку производительности, допущенную в исходных запросах темы форума:
ГДЕ
(НЕ &ОтборПоПодразделению ИЛИ НачисленияУдержанияПоСотрудникам.Подразделение В ИЕРАРХИИ (&Подразделение))
Использование конструкции логического ИЛИ совместно с параметром-флагом (булево &ОтборПоПодразделению) в секции фильтрации ГДЕ крайне пагубно сказывается на скорости выполнения запроса СУБД.
Оптимизатор запросов СУБД (будь то Microsoft SQL Server или PostgreSQL) при наличии условия ИЛИ в предложении WHERE часто не может построить эффективный план выполнения. Система отказывается от использования индексов по полям ФизическоеЛицо или Подразделение и переходит к полному сканированию таблиц (Table Scan / Index Scan), что приводит к зависанию отчетов на реальных рабочих базах данных.
Как правильно оптимизировать фильтрацию данных:
ГДЕ или в параметры виртуальных таблиц на лету, не создавая лишних логических ветвлений.
БазовыйТекст =
"ВЫБРАТЬ
| Обороты.ФизическоеЛицо,
| Обороты.СуммаОборот
|ИЗ
| РегистрНакопления.НачисленияУдержанияПоСотрудникам.Обороты(&Нач, &Кон, , ) КАК Обороты
|ГДЕ
| Истина";
Если ЗначениеЗаполнено(ВыбранноеПодразделение) Тогда
БазовыйТекст = БазовыйТекст + " И Обороты.Подразделение В ИЕРАРХИИ(&Подразделение)";
КонецЕсли;
Запрос.Текст = БазовыйТекст;
Запрос.УстановитьПараметр("Нач", ПериодНачала);
Запрос.УстановитьПараметр("Кон", ПериодОкончания);
Если ЗначениеЗаполнено(ВыбранноеПодразделение) Тогда
Запрос.УстановитьПараметр("Подразделение", ВыбранноеПодразделение);
КонецЕсли;
Благодаря этому приему СУБД всегда будет получать чистый, лаконичный запрос с точными условиями по индексированным полям, что обеспечит максимальное быстродействие вашей информационной системы.