При разработке механизмов автоматического заполнения документов или переноса данных из одних табличных частей в другие программисты часто сталкиваются с необходимостью фильтрации строк. Типичный пример — создание документа РеализацияТоваровУслуг на основании данных логистического документа (например, МаршрутПоездки), где некоторые позиции могут быть отмечены как брак. Наша задача — сделать так, чтобы бракованный товар не попадал в итоговый документ реализации.
Однако простая, на первый взгляд, задача добавления фильтра по булевому реквизиту Брак может преподнести неприятный сюрприз: после наложения условий запрос перестает возвращать даже те строки, которые заведомо не являются браком. В этой статье мы подробно разберем, почему так происходит, изучим трехзначную логику баз данных СУБД и напишем оптимальный, быстродействующий запрос.
Давайте проанализируем ситуацию, с которой столкнулся автор темы. Изначально у него был рабочий запрос, который собирал все товары из табличной части документа поездки и группировал их по водителям для последующего создания документов реализации. Когда возникла задача исключить брак, разработчик попытался добавить в секцию ГДЕ условие фильтрации:
И НЕ ДоставкаТовары.Брак
Или его аналог:
И ДоставкаТовары.Брак = Ложь
После этого система повела себя неожиданно: документы реализации перестали создаваться вообще для тех поездок, где была хоть одна строка с браком, а в некоторых случаях данные исчезли и из документов без брака. Выясним причину этого поведения.
Главная причина кроется в особенностях работы СУБД со значениями типа Булево, которые могут принимать не два состояния (Истина и Ложь), а три. Третьим состоянием является NULL — отсутствие значения как такового.
Рассмотрим по шагам, как это происходит на практике:
Брак с типом Булево), платформа 1С не перезаписывает старые документы автоматически. В физических таблицах базы данных для всех ранее записанных строк в поле нового реквизита записывается значение NULL.NULL возвращает специальный результат UNKNOWN (неизвестно), который СУБД трактует как ложь при фильтрации в блоке ГДЕ.Давайте посмотрим, как СУБД вычисляет условия для разных строк таблицы:
Истина): условие НЕ Истина дает Ложь. Строка отсекается. Это правильное поведение.Ложь): условие НЕ Ложь дает Истина. Строка попадает в выборку. Это тоже правильно.NULL): условие НЕ NULL дает значение NULL. В секции ГДЕ этот результат отсекается СУБД. В итоге качественный товар из старых документов бесследно исчезает из выборки!Поскольку в результате работы запроса по конкретному водителю не оставалось ни одной строки, коллекция товаров в коде 1С оказывалась пустой. Проверка Если Док.Товары.Количество > 0 Тогда не срабатывала, и документ реализации просто не создавался.
Чтобы решить эту проблему раз и навсегда на уровне базы данных, мы должны явно заменять все значения NULL на значение Ложь перед выполнением логических операций. Для этого в языке запросов 1С предусмотрена функция ЕСТЬNULL.
Синтаксис функции выглядит следующим образом: ЕСТЬNULL(Поле, ЗначениеЗамены). Если поле содержит реальное значение, функция возвращает его. Если же в поле хранится NULL, функция возвращает указанное значение замены.
Применим эту функцию в нашем запросе в секции фильтрации:
И НЕ ЕСТЬNULL(ДоставкаТовары.Брак, ЛОЖЬ)
Теперь, если реквизит Брак не заполнен (равен NULL), он преобразуется в ЛОЖЬ, выражение примет вид НЕ ЛОЖЬ (что дает ИСТИНА), и строка успешно попадет в итоговый документ реализации.
При детальном анализе исходного запроса автора можно заметить еще одну серьезную ошибку проектирования, которая негативно сказывается на скорости работы системы при увеличении объемов базы данных. Обратите внимание на условие связи левого соединения:
ПО ДоставкаТовары.Номенклатура = ЦеныПоставщиковСрезПоследних.Номенклатура.Номенклатура
Здесь происходит обращение через точку к реквизиту ссылки: Номенклатура.Номенклатура. В терминах СУБД это называется разыменованием ссылки. Из-за этого платформа 1С вынуждена генерировать дополнительное неявное левое соединение (LEFT JOIN) с таблицей справочника номенклатуры для каждой проверяемой строки. Это лавинообразно увеличивает нагрузку на СУБД.
Поскольку измерение Номенклатура в регистре сведений ЦеныПоставщиков само по себе является ссылкой нужного типа, мы можем сопоставлять поля напрямую:
ПО ДоставкаТовары.Номенклатура = ЦеныПоставщиковСрезПоследних.Номенклатура
Такое простое исправление значительно повысит скорость выполнения запроса.
Соберем все наши исправления воедино. Мы перенесем фильтрацию по браку на самый ранний этап — во вложенный запрос. Это позволит СУБД группировать только те данные, которые действительно должны попасть в реализацию, экономя оперативную память сервера.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ВложенныйЗапрос.Номенклатура КАК Номенклатура,
| ВложенныйЗапрос.ЕдиницаИзмерения КАК ЕдиницаИзмерения,
| ВложенныйЗапрос.Коэффициент КАК Коэффициент,
| ВложенныйЗапрос.Количество КАК Количество,
| ВложенныйЗапрос.Сумма КАК Сумма,
| ВложенныйЗапрос.ЦенаДоставки КАК ЦенаДоставки,
| ВложенныйЗапрос.ЦенаДоставкиЗаМКАД КАК ЦенаДоставкиЗаМКАД,
| ВложенныйЗапрос.Водители КАК Водители,
| ВложенныйЗапрос.Цена КАК Цена
|ИЗ
| (ВЫБРАТЬ
| ДоставкаТовары.Номенклатура КАК Номенклатура,
| ДоставкаТовары.ЕдиницаИзмерения КАК ЕдиницаИзмерения,
| ДоставкаТовары.Коэффициент КАК Коэффициент,
| СУММА(ДоставкаТовары.Количество) КАК Количество,
| СУММА(ДоставкаТовары.Сумма) КАК Сумма,
| ДоставкаТовары.Водители КАК Водители,
| ДоставкаТовары.ЦенаДоставки КАК ЦенаДоставки,
| ДоставкаТовары.ЦенаДоставкиЗаМКАД КАК ЦенаДоставкиЗаМКАД,
| ДоставкаТовары.Цена КАК Цена
| ИЗ
| Документ.МаршрутПоездки.Маршруты КАК ДоставкаТовары
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныПоставщиков.СрезПоследних КАК ЦеныПоставщиковСрезПоследних
| ПО ДоставкаТовары.Номенклатура = ЦеныПоставщиковСрезПоследних.Номенклатура
| И ДоставкаТовары.Водители = ЦеныПоставщиковСрезПоследних.НастройкаЗагрузки.Контрагент
| ГДЕ
| ДоставкаТовары.Ссылка В(&МассивДокументов)
| И ДоставкаТовары.Водители <> ЗНАЧЕНИЕ(Справочник.Контрагенты.ПустаяСсылка)
| И НЕ ДоставкаТовары.Номенклатура.Услуга
| И НЕ ЕСТЬNULL(ДоставкаТовары.Брак, ЛОЖЬ) // Фильтрация брака с учетом NULL
|
| СГРУППИРОВАТЬ ПО
| ДоставкаТовары.Номенклатура,
| ДоставкаТовары.ЕдиницаИзмерения,
| ДоставкаТовары.Коэффициент,
| ДоставкаТовары.Водители,
| ДоставкаТовары.ЦенаДоставки,
| ДоставкаТовары.ЦенаДоставкиЗаМКАД,
| ДоставкаТовары.Цена,
| ЕСТЬNULL(ЦеныПоставщиковСрезПоследних.ЦенаПоставщикаВал, 0)) КАК ВложенныйЗапрос
|ИТОГИ ПО
| Водители";
Если по каким-то причинам вы не хотите менять структуру сложного запроса, можно выполнить фильтрацию непосредственно на стороне сервера приложений 1С при обходе выборки в цикле. Для этого нам потребуется добавить реквизит Брак в поля выборки запроса и применить простую проверку в коде:
ВыборкаТоваров = ВыборкаДокументов.Выбрать();
Пока ВыборкаТоваров.Следующий() Цикл
// Проверяем значение реквизита Брак.
// Поскольку значение в выборке может быть типа NULL, используем проверку на Истина
Если ВыборкаТоваров.Брак = Истина Тогда
Продолжить; // Пропускаем бракованную строку
КонецЕсли;
НоваяСтрока = Док.Товары.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, ВыборкаТоваров);
// Выполняем типовые действия при изменении номенклатуры
ОбработкаТабличныхЧастей.ПриИзмененииНоменклатурыТабЧасти(НоваяСтрока, Док);
НоваяСтрока.ЕдиницаИзмерения = ВыборкаТоваров.Номенклатура.ЕдиницаХраненияОстатков;
НоваяСтрока.Коэффициент = НоваяСтрока.ЕдиницаИзмерения.Коэффициент;
НоваяСтрока.СтавкаНДС = Перечисления.СтавкиНДС.БезНДС;
КонецЦикла;
Обратите внимание на конструкцию Если ВыборкаТоваров.Брак = Истина Тогда. Прямое сравнение с Истина в коде на встроенном языке 1С безопасно обрабатывает значение NULL (выражение NULL = Истина вернет значение Ложь, и выполнение кода пойдет по нужной ветке). Однако этот метод менее эффективен, так как сервер приложений тратит ресурсы на передачу лишних бракованных строк из СУБД.
Чтобы навсегда избавить систему от неопределенности в булевых полях и ускорить работу запросов, хорошей практикой является проведение однократной миграции данных после добавления нового реквизита. Вы можете написать простую обработку, которая найдет все документы МаршрутПоездки и заполнит пустое поле Брак значением Ложь:
ВыборкаДокументов = Документы.МаршрутПоездки.Выбрать();
Пока ВыборкаДокументов.Следующий() Цикл
ОбъектДок = ВыборкаДокументов.ПолучитьОбъект();
Модифицирован = Ложь;
Для Каждого СтрокаТЧ Из ОбъектДок.Маршруты Цикл
Если СтрокаТЧ.Брак = Неопределено Или СтрокаТЧ.Брак = NULL Тогда
СтрокаТЧ.Брак = Ложь;
Модифицирован = Истина;
КонецЕсли;
КонецЦикла;
Если Модифицирован Тогда
ОбъектДок.Записать(РежимЗаписиДокумента.Запись);
КонецЕсли;
КонецЦикла;
После этого поле Брак в базе данных будет гарантированно содержать только валидные булевы значения, упрощая написание будущих запросов и снижая риск возникновения непредвиденных логических ошибок.