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