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