Как вывести всех сотрудников из справочника вместе с данными из документа «Табель» (если они есть) с помощью левого соединения в 1С?

Программист 1С v8.3 (Управляемые формы) 1С:Зарплата и Управление Персоналом Управленческий учет
← На главную

При работе с базами данных 1С часто возникает задача объединения данных из различных источников, например, из справочника и табличной части документа. Особенно сложной эта задача становится, когда нам необходимо получить полный список элементов из одного источника (например, всех сотрудников из справочника), дополнив его данными из другого источника (например, из документа «Табель»), при этом сохраняя в выборке и те записи, для которых соответствий во втором источнике не нашлось. Именно для таких случаев идеально подходит оператор ЛЕВОЕ СОЕДИНЕНИЕ. Однако, неправильное использование условий отбора может значительно исказить результат. Давайте подробно рассмотрим, как решить эту задачу, избегая распространенных ошибок.

Разбираем исходный запрос и выявляем проблему

Представим, что у нас есть справочник «Сотрудники» и документ «Табель», который содержит табличную часть со списком сотрудников и информацией по дням табелирования. Наша цель — вывести всех сотрудников из справочника, а рядом с ними — данные из табеля за определенную дату, если таковые имеются. Изначально запрос может выглядеть следующим образом:


ВЫБРАТЬ
    Сотрудники.Ссылка КАК Ссылка,
    Сотрудники.Код КАК Код,
    ТабельСписок.ФИО КАК ФИО,
    Сотрудники.Должность КАК Должность,
    ТабельСписок.ДатаДок КАК ДатаДок,
    ЕСТЬNULL(ТабельСписок.День1, 0) КАК День1,
    // ... остаток дней до День31
    ЕСТЬNULL(ТабельСписок.День31, 0) КАК День31
ИЗ
    Справочник.Сотрудники КАК Сотрудники
        ЛЕВОЕ СОЕДИНЕНИЕ Документ.Табель.Список КАК ТабельСписок
        ПО Сотрудники.Ссылка = ТабельСписок.ФИО
ГДЕ
    Сотрудники.ДатаУвольнения <> НЕОПРЕДЕЛЕНО
    И ТабельСписок.ДатаДок = &ДатаДок
УПОРЯДОЧИТЬ ПО
    Код

На первый взгляд, этот запрос кажется логичным: мы используем ЛЕВОЕ СОЕДИНЕНИЕ, чтобы получить всех сотрудников, и пытаемся отфильтровать табели по дате. Однако, в данном запросе присутствует критическая ошибка. Условие ТабельСписок.ДатаДок = &ДатаДок, расположенное в секции ГДЕ, фактически преобразует ЛЕВОЕ СОЕДИНЕНИЕ во внутреннее соединение. Почему так происходит?

При ЛЕВОМ СОЕДИНЕНИИ, если для записи из левой таблицы (в нашем случае, <КрасныйТекст>Справочник.Сотрудники</КрасныйТекст>) не находится соответствия в правой таблице (<КрасныйТекст>Документ.Табель.Список</КрасныйТекст>) по условию в секции ПО, то все поля из правой таблицы в результирующей выборке будут иметь значение NULL. Когда мы добавляем в секцию ГДЕ условие <КрасныйТекст>ТабельСписок.ДатаДок = &ДатаДок</КрасныйТекст>, мы неявно требуем, чтобы поле <КрасныйТекст>ТабельСписок.ДатаДок</КрасныйТекст> было заполнено определенным значением (не NULL). Следовательно, строки, где для сотрудников нет записей в табеле (и, соответственно, <КрасныйТекст>ТабельСписок.ДатаДок</КрасныйТекст> равно NULL), будут отброшены. В результате мы увидим только тех сотрудников, которые присутствуют в табеле на указанную дату, что противоречит исходной задаче вывода всех сотрудников.

Ключевое решение — перенос условий в секцию ПО левого соединения

Чтобы правильно использовать ЛЕВОЕ СОЕДИНЕНИЕ и получить всех сотрудников, независимо от наличия записи в табеле на конкретную дату, необходимо перенести условие отбора по дате документа из секции ГДЕ в секцию ПО оператора ЛЕВОЕ СОЕДИНЕНИЕ. Таким образом, условие будет применяться не к результату всего соединения, а непосредственно к правой таблице в процессе соединения.

