При написании запросов в платформе 1С:Предприятие разработчики нередко сталкиваются с ситуацией, когда запрос корректно отрабатывает с жестко заданным числовым литералом в условии ГДЕ, но перестает возвращать данные или выдает ошибку, если литерал заменяется на параметр запроса.
Рассмотрим конкретный практический пример, разберем причины такого поведения платформы и СУБД, а также изучим правильные способы решения и оптимизации подобных запросов.
Проанализируем исходный запрос, в котором происходит выборка данных из табличной части с вычислением разницы дат:
ВЫБРАТЬ
Компьютеры.Ссылка КАК СерийныйНомер,
Компьютеры.Контрагент КАК Контрагент,
КомпьютерыРабочаяСтанция.Номенклатура КАК Оборудование,
КомпьютерыСвойства.ЗначениеСвойства КАК ДатаВвода,
ДОБАВИТЬКДАТЕ(ВЫРАЗИТЬ(КомпьютерыСвойства.ЗначениеСвойства КАК ДАТА), МЕСЯЦ, 13) КАК ДатаОкончанияФН,
Компьютеры.ВидРабочейСтанции.Ссылка КАК НомерФН
ИЗ
Справочник.Компьютеры.РабочаяСтанция КАК КомпьютерыРабочаяСтанция
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Компьютеры КАК Компьютеры
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Компьютеры.Свойства КАК КомпьютерыСвойства
ПО Компьютеры.Ссылка = КомпьютерыСвойства.Ссылка.Ссылка
ПО КомпьютерыРабочаяСтанция.Ссылка.Ссылка = Компьютеры.Ссылка
ГДЕ
Компьютеры.ВидРабочейСтанции = &ВидРабочейСтанции1
И КомпьютерыСвойства.ВидСвойства В(&ВидСвойства)
И РАЗНОСТЬДАТ(ВЫРАЗИТЬ(КомпьютерыСвойства.ЗначениеСвойства КАК ДАТА), &ТекущаяДата, МЕСЯЦ) <= 26
И РАЗНОСТЬДАТ(ВЫРАЗИТЬ(КомпьютерыСвойства.ЗначениеСвойства КАК ДАТА), &ТекущаяДата, МЕСЯЦ) >= 1
Если в условии вместо числа 26 подставить параметр &ПериодВМесяцах, выборка перестает возвращать ожидаемые строки, несмотря на то, что в параметр передается обычное число.
Выясним причину этой проблемы:
РАЗНОСТЬДАТ на уровне СУБД преобразуется в целочисленное значение (например, DATEDIFF в MS SQL Server с типом INT). При этом числовые переменные встроенного языка 1С по умолчанию передаются в параметры СУБД как числа с фиксированной точностью (NUMERIC/DECIMAL). В зависимости от используемой СУБД и версии платформы неявное приведение типов в предикатах фильтрации может приводить к некорректному сопоставлению типов или построению неверного плана выполнения.ЗначениеСвойства обычно имеет составной тип. Если в строке хранится не дата, а строка или ссылка, конструкция ВЫРАЗИТЬ(КомпьютерыСвойства.ЗначениеСвойства КАК ДАТА) вернет значение NULL. Любая функция РАЗНОСТЬДАТ от NULL также возвращает NULL, а результат сравнения NULL <= &Параметр дает Ложь.Рассмотрим быстрое решение, позволяющее устранить конфликт типов непосредственно в тексте запроса. Для этого необходимо явно указать тип передаваемого числового параметра с помощью оператора ВЫРАЗИТЬ.
Разберем, как изменить секцию ГДЕ:
ГДЕ
Компьютеры.ВидРабочейСтанции = &ВидРабочейСтанции1
И КомпьютерыСвойства.ВидСвойства В(&ВидСвойства)
И РАЗНОСТЬДАТ(ВЫРАЗИТЬ(КомпьютерыСвойства.ЗначениеСвойства КАК ДАТА), &ТекущаяДата, МЕСЯЦ) <= ВЫРАЗИТЬ(&ПериодВМесяцах КАК ЧИСЛО(10, 0))
И РАЗНОСТЬДАТ(ВЫРАЗИТЬ(КомпьютерыСвойства.ЗначениеСвойства КАК ДАТА), &ТекущаяДата, МЕСЯЦ) >= 1
Конструкция ВЫРАЗИТЬ(&ПериодВМесяцах КАК ЧИСЛО(10, 0)) жестко фиксирует длину и разрядность параметра на уровне запроса, что исключает ошибки сопоставления типов при трансляции в SQL.
Использование функций РАЗНОСТЬДАТ или ДОБАВИТЬКДАТЕ над полями базы данных в секции ГДЕ является неоптимальным с точки зрения производительности. СУБД не может применить индекс по полю даты, так как вынуждена вычислять функцию для каждой строки таблицы (происходит полное сканирование таблицы — Table Scan или Index Scan).
Разберем по шагам, как переписать условие по стандарту разработки 1С:
ДобавитьМесяц.РАЗНОСТЬДАТ в секции ГДЕ на прямое сравнение поля даты с интервалом через оператор МЕЖДУ.Посмотрим на пример кода во встроенном языке 1С:
// 1. Рассчитываем границы периода
ТекущаяДатаСеанса = ТекущаяДатаСеанса();
КоличествоМесяцев = 26;
// Сдвигаем даты на нужный интервал назад
ДатаНачала = ДобавитьМесяц(ТекущаяДатаСеанса, -КоличествоМесяцев);
ДатаОкончания = ДобавитьМесяц(ТекущаяДатаСеанса, -1);
// 2. Устанавливаем параметры и выполняем запрос
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Компьютеры.Ссылка КАК СерийныйНомер,
| Компьютеры.Контрагент КАК Контрагент,
| КомпьютерыРабочаяСтанция.Номенклатура КАК Оборудование,
| КомпьютерыСвойства.ЗначениеСвойства КАК ДатаВвода,
| ДОБАВИТЬКДАТЕ(ВЫРАЗИТЬ(КомпьютерыСвойства.ЗначениеСвойства КАК ДАТА), МЕСЯЦ, 13) КАК ДатаОкончанияФН,
| Компьютеры.ВидРабочейСтанции.Ссылка КАК НомерФН
|ИЗ
| Справочник.Компьютеры.РабочаяСтанция КАК КомпьютерыРабочаяСтанция
| ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Компьютеры КАК Компьютеры
| ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Компьютеры.Свойства КАК КомпьютерыСвойства
| ПО Компьютеры.Ссылка = КомпьютерыСвойства.Ссылка.Ссылка
| ПО КомпьютерыРабочаяСтанция.Ссылка.Ссылка = Компьютеры.Ссылка
|ГДЕ
| Компьютеры.ВидРабочейСтанции = &ВидРабочейСтанции1
| И КомпьютерыСвойства.ВидСвойства В(&ВидСвойства)
| И ТИПЗНАЧЕНИЯ(КомпьютерыСвойства.ЗначениеСвойства) = ТИП(ДАТА)
| И ВЫРАЗИТЬ(КомпьютерыСвойства.ЗначениеСвойства КАК ДАТА) МЕЖДУ &ДатаНачала И &ДатаОкончания";
Запрос.УстановитьПараметр("ВидРабочейСтанции1", ВидРабочейСтанции);
Запрос.УстановитьПараметр("ВидСвойства", МассивВидовСвойств);
Запрос.УстановитьПараметр("ДатаНачала", ДатаНачала);
Запрос.УстановитьПараметр("ДатаОкончания", ДатаОкончания);
РезультатЗапроса = Запрос.Выполнить();
Преимущества второго подхода:
ТИПЗНАЧЕНИЯ(КомпьютерыСвойства.ЗначениеСвойства) = ТИП(ДАТА), что защищает от появления значений NULL при приведении типов.