При разработке отчетов в Системе Компоновки Данных (СКД) часто возникает задача подсчета уникальных объектов (например, сотрудников), у которых определенный показатель изменился за некоторый период. Казалось бы, простая задача, но на практике она может столкнуться с неожиданными трудностями, особенно при использовании условных выражений и агрегатных функций. Рассмотрим подробнее проблему, с которой столкнулся наш пользователь, и разберем эффективные решения.
Наш пользователь столкнулся с проблемой в отчете СКД: необходимо было подсчитать количество различных сотрудников, у которых значение "Грейда" до кадрового перевода и после него изменилось. Исходный подход заключался в создании двух временных таблиц: ВТ_До и ВТ_После, которые содержали данные о грейдах сотрудников на разные моменты времени (до и после предполагаемого кадрового перевода соответственно). Эти таблицы формировались на основе срезов регистров сведений РегистрСведений.ПлановыеНачисления.СрезПоследних и РегистрСведений.ЗначенияПериодическихПоказателейРасчетаЗарплатыСотрудников.СрезПоследних.
// Фрагмент формирования ВТ_После
ВЫБРАТЬ РАЗЛИЧНЫЕ
ЗначенияПериодическихПоказателейПосле.Показатель КАК ПоказательПосле,
ЗначенияПериодическихПоказателейПосле.Значение КАК ЗначениеПосле,
ПлановыеНачисленияПосле.Начисление КАК НачислениеПосле,
ПлановыеНачисленияПосле.Сотрудник КАК Сотрудник,
ПлановыеНачисленияПосле.Используется КАК Используется
ПОМЕСТИТЬ ВТ_После
ИЗ
РегистрСведений.ПлановыеНачисления.СрезПоследних(&ПериодПосле {(&ПериодНеИспользовать)}, Сотрудник = &Сотрудник) КАК ПлановыеНачисленияПосле
ЛЕВОЕ СОЕДИНЕНИЕ ПланВидовРасчета.Начисления.Показатели КАК НачисленияПоказатели
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ЗначенияПериодическихПоказателейРасчетаЗарплатыСотрудников.СрезПоследних(
&ПериодПосле {(&ПериодНеИспользовать)},
Сотрудник = &Сотрудник
И Показатель = &ПоказательГрейд) КАК ЗначенияПериодическихПоказателейПосле
ПО НачисленияПоказатели.Показатель = ЗначенияПериодическихПоказателейПосле.Показатель
ПО ПлановыеНачисленияПосле.Начисление = НачисленияПоказатели.Ссылка
ГДЕ
ЗначенияПериодическихПоказателейПосле.Значение <> 0
;
Аналогично создавалась таблица ВТ_До. Затем эти временные таблицы объединялись с помощью ОБЪЕДИНИТЬ ВСЕ, и в итоговом запросе формировалось поле ИзмененияГрейдов, которое должно было содержать ссылку на сотрудника, если его грейд изменился, или 0, если нет:
// Фрагмент из объединенного запроса, формирующий поле ИзмененияГрейдов
ВЫБРАТЬ
...
ВЫБОР
КОГДА ВТ_После.ЗначениеПосле <> ВТ_До.Значение
ТОГДА ВТ_После.Сотрудник
ИНАЧЕ 0
КОНЕЦ КАК ИзмененияГрейдов
ИЗ
ВТ_После КАК ВТ_После
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_До КАК ВТ_До
ПО ВТ_После.Сотрудник = ВТ_До.Сотрудник
И ВТ_После.ПоказательПосле = ВТ_До.Показатель
ГДЕ
ВТ_После.Используется
СГРУППИРОВАТЬ ПО
ВТ_После.Сотрудник,
ВТ_После.ПоказательПосле,
ВТ_После.ЗначениеПосле,
ВЫБОР
КОГДА ВТ_После.ЗначениеПосле <> ВТ_До.Значение
ТОГДА ВТ_После.Сотрудник
ИНАЧЕ 0
КОНЕЦ
Затем это поле ИзмененияГрейдов использовалось в ресурсе СКД с агрегатной функцией Количество(Различные). Проблема заключалась в том, что отчет местами работал некорректно, выдавая неверные значения.
Давайте разберем, почему исходный подход мог приводить к ошибкам. Центральная проблема кроется в выражении ВЫБОР КОГДА ВТ_После.ЗначениеПосле <> ВТ_До.Значение ТОГДА ВТ_После.Сотрудник ИНАЧЕ 0 КОНЕЦ и его дальнейшем использовании с функцией Количество(Различные).
Количество(Различные): Функция Количество(Различные) в 1С СКД имеет важное свойство: она по умолчанию игнорирует значения NULL. Это ключевой момент для нашего случая, поскольку мы хотим подсчитывать только тех сотрудников, у которых произошли изменения.
ИНАЧЕ 0 и несовместимостью типов: Поле ВТ_После.Сотрудник, как правило, является ссылкой на объект (например, на элемент справочника "Сотрудники"). В то же время, в ветке ИНАЧЕ нашего условного выражения возвращается число 0. Таким образом, поле ИзмененияГрейдов становится неоднородным по типу данных: оно может содержать либо ссылку на сотрудника, либо число 0. При агрегации или преобразовании данных СКД может попытаться привести все значения к одному типу. В этом процессе 0 (число) будет рассматриваться как отдельное, отличное от NULL и от других ссылок значение. Это означает, что если у многих сотрудников грейд не изменился, их строки все равно будут содержать 0 в поле ИзмененияГрейдов, и эта 0 может быть учтена функцией Количество(Различные) как одно "различное" значение, искажая итоговый подсчет.
Таким образом, основная причина некорректной работы — это смешение типов данных (ссылка и число) в одном поле и некорректная обработка случаев "без изменения" как 0 вместо NULL.
Наиболее простой и рекомендуемый способ решения этой проблемы заключается в изменении условного выражения таким образом, чтобы в случае отсутствия изменений возвращалось значение NULL, а не 0. Это позволяет эффективно использовать стандартное поведение функции Количество(Различные).
В вашем запросе, в секции ОБЪЕДИНИТЬ ВСЕ, скорректируйте выражение для поля ИзмененияГрейдов следующим образом:
ВЫБОР
КОГДА ВТ_После.ЗначениеПосле <> ВТ_До.Значение
ТОГДА ВТ_После.Сотрудник
ИНАЧЕ NULL // Важно: вместо 0 используем NULL
КОНЕЦ КАК ИзмененияГрейдов
Теперь, если грейд сотрудника не изменился, поле ИзмененияГрейдов будет содержать NULL. Поскольку функция Количество(Различные) игнорирует NULL, эти строки не будут влиять на подсчет.
После корректировки запроса, ресурс в СКД можно настроить одним из следующих способов:
ИзмененияГрейдов уже выведено в набор данных, то в ресурсах достаточно указать:
Количество(Различные ИзмененияГрейдов)
Количество(Различные ВЫБОР КОГДА ВТ_После.ЗначениеПосле <> ВТ_До.Значение ТОГДА ВТ_После.Сотрудник ИНАЧЕ NULL КОНЕЦ)
Этот подход гарантирует, что Количество(Различные) будет подсчитывать только уникальные ссылки на сотрудников, у которых грейд действительно изменился, игнорируя строки, где изменение не произошло.
Почему это работает: Используя NULL, мы явно указываем системе, что для данных строк нет значения, которое следует учитывать в подсчете уникальных элементов. Это соответствует стандартному поведению агрегатных функций SQL и СКД, которые предназначены для обработки отсутствующих данных через NULL, а не через специальное числовое значение (как 0).
Существует и альтернативный, элегантный способ решения задачи, который также позволяет получить список сотрудников с изменившимся грейдом. Этот метод основан на использовании признаков и последующей группировки.
Рассмотрим структуру запроса, предложенную в сообщении 10:
ВЫБРАТЬ "Сотрудник1" КАК Сотрудник, "СтарыйГрейд" КАК ЗначениеГрейда, 1 КАК Признак
ПОМЕСТИТЬ ВТ_ГрейдДо
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ "Сотрудник2", "СтарыйГрейд", 1
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ "Сотрудник3", "СтарыйГрейд", 1
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ "Сотрудник1" КАК Сотрудник, "СтарыйГрейд" КАК ЗначениеГрейда, -1 КАК Признак
ПОМЕСТИТЬ ВТ_ГрейдПосле
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ "Сотрудник2", "НовыйГрейд", -1
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ "Сотрудник4", "НовыйГрейд", -1
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Вложенный.Сотрудник КАК Сотрудник,
КОЛИЧЕСТВО(Вложенный.Сотрудник) КАК ИзмененияГрейдов
ИЗ
(ВЫБРАТЬ
ВТ_ГрейдДо.Сотрудник КАК Сотрудник,
ВТ_ГрейдДо.ЗначениеГрейда КАК ЗначениеГрейда,
ВТ_ГрейдДо.Признак КАК Признак
ИЗ
ВТ_ГрейдДо КАК ВТ_ГрейдДо
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ВТ_ГрейдПосле.Сотрудник,
ВТ_ГрейдПосле.ЗначениеГрейда,
ВТ_ГрейдПосле.Признак
ИЗ
ВТ_ГрейдПосле КАК ВТ_ГрейдПосле) КАК Вложенный
СГРУППИРОВАТЬ ПО
Вложенный.Сотрудник
ИМЕЮЩИЕ
СУММА(Вложенный.Признак) <> 0
Разберем этот подход по шагам:
Мы создаем две временные таблицы, которые содержат данные о грейдах сотрудников на два разных момента времени (условно "до" и "после").
ВТ_ГрейдДо: содержит сотрудников и их грейды до изменения. Каждой записи присваивается Признак = 1.ВТ_ГрейдПосле: содержит сотрудников и их грейды после изменения. Каждой записи присваивается Признак = -1.В реальном запросе, вместо строк "Сотрудник1", "СтарыйГрейд" и т.д., вы будете использовать ссылки на сотрудников и их фактические значения грейдов, полученные из срезов регистров.
Далее мы объединяем содержимое ВТ_ГрейдДо и ВТ_ГрейдПосле в одну вложенную таблицу. Это создает единый набор данных, где для каждого сотрудника могут быть представлены как "старый", так и "новый" грейд, каждый со своим признаком.
ИМЕЮЩИЕ:
Наконец, мы группируем полученные данные по Сотрудник. В секции ИМЕЮЩИЕ мы используем условие СУММА(Вложенный.Признак) <> 0.
0 (например, если "СтарыйГрейд" = "НовыйГрейд" для "Сотрудника1" в примере, то 1 + (-1) = 0).0 (например, для "Сотрудника2" в примере: 1 (СтарыйГрейд) + (-1) (НовыйГрейд) = 0, если бы он был только в ВТ_ГрейдПосле с новым грейдом -1, то сумма была бы -1. А если он был в ВТ_ГрейдДо со СтарымГрейдом и в ВТ_ГрейдПосле с НовымГрейдом, то его записи будут: (Сотрудник2, СтарыйГрейд, 1) и (Сотрудник2, НовыйГрейд, -1). При группировке по сотруднику, мы суммируем признаки. Если грейды разные, то СУММА(Признак) все равно может быть 0, если каждый грейд встречается по одному разу. Ошибка в понимании примера: данный пример в сообщении 10 не совсем корректно демонстрирует изменение грейда. Если "Сотрудник2" имел "СтарыйГрейд" до и "НовыйГрейд" после, то в объединенной таблице будут две записи для "Сотрудник2": одна с Признак = 1 и "СтарыйГрейд", другая с Признак = -1 и "НовыйГрейд". Сумма их признаков все равно будет 0. Этот метод корректен, когда мы хотим найти сотрудников, у которых набор значений в определенном поле изменился. Если нас интересует смена самого значения грейда, то нужно не просто суммировать признаки, а проверять количество уникальных значений грейда для каждого сотрудника.Однако, если мы адаптируем метод, чтобы он действительно работал для подсчета изменившихся грейдов, то нам нужно немного другой подход внутри ИМЕЮЩИЕ, например, проверять, что КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Вложенный.ЗначениеГрейда) > 1 для данного сотрудника, но это уже другая задача. Пример в сообщении 10, вероятно, предназначался для выявления сотрудников, которые присутствуют в одном списке, но отсутствуют в другом, или появились/исчезли, а не для прямого сравнения значений.
Для задачи "подсчитать различных сотрудников, у которых значение Грейда изменилось", более точным будет метод, который явно сравнивает значения "до" и "после", как в Решении 1.
Тем не менее, этот метод с признаками может быть полезен в других сценариях, например, для поиска сотрудников, которые присутствовали в одном периоде, но отсутствуют в другом, или наоборот. Для нашей же конкретной задачи сравнения значений, Решение 1 с использованием NULL является более прямым и менее подверженным ошибкам.
При возникновении проблем с отчетами СКД, особенно когда подсчеты кажутся неверными, рекомендуем следующие шаги для отладки:
ИзмененияГрейдов), без агрегации. Это позволит увидеть фактические значения, которые попадают в него для каждого сотрудника. Вы сможете проверить, действительно ли NULL появляется там, где изменение не произошло, и ссылки на сотрудников там, где оно есть.ПО) временных таблиц. Убедитесь, что они корректно сопоставляют данные "до" и "после" для каждого сотрудника и показателя.Надеемся, что эти подробные объяснения и решения помогут вам создавать более точные и надежные отчеты в 1С СКД. Помните, что корректное использование NULL и однородность типов данных в условных выражениях являются краеугольными камнями для правильной работы агрегатных функций.