При разработке сложных отчетов, особенно в конфигурациях типа Зарплата и управление персоналом 3.1, программисты часто сталкиваются с необходимостью найти «дыры» в данных — например, периоды, за которые не введены документы или отсутствует рабочее время. Для этого традиционно используется ЛЕВОЕ СОЕДИНЕНИЕ (или ПРАВОЕ СОЕДИНЕНИЕ) двух таблиц с последующей фильтрацией по условию ЕСТЬ NULL. Однако ситуация осложняется, когда мы переходим от анализа одного конкретного объекта (сотрудника) к выборке по всей организации. Рассмотрим, почему запрос, работающий для одного человека, может выдавать пустой результат для списка сотрудников, и как это исправить.
Представим ситуацию: мы анализируем данные регистра накопления ДанныеТабельногоУчетаРабочегоВремениСотрудников. У нас есть две временные таблицы. Первая (ТЧРабочееВремя) содержит фактически отработанные дни, исключая выходные. Вторая (ТЧВыходные) собирает данные о выходных днях. Задача — найти записи, которые присутствуют в таблице выходных, но отсутствуют в таблице рабочего времени.
Исходный код проблемного участка запроса выглядел так:
ВЫБРАТЬ
ТЧВыходные.Период КАК Период,
ТЧВыходные.Сотрудник КАК Сотрудник,
ТЧВыходные.ВидУчетаВремени КАК ВидУчетаВремени
ИЗ
ТЧРабочееВремя КАК ТЧРабочееВремя
ПРАВОЕ СОЕДИНЕНИЕ ТЧВыходные КАК ТЧВыходные
ПО ТЧРабочееВремя.Период = ТЧВыходные.Период
ГДЕ
ТЧРабочееВремя.Сотрудник ЕСТЬ NULL
Пока в параметрах запроса был указан конкретный &Сотрудник, запрос работал корректно. Но как только условие по сотруднику убрали, чтобы получить данные по всей организации, результат стал пустым. Проанализируем причину этого поведения.
Проблема кроется в условии соединения ПО ТЧРабочееВремя.Период = ТЧВыходные.Период. Когда мы соединяем таблицы только по периоду (дате), система пытается сопоставить записи следующим образом:
ТЧВыходные за определенную дату (например, 01.09.2023) для Сотрудника №1.ТЧРабочееВремя любую запись за эту же дату (01.09.2023), независимо от того, какому сотруднику она принадлежит.ТЧРабочееВремя найдется запись.ТЧРабочееВремя.Сотрудник для этой даты не будет равно NULL, так как соединение произошло по периоду.ГДЕ ТЧРабочееВремя.Сотрудник ЕСТЬ NULL отсекает эту строку, так как связь по дате успешно установилась.Таким образом, если в организации ежедневно работает хотя бы один сотрудник, условие ЕСТЬ NULL никогда не сработает, и мы получим пустой результат.
Для того чтобы запрос корректно находил отсутствие данных для каждого конкретного человека, необходимо устанавливать связь по всем ключевым полям аналитики. В данном случае — и по Период, и по Сотрудник. Рассмотрим исправленный фрагмент кода:
ВЫБРАТЬ
ТЧВыходные.Период КАК Период,
ТЧВыходные.Сотрудник КАК Сотрудник,
ТЧВыходные.ВидУчетаВремени КАК ВидУчетаВремени,
ТЧВыходные.Дни КАК Дни,
ТЧВыходные.Часы КАК Часы
ИЗ
ТЧРабочееВремя КАК ТЧРабочееВремя
ПРАВОЕ СОЕДИНЕНИЕ ТЧВыходные КАК ТЧВыходные
ПО ТЧРабочееВремя.Период = ТЧВыходные.Период
И ТЧРабочееВремя.Сотрудник = ТЧВыходные.Сотрудник
ГДЕ
ТЧРабочееВремя.Сотрудник ЕСТЬ NULL
Теперь система будет искать соответствие не просто по дате, а по паре «Дата + Сотрудник». Если для Сотрудника №1 за 01.09.2023 нет записи в ТЧРабочееВремя, то даже при наличии там записей по другим коллегам, результатом соединения для Сотрудника №1 станет NULL, и условие в секции ГДЕ сработает правильно.
Разберем еще несколько важных моментов, которые помогут сделать подобный запрос более качественным и производительным:
ЛЕВОЕ СОЕДИНЕНИЕ стандартом де-факто для читаемости. Рекомендуется ставить «главную» таблицу (где данных больше или откуда мы берем базис) первой. В нашем случае логичнее взять ТЧВыходные и левым соединением присоединить ТЧРабочееВремя.ПО отрабатывало быстро, временные таблицы ТЧРабочееВремя и ТЧВыходные должны быть проиндексированы. Важно помнить, что порядок полей в индексе должен совпадать с порядком в условии соединения. Рекомендуется использовать ИНДЕКСИРОВАТЬ ПО Сотрудник, Период.Сотрудник.Наименование). Всегда используйте ссылки. Сравнение ссылок в СУБД происходит на уровне идентификаторов (GUID), что значительно быстрее строковых операций.Организация также должно участвовать в условиях связи, либо данные должны быть жестко отфильтрованы по одной организации на этапе формирования временных таблиц.Помимо соединений, в языке запросов 1С можно использовать оператор НЕ В с вложенным запросом. Рассмотрим этот вариант:
ВЫБРАТЬ
ТЧВыходные.Период,
ТЧВыходные.Сотрудник
ИЗ
ТЧВыходные КАК ТЧВыходные
ГДЕ
(ТЧВыходные.Период, ТЧВыходные.Сотрудник) НЕ В
(ВЫБРАТЬ
ТЧРабочееВремя.Период,
ТЧРабочееВремя.Сотрудник
ИЗ
ТЧРабочееВремя КАК ТЧРабочееВремя)
Хотя такой подход выглядит лаконично, на больших объемах данных (десятки тысяч сотрудников и миллионы записей времени) ЛЕВОЕ СОЕДИНЕНИЕ обычно показывает лучшую производительность, так как SQL-планировщик эффективнее оптимизирует операцию Hash Join или Merge Join, чем вложенную фильтрацию.
Важный совет: Всегда прописывайте все условия связи в соединениях, даже если на текущий момент данные кажутся уникальными по одному полю. Это убережет ваш код от логических ошибок при изменении структуры данных или расширении условий выборки.