Работа с языком запросов 1С часто вызывает сложности у начинающих разработчиков, особенно когда речь заходит о соединении нескольких таблиц и агрегации данных. Одной из самых частых проблем является неожиданное дублирование строк ("задвоение" данных) или некорректные итоговые суммы. В этой статье мы подробно разберем, почему возникают эти ошибки, как правильно использовать группировку и почему параметры виртуальных таблиц важнее условий ГДЕ.
Рассмотрим типичную ситуацию. У нас есть две таблицы (или временные таблицы), которые мы хотим соединить:
При попытке сделать ЛЕВОЕ СОЕДИНЕНИЕ первой таблицы со второй, мы ожидаем увидеть одну строку по сотруднику с присоединенными данными о днях. Однако, если во второй таблице (ВТ_Дни) по этому сотруднику найдется несколько записей (например, разные виды явок или просто разбивка по датам), то результат соединения увеличится кратно количеству этих записей.
Это классическая проблема соединения «один-ко-многим». Строка из левой таблицы дублируется для каждой подходящей строки из правой таблицы. Если после этого вы попытаетесь просуммировать начисления, итоговая сумма окажется завышенной.
Чтобы избежать дублей, нам необходимо обеспечить уникальность записей по ключевым полям связи (обычно это Сотрудник) в присоединяемой таблице. Рассмотрим, как это сделать на примере попытки развернуть данные по месяцам в колонки.
В исходной задаче использовалась конструкция ВЫБОР КОГДА... для распределения дней по колонкам, но без использования агрегатных функций. Это ошибка. Правильный подход — использовать функцию СУММА вместе с группировкой.
Давайте проанализируем, как должен выглядеть запрос для формирования правильной таблицы ВТ_Дни:
ВЫБРАТЬ
ДанныеОбороты.Сотрудник КАК Сотрудник,
// Агрегируем данные, чтобы получить одну строку на сотрудника
СУММА(ВЫБОР
КОГДА МЕСЯЦ(ДанныеОбороты.ПериодРегистрации) = МЕСЯЦ(&ДатаНачала)
ТОГДА ЕСТЬNULL(ДанныеОбороты.ДниОборот, 0)
ИНАЧЕ 0
КОНЕЦ) КАК ОтработанныеДни_Месяц1,
СУММА(ВЫБОР
КОГДА МЕСЯЦ(ДанныеОбороты.ПериодРегистрации) = МЕСЯЦ(&ДатаОкончания)
ТОГДА ЕСТЬNULL(ДанныеОбороты.ДниОборот, 0)
ИНАЧЕ 0
КОНЕЦ) КАК ОтработанныеДни_Месяц2
ПОМЕСТИТЬ ВТ_Дни
ИЗ
РегистрНакопления.ДанныеТабельногоУчетаРабочегоВремениСотрудников.Обороты(...) КАК ДанныеОбороты
СГРУППИРОВАТЬ ПО
ДанныеОбороты.Сотрудник
Что мы сделали:
ВЫБОР КОГДА в функцию СУММА(). Это позволяет "схлопнуть" несколько строк с разными периодами в одну итоговую строку.СГРУППИРОВАТЬ ПО. Мы группируем по полю Сотрудник. Это гарантирует, что в результате в таблице ВТ_Дни каждому сотруднику будет соответствовать ровно одна строка.Теперь, когда мы будем делать левое соединение ВТ_Начисления с подготовленной ВТ_Дни, связь будет «один-к-одному», и дублирования сумм начислений не произойдет.
Второй важный аспект, который мы должны разобрать — это производительность запроса. Часто новички выбирают данные из виртуальной таблицы (например, Обороты), а затем фильтруют их с помощью условия ГДЕ.
Неправильный подход ("Красный свет"):
ВЫБРАТЬ
...
ИЗ
РегистрНакопления.ДанныеТабельного.Обороты(,,,) КАК Обороты
ГДЕ
Обороты.Сотрудник = &Сотрудник
Правильный подход ("Зеленый свет"):
ВЫБРАТЬ
...
ИЗ
РегистрНакопления.ДанныеТабельного.Обороты(,,, Сотрудник = &Сотрудник) КАК Обороты
Почему это важно? Рассмотрим механику работы:
ГДЕ) система сначала получает все данные из таблицы оборотов (возможно, миллионы записей за все время), передает их на сервер 1С, и только потом отбрасывает лишнее. Это создает огромную нагрузку на память и диск.Используйте параметры виртуальных таблиц всегда, когда это возможно. Это не только ускоряет работу, но и необходимо для корректной работы механизма ограничения прав доступа (RLS).
При написании запросов также стоит обращать внимание на детали, которые могут привести к ошибкам в будущем.
В исходном примере использовалось условие МЕСЯЦ(Период) = МЕСЯЦ(&Дата). Это условие опасно, если ваш отчет строится на стыке лет. Например, январь 2023 и январь 2024 года вернут одинаковый номер месяца — 1. Чтобы избежать смешивания данных разных лет, лучше использовать функцию НАЧАЛОПЕРИОДА:
ВЫБОР
КОГДА НАЧАЛОПЕРИОДА(ДанныеОбороты.ПериодРегистрации, МЕСЯЦ) = НАЧАЛОПЕРИОДА(&ДатаНачала, МЕСЯЦ)
...
Когда вы используете ЛЕВОЕ СОЕДИНЕНИЕ, данные из правой таблицы могут отсутствовать. В этом случае поля правой таблицы будут содержать значение NULL. Любая арифметическая операция с NULL (например, сложение) вернет NULL.
Поэтому в финальном запросе, где вы выбираете данные из соединенных таблиц, обязательно используйте функцию ЕСТЬNULL:
ВЫБРАТЬ
ВТ_Начисления.Сотрудник,
ВТ_Начисления.СуммаНачисления,
ЕСТЬNULL(ВТ_Дни.ОтработанныеДни_Месяц1, 0) КАК ДниМесяц1
ИЗ
ВТ_Начисления КАК ВТ_Начисления
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Дни КАК ВТ_Дни
ПО ВТ_Начисления.Сотрудник = ВТ_Дни.Сотрудник
Для построения корректного и быстрого отчета следуйте этому алгоритму:
СУММА() или МАКСИМУМ().ГДЕ.ЕСТЬNULL, чтобы избежать пустот в числовых полях.