Разберем по шагам, как работает ЛЕВОЕ СОЕДИНЕНИЕ с условиями в секции ПО:

  1. Для каждой записи из левой таблицы (Справочник.Сотрудники) система пытается найти соответствующие записи в правой таблице (Документ.Табель.Список).
  2. Поиск соответствий в правой таблице происходит по всем условиям, указанным в секции ПО. Если условия включают фильтрацию, например, по дате, то в правой таблице будут рассматриваться только те записи, которые удовлетворяют этому фильтру.
  3. Если для записи из левой таблицы найдено одно или несколько соответствий в правой таблице, удовлетворяющих всем условиям в ПО, они будут соединены.
  4. Если для записи из левой таблицы не найдено ни одного соответствия в правой таблице (либо потому, что их нет, либо потому, что ни одно из них не удовлетворило условиям в ПО), то в результирующую выборку будет включена запись из левой таблицы, а все поля из правой таблицы будут заполнены значениями NULL.

Исправленный фрагмент соединения будет выглядеть так:


ИЗ
    Справочник.Сотрудники КАК Сотрудники
        ЛЕВОЕ СОЕДИНЕНИЕ Документ.Табель.Список КАК ТабельСписок
        ПО Сотрудники.Ссылка = ТабельСписок.ФИО
        И ТабельСписок.ДатаДок = &ДатаДок // Условие перенесено в секцию ПО

Теперь, если для сотрудника нет записей в табеле на заданную дату, или если табель вообще отсутствует, в полях из <КрасныйТекст>ТабельСписок</КрасныйТекст> будут значения NULL, но сам сотрудник все равно будет присутствовать в итоговой выборке. Это именно то поведение, которое мы хотели достичь.

Корректная обработка пустых значений даты

В исходном запросе также присутствовало условие Сотрудники.ДатаУвольнения <> НЕОПРЕДЕЛЕНО. В 1С для полей типа «Дата» (Дата, ДатаВремя) пустое значение в базе данных обычно хранится как ДАТАВРЕМЯ(1,1,1). Константа НЕОПРЕДЕЛЕНО используется для работы со значениями составных типов или для указания на отсутствие какого-либо значения в контексте языка 1С, но не для проверки пустой даты в запросах к СУБД. Поэтому, для корректного отбора неуволенных сотрудников, следует использовать сравнение с ДАТАВРЕМЯ(1,1,1).

Исправленное условие будет выглядеть так:


ГДЕ
    Сотрудники.ДатаУвольнения = ДАТАВРЕМЯ(1,1,1) // Выбираем всех неуволенных сотрудников
    // Если вам нужны также уволенные сотрудники, но с незаполненной датой увольнения,
    // или вообще все сотрудники, это условие можно модифицировать или убрать.

Важно отметить, что условие <КрасныйТекст>Сотрудники.ДатаУвольнения = ДАТАВРЕМЯ(1,1,1)</КрасныйТекст> выбирает только неуволенных сотрудников. Если задача состоит в том, чтобы вывести всех сотрудников, включая уволенных, но посмотреть их табель за прошлый период (если он есть), тогда это условие может быть либо убрано, либо изменено в соответствии с конкретными требованиями бизнес-логики (например, <КрасныйТекст>Сотрудники.ДатаПриема < &ДатаДок</КрасныйТекст> и <KрасныйТекст>Сотрудники.ДатаУвольнения > &ДатаДок ИЛИ Сотрудники.ДатаУвольнения = ДАТАВРЕМЯ(1,1,1)</KрасныйТекст>).

Итоговый запрос для вывода всех сотрудников с информацией о табелировании

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


ВЫБРАТЬ
    Сотрудники.Ссылка КАК Ссылка,
    Сотрудники.Код КАК Код,
    Сотрудники.Наименование КАК ФИО, // Используем Наименование из справочника для ФИО
    Сотрудники.Должность КАК Должность,
    ЕСТЬNULL(ТабельСписок.Документ.Дата, ДАТАВРЕМЯ(1,1,1)) КАК ДатаДок, // Дата документа Табель
    ЕСТЬNULL(ТабельСписок.День1, 0) КАК День1,
    ЕСТЬNULL(ТабельСписок.День2, 0) КАК День2,
    ЕСТЬNULL(ТабельСписок.День3, 0) КАК День3,
    ЕСТЬNULL(ТабельСписок.День4, 0) КАК День4,
    ЕСТЬNULL(ТабельСписок.День5, 0) КАК День5,
    ЕСТЬNULL(ТабельСписок.День6, 0) КАК День6,
    ЕСТЬNULL(ТабельСписок.День7, 0) КАК День7,
    ЕСТЬNULL(ТабельСписок.День8, 0) КАК День8,
    ЕСТЬNULL(ТабельСписок.День9, 0) КАК День9,
    ЕСТЬNULL(ТабельСписок.День10, 0) КАК День10,
    ЕСТЬNULL(ТабельСписок.День11, 0) КАК День11,
    ЕСТЬNULL(ТабельСписок.День12, 0) КАК День12,
    ЕСТЬNULL(ТабельСписок.День13, 0) КАК День13,
    ЕСТЬNULL(ТабельСписок.День14, 0) КАК День14,
    ЕСТЬNULL(ТабельСписок.День15, 0) КАК День15,
    ЕСТЬNULL(ТабельСписок.День16, 0) КАК День16,
    ЕСТЬNULL(ТабельСписок.День17, 0) КАК День17,
    ЕСТЬNULL(ТабельСписок.День18, 0) КАК День18,
    ЕСТЬNULL(ТабельСписок.День19, 0) КАК День19,
    ЕСТЬNULL(ТабельСписок.День20, 0) КАК День20,
    ЕСТЬNULL(ТабельСписок.День21, 0) КАК День21,
    ЕСТЬNULL(ТабельСписок.День22, 0) КАК День22,
    ЕСТЬNULL(ТабельСписок.День23, 0) КАК День23,
    ЕСТЬNULL(ТабельСписок.День24, 0) КАК День24,
    ЕСТЬNULL(ТабельСписок.День25, 0) КАК День25,
    ЕСТЬNULL(ТабельСписок.День26, 0) КАК День26,
    ЕСТЬNULL(ТабельСписок.День27, 0) КАК День27,
    ЕСТЬNULL(ТабельСписок.День28, 0) КАК День28,
    ЕСТЬNULL(ТабельСписок.День29, 0) КАК День29,
    ЕСТЬNULL(ТабельСписок.День30, 0) КАК День30,
    ЕСТЬNULL(ТабельСписок.День31, 0) КАК День31
ИЗ
    Справочник.Сотрудники КАК Сотрудники
        ЛЕВОЕ СОЕДИНЕНИЕ Документ.Табель.Список КАК ТабельСписок
        ПО Сотрудники.Ссылка = ТабельСписок.ФИО
        И ТабельСписок.Документ.Дата = &ДатаДок // Важно: используем ТабельСписок.Документ.Дата для ссылки на дату самого документа, а не поля ТЧ
ГДЕ
    Сотрудники.ДатаУвольнения = ДАТАВРЕМЯ(1,1,1) // Отбор по неуволенным сотрудникам
УПОРЯДОЧИТЬ ПО
    Код

Обратите внимание на несколько важных моментов в доработанном запросе:

  1. Мы используем <КрасныйТекст>Сотрудники.Наименование КАК ФИО</КрасныйТекст> для вывода полного имени сотрудника, так как <КрасныйТекст>ТабельСписок.ФИО</КрасныйТекст> является ссылочным полем, и его прямое использование может быть менее информативным или требовать дополнительного "разыменования".
  2. Для <КрасныйТекст>ДатаДок</КрасныйТекст> мы теперь берем <КрасныйТекст>ТабельСписок.Документ.Дата</КрасныйТекст>, чтобы получить дату самого документа «Табель», а не какое-то поле из табличной части. Использование <KрасныйТекст>ЕСТЬNULL</KраснийТекст> здесь помогает отобразить <КрасныйТекст>ДАТАВРЕМЯ(1,1,1)</KраснийТекст> для сотрудников, не попавших в табель, вместо <КрасныйТекст>NULL</КраснийТекст>.
  3. Условие <КрасныйТекст>ТабельСписок.Документ.Дата = &ДатаДок</КраснийТекст> корректно перенесено в секцию <KрасныйТекст>ПО</KраснийТекст> левого соединения, что обеспечивает включение в результат всех сотрудников.
  4. Условие <КрасныйТекст>Сотрудники.ДатаУвольнения = ДАТАВРЕМЯ(1,1,1)</КраснийТекст> фильтрует список, оставляя только неуволенных сотрудников. При необходимости его можно модифицировать или удалить.

Таким образом, мы добились нашей цели: получили список всех сотрудников из справочника, а для тех из них, кто был протабелирован на указанную дату, вывели соответствующую информацию. Для остальных сотрудников, не фигурирующих в табеле на эту дату, поля, связанные с табелем, будут заполнены значениями NULL (или значениями по умолчанию, заданными через ЕСТЬNULL()).

Дополнительные рекомендации по оптимизации запросов с левым соединением

Правильное построение запросов — это не только получение верного результата, но и обеспечение его высокой производительности. Рассмотрим несколько рекомендаций по оптимизации запросов, особенно с использованием ЛЕВОГО СОЕДИНЕНИЯ:

  1. Использование функции ЕСТЬNULL(): Эта функция крайне полезна при ЛЕВОМ СОЕДИНЕНИИ. Она позволяет заменить значения NULL (которые могут появиться в полях правой таблицы, если нет соответствий) на осмысленные значения. Например, ЕСТЬNULL(ТабельСписок.День1, 0) заменит NULL на 0, что удобно для числовых полей. Для ссылочных полей можно использовать пустую ссылку (например, ЕСТЬNULL(ТабельСписок.ФИО, ЗНАЧЕНИЕ(Справочник.Сотрудники.ПустаяСсылка))) или пустую строку для строковых полей.
  2. Применение временных таблиц: Если в запросе используются сложные подзапросы или соединение с виртуальными таблицами, которые строятся долго, рекомендуется выносить их в отдельные запросы с сохранением результатов во временные таблицы. Это может значительно улучшить производительность. Временные таблицы можно проиндексировать по полям соединения, что ускорит дальнейшую работу с ними.
  3. Оптимизация параметров виртуальных таблиц: При обращении к виртуальным таблицам (например, регистрам накопления, регистрам сведений) всегда используйте параметры для ограничения объема выборки данных на этапе их получения. Отсутствие параметров приводит к считыванию всей таблицы и избыточной блокировке ресурсов СУБД, а затем к отсеиванию лишних записей, что сильно замедляет работу.
  4. Эффективные условия поиска по индексу: Условия в секциях ГДЕ, ПО и виртуальных таблицах должны быть написаны таким образом, чтобы СУБД могла эффективно использовать индексы. Для ЛЕВОГО СОЕДИНЕНИЯ особенно важно, чтобы поля в правой таблице, используемые в условии ПО, были проиндексированы. Избегайте использования функций над полями в условиях, так как это часто приводит к невозможности использования индексов.
  5. Избегаем "разыменования" ссылочных полей через точку (используем ВЫРАЗИТЬ): Если вы получаете значения полей составного типа через точку (например, ТоварыНаСкладах.Склад.Наименование), и не знаете точный тип объекта, система 1С может генерировать дополнительные запросы для всех возможных типов. Это может привести к значительным замедлениям. Вместо этого используйте конструкцию ВЫРАЗИТЬ(<Выражение> КАК <Тип>) для явного указания конкретного типа, если это возможно и необходимо. Например, ВЫРАЗИТЬ(ТабельСписок.ФИО КАК Справочник.Сотрудники).Должность.

Эти рекомендации помогут вам создавать не только функционально правильные, но и высокопроизводительные запросы в 1С, что особенно важно при работе с большими объемами данных и сложными отчетами. Всегда стремитесь к максимальной конкретизации условий и эффективному использованию возможностей СУБД.

← На главную