При разработке отчетов и обработок в среде «1С:Предприятие» программисты часто сталкиваются с задачей фильтрации данных по определенному временному интервалу, например, за конкретный год. При этом сам год (число или строка) передается в процедуру в качестве входящего параметра. Начинающие разработчики часто пытаются использовать для этого привычную функцию ДАТАВРЕМЯ прямо в тексте запроса, что приводит к синтаксическим ошибкам. Другие используют неоптимальные конструкции, которые существенно замедляют работу системы на больших базах данных.
В этой статье мы подробно разберем, почему стандартные подходы могут не работать или работать медленно, проанализируем популярные ошибки и рассмотрим наиболее эффективные и архитектурно правильные способы решения этой задачи.
Разберем типичную ситуацию, с которой сталкиваются разработчики. Попытка написать в запросе конструкцию вида ДАТАВРЕМЯ(&Год, 1, 1) неизбежно вызовет ошибку компиляции запроса. Выясним причину такого поведения платформы.
Функция ДАТАВРЕМЯ в языке запросов «1С» предназначена исключительно для конструирования литералов (постоянных значений, констант). Она обрабатывается на этапе синтаксического анализа и трансляции запроса платформой. Это значит, что аргументами этой функции могут выступать только жестко прописанные числа (например, ДАТАВРЕМЯ(2023, 1, 1)). Использовать внутри этой функции параметры (такие как &Год), переменные или другие функции языка запросов категорически запрещено синтаксисом платформы.
Поэтому нам необходимо искать альтернативные пути решения. Рассмотрим их подробнее, двигаясь от самых неэффективных к наиболее оптимальным.
Иногда на форумах можно встретить совет обойти ограничение платформы путем прямой подстановки значения года в текст запроса перед его выполнением. Код такого решения выглядит следующим образом:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Договоры.Ссылка КАК Договор
|ИЗ
| Справочник.Договоры КАК Договоры
|ГДЕ
| Договоры.ДатаДоговора МЕЖДУ ДАТАВРЕМЯ(&Год, 1, 1) И ДАТАВРЕМЯ(&Год, 12, 31)";
// Динамически заменяем параметр в тексте запроса на реальное число
Запрос.Текст = СтрЗаменить(Запрос.Текст, "&Год", Формат(Год, "ЧГ=0"));
Проанализируем ситуацию и поймем, почему этот метод считается плохой практикой разработки:
Формат(Год, "ЧГ=0") для удаления пробелов, что усложняет код.Другой популярный совет — использовать встроенную функцию языка запросов ГОД(). На первый взгляд, это решение выглядит очень изящно и просто:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Договоры.Ссылка КАК Договор
|ИЗ
| Справочник.Договоры КАК Договоры
|ГДЕ
| ГОД(Договоры.ДатаДоговора) = &Год";
Запрос.УстановитьПараметр("Год", Год); // Передаем число, например, 2019
Давайте разберем, почему этот вариант содержит в себе скрытую «мину замедленного действия» для производительности вашей базы данных.
Поле ДатаДоговора в реальных базах данных практически всегда является индексированным (по нему строится индекс для быстрого поиска документов). Когда мы пишем условие сравнения, СУБД пытается использовать этот индекс. Однако, когда мы применяем к индексированному полю любую функцию (в данном случае функцию ГОД()), выражение становится так называемым Non-Sargable (не подлежащим оптимизации с помощью индексов).
СУБД не может применить индекс к результату вычисления функции динамически. Вместо быстрого поиска по индексу (Index Seek) СУБД будет вынуждена выполнить полное сканирование таблицы или индекса (Table Scan / Index Scan). Это означает, что система считает абсолютно все записи из таблицы договоров с диска в оперативную память, вычислит для каждого договора год и только потом сравнит его с параметром. Если в базе данных хранятся сотни тысяч или миллионы договоров, такой запрос приведет к колоссальной нагрузке на дисковую подсистему, процессор и вызовет длительные блокировки таблиц, мешая работе других пользователей.
Важное правило оптимизации: Левая часть условия сравнения в секции ГДЕ всегда должна оставаться «чистым» полем базы данных, без применения к нему каких-либо функций.
Мы можем использовать функции работы с датами платформы «1С» внутри самого запроса. Для этого в качестве параметра в запрос необходимо передать не число (год), а полноценную дату (например, начало этого года), после чего применить функции НАЧАЛОПЕРИОДА и КОНЕЦПЕРИОДА:
// Во встроенном языке формируем дату начала года
ДатаДляПараметра = Дата(Год, 1, 1);
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Договоры.Ссылка КАК Договор
|ИЗ
| Справочник.Договоры КАК Договоры
|ГДЕ
| Договоры.ДатаДоговора МЕЖДУ НАЧАЛОПЕРИОДА(&БазоваяДата, ГОД) И КОНЕЦПЕРИОДА(&БазоваяДата, ГОД)";
Запрос.УстановитьПараметр("БазоваяДата", ДатаДляПараметра);
Этот способ гораздо лучше предыдущих. Левая часть условия (Договоры.ДатаДоговора) остается «чистой», что позволяет СУБД эффективно использовать индексы. Однако функции НАЧАЛОПЕРИОДА и КОНЕЦПЕРИОДА все равно будут транслироваться в SQL-функции и вычисляться на стороне СУБД для переданного параметра.
Посмотрим, как можно сделать этот процесс еще более быстрым и оптимальным.
Самым правильным, быстрым и безопасным с точки зрения производительности подходом является предварительный расчет точных границ периода во встроенном языке «1С» еще до того, как запрос будет отправлен на выполнение.
Давайте посмотрим на пример реализации этого подхода:
// Предположим, на входе у нас есть числовое значение Год (например, 2019)
// 1. Конструируем дату начала года во встроенном языке.
// Функция Дата(Год, Месяц, День) вернет нам 01.01.2019 00:00:00
НачалоПериода = Дата(Год, 1, 1);
// 2. Определяем точную дату конца года с помощью встроенной функции платформы.
// Функция КонецГода() вернет нам 31.12.2019 23:59:59
КонецПериода = КонецГода(НачалоПериода);
// 3. Создаем запрос и передаем в него готовые «чистые» даты
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Договоры.Ссылка КАК Договор
|ИЗ
| Справочник.Договоры КАК Договоры
|ГДЕ
| Договоры.ДатаДоговора МЕЖДУ &НачалоПериода И &КонецПериода";
Запрос.УстановитьПараметр("НачалоПериода", НачалоПериода);
Запрос.УстановитьПараметр("КонецПериода", КонецПериода);
РезультатЗапроса = Запрос.Выполнить();
Рассмотрим подробнее преимущества данного подхода:
ДатаДоговора не обернуто в функции, СУБД мгновенно выполняет операцию поиска по диапазону индекса (Index Seek), считывая только нужные строки.YEAR() или приведение типов для каждой строки таблицы. Вычисление границ периода происходит ровно один раз на клиенте или сервере приложений «1С» силами встроенного языка.При необходимости передать год в запрос всегда стремитесь перенести любые вычисления и подготовку данных из самого запроса во встроенный язык «1С». Помните, что СУБД должна заниматься только поиском и выборкой данных, а не сложными вычислениями дат для условий фильтрации. Предварительное формирование параметров &НачалоПериода и &КонецПериода с помощью функций встроенного языка Дата() и КонецГода() — это залог быстродействия и стабильности вашей информационной системы.