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