При разработке отчетов и обработок в 1С часто возникает задача отфильтровать данные справочника или регистра по дате, введенной пользователем на форме. Начинающие разработчики нередко сталкиваются с ситуацией, когда запрос возвращает пустой результат, хотя запись с указанной датой гарантированно присутствует в информационной базе. Разберем по шагам, почему возникает эта ошибка и как правильно организовать сравнение дат в языке запросов платформы 1С:Предприятие 8.3.
Проанализируем типичный ошибочный код, в котором текст запроса формируется путем конкатенации строк:
Запрос.Текст = "ВЫБРАТЬ
| ФизЛицо.Наименование,
| ФизЛицо.ДатаРождения
|ИЗ
| Справочник.ФизическиеЛица КАК ФизЛицо
|ГДЕ
| ФизЛицо.Наименование = """ + ФИОТекст + """
| И ФизЛицо.ДатаРождения = """ + Формат(ДатаРождения, "ДФ=dd.MM.yyyy; ДЛФ=D") + """";
В этом примере допущены сразу две серьезные ошибки:
Формат() преобразует значение типа Дата в значение типа Строка. В итоге после подстановки в тексте запроса получается условие вида ФизЛицо.ДатаРождения = "01.01.2018". Поле базы данных имеет тип Дата, а сравнивается оно со строковым литералом. В строго типизированных СУБД такое сравнение всегда дает ложный результат (или ошибку), поэтому выборка оказывается пустой.Рассмотрим правильный подход: текст запроса должен оставаться статичным, а все пользовательские значения необходимо передавать через механизм параметров с помощью метода УстановитьПараметр().
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ФизЛицо.Ссылка КАК Ссылка,
| ФизЛицо.Наименование КАК Наименование,
| ФизЛицо.ДатаРождения КАК ДатаРождения
|ИЗ
| Справочник.ФизическиеЛица КАК ФизЛицо
|ГДЕ
| ФизЛицо.Наименование = &Наименование
| И ФизЛицо.ДатаРождения = &ДатаРождения";
Запрос.УстановитьПараметр("Наименование", ФИОТекст);
Запрос.УстановитьПараметр("ДатаРождения", НачалоДня(ДатаРождения));
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Обратите внимание на использование функции НачалоДня(). В платформе 1С тип Дата всегда содержит время с точностью до секунды. Если у реквизита справочника в конфигураторе установлен состав даты «Дата», платформа сохраняет время как 00:00:00. Приведение введенной даты к началу дня гарантирует совпадение времени.
Иногда разработчики пытаются отсечь время непосредственно в тексте запроса с помощью встроенных функций платформы:
ГДЕ
НАЧАЛОПЕРИОДА(ФизЛицо.ДатаРождения, ДЕНЬ) = НАЧАЛОПЕРИОДА(&ДатаРождения, ДЕНЬ)
Выясним, почему так делать не рекомендуется: применение функций к полям таблиц в блоке ГДЕ делает выражение «не-SARGable». СУБД не может применить индекс по полю ДатаРождения и вынуждена выполнять полное сканирование таблицы (Table Scan), что существенно замедляет работу на больших объемах данных.
Для эффективного поиска по дате без потери производительности сформируем границы интервала во встроенном языке и используем оператор МЕЖДУ:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ФизЛицо.Ссылка,
| ФизЛицо.Наименование,
| ФизЛицо.ДатаРождения
|ИЗ
| Справочник.ФизическиеЛица КАК ФизЛицо
|ГДЕ
| ФизЛицо.ДатаРождения МЕЖДУ &ДатаНачала И &ДатаОкончания";
Запрос.УстановитьПараметр("ДатаНачала", НачалоДня(ДатаРождения));
Запрос.УстановитьПараметр("ДатаОкончания", КонецДня(ДатаРождения));
РезультатЗапроса = Запрос.Выполнить();
Если дату необходимо жестко зафиксировать в исходном коде или тексте запроса без передачи переменной, используются специальные литералы:
ДАТАВРЕМЯ(ГГГГ, ММ, ДД) или ДАТАВРЕМЯ(ГГГГ, ММ, ДД, ЧЧ, ММ, СС).
// Пример условия в запросе с литералом даты
ГДЕ
ФизЛицо.ДатаРождения = ДАТАВРЕМЯ(2018, 1, 1)
'ГГГГММДД' или 'ГГГГММДДЧЧММСС'.
// Пример инициализации даты в коде
ФиксированнаяДата = '20180101';
Запрос.УстановитьПараметр("ДатаРождения", ФиксированнаяДата);
Для обозначения пустой даты в языке запросов используется литерал ДАТАВРЕМЯ(1, 1, 1), а во встроенном языке — '00010101000000' или метод ПустаяДата().
Если требуется быстро найти элемент справочника по точному значению реквизита, можно воспользоваться методом менеджера справочника НайтиПоРеквизиту():
НайденнаяСсылка = Справочники.ФизическиеЛица.НайтиПоРеквизиту("ДатаРождения", НачалоДня(ДатаРождения));
Если ЗначениеЗаполнено(НайденнаяСсылка) Тогда
// Элемент успешно найден
КонецЕсли;
Посмотрим на ограничения этого метода: он возвращает только одну первую найденную ссылку и эффективен только в том случае, если для реквизита в конфигураторе установлено свойство «Индексировать».