Отбор по датам в языке запросов 1С 8.3: от МЕЖДУ до Граница
Содержание
Период в запросе 1С задают двумя параметрами: в тексте пишут ГДЕ Документ.Дата МЕЖДУ &НачалоПериода И &КонецПериода, а в коде передают значения через Запрос.УстановитьПараметр(). Для конца периода передают не голую дату, а КонецДня(Дата): иначе документы последнего дня, проведённые после 00:00:00, в выборку не попадут.
Самое частое заблуждение звучит так: «указал 31 января — значит, январь целиком». Нет. Дата 31.01.2024 в 1С — это 31.01.2024 0:00:00. Документ от 31.01.2024 14:25:10 больше этого значения и в отбор не войдёт. Ниже — как задавать период так, чтобы выборка была точной: в обычных таблицах, в виртуальных таблицах регистров, в СКД и в динамически собранном коде. Всё описанное относится к платформе 1С:Предприятие 8.3 и не зависит от конфигурации.
Период через параметры запроса: базовая схема
Даты в текст запроса не вклеивают строкой. Их передают параметрами — платформа сама подставит значение нужного типа, а запрос не сломается из-за формата даты в региональных настройках.
Минимальный рабочий пример — реализация товаров за месяц:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Реализация.Ссылка КАК Документ,
| Реализация.Дата КАК Дата,
| Реализация.СуммаДокумента КАК Сумма
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Реализация
|ГДЕ
| Реализация.Дата МЕЖДУ &НачалоПериода И &КонецПериода
| И Реализация.Проведен
|
|УПОРЯДОЧИТЬ ПО
| Дата";
Запрос.УстановитьПараметр("НачалоПериода", НачалоМесяца(ДатаОтбора));
Запрос.УстановитьПараметр("КонецПериода", КонецМесяца(ДатаОтбора));
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Сообщить(Формат(Выборка.Дата, "ДФ=dd.MM.yyyy") + " — " + Формат(Выборка.Сумма, "ЧДЦ=2"));
КонецЦикла;
Что здесь важно:
- 📌 имя параметра в тексте начинается с
&, а вУстановитьПараметр()пишется без него; - 📌
НачалоМесяца()иКонецМесяца()возвращают01.01.2024 0:00:00и31.01.2024 23:59:59— граница покрывает весь месяц; - 📌 параметр, который есть в тексте, но не установлен в коде, приводит к ошибке выполнения запроса.
Если параметр нужен в нескольких пакетах одного запроса, устанавливают его один раз — он действует на весь пакет.
Оператор МЕЖДУ и сравнения: где теряются записи
МЕЖДУ … И … включает обе границы. Запись Дата МЕЖДУ &Н И &К равносильна Дата >= &Н И Дата <= &К. Ошибки возникают не из-за самого оператора, а из-за значений, которые в него приходят.
| Что передали в &КонецПериода | Что реально попадёт в выборку |
|---|---|
31.01.2024 (дата без времени) | только документы от 31.01 ровно в 0:00:00 |
КонецДня(31.01.2024) | весь день 31 января, до 23:59:59 |
КонецМесяца(...) | весь последний день месяца |
Пустая дата '00010101' | ничего: условие не выполнится ни для одной записи |
Вторая типичная ловушка — полуоткрытый интервал. Иногда удобнее писать Дата >= &НачалоПериода И Дата < &НачалоСледующегоПериода. Такой вариант исключает путаницу с 23:59:59 и хорошо работает при отборе «по месяцам подряд»: конец одного периода совпадает с началом следующего, но запись не задваивается.
"ГДЕ
| Док.Дата >= &НачалоПериода
| И Док.Дата < &НачалоСледующего"
Запрос.УстановитьПараметр("НачалоПериода", НачалоМесяца(ДатаОтбора));
Запрос.УстановитьПараметр("НачалоСледующего", ДобавитьМесяц(НачалоМесяца(ДатаОтбора), 1));
Функции периода прямо в тексте запроса
Если в запрос пришла одна дата, границы можно вычислить на стороне СУБД:
"ГДЕ
| Док.Дата МЕЖДУ НАЧАЛОПЕРИОДА(&Дата, МЕСЯЦ) И КОНЕЦПЕРИОДА(&Дата, МЕСЯЦ)"
Вторым аргументом НАЧАЛОПЕРИОДА и КОНЕЦПЕРИОДА принимают МИНУТА, ЧАС, ДЕНЬ, НЕДЕЛЯ, МЕСЯЦ, КВАРТАЛ, ГОД, ДЕКАДА, ПОЛУГОДИЕ. Для сдвига служит ДОБАВИТЬКДАТЕ(&Дата, МЕСЯЦ, -1), для разницы — РАЗНОСТЬДАТ(Дата1, Дата2, ДЕНЬ). Константная дата в тексте пишется так: ДАТАВРЕМЯ(2024, 1, 31, 23, 59, 59).
Не оборачивайте в функцию само поле таблицы ради отбора: условие НАЧАЛОПЕРИОДА(Док.Дата, ДЕНЬ) = &Дата не даёт СУБД использовать индекс по дате, и на большой базе запрос пойдёт полным сканированием. Функцию применяйте к параметру, а поле оставляйте «голым»: Док.Дата МЕЖДУ НАЧАЛОПЕРИОДА(&Дата, ДЕНЬ) И КОНЕЦПЕРИОДА(&Дата, ДЕНЬ).
Граница периода: точный момент для остатков
Когда нужны остатки, одной даты часто мало. Документы внутри одного дня, а тем более внутри одной секунды, упорядочены по моменту времени. Чтобы указать, включать ли движения в самой точке, используют объект Граница.
Конструктор принимает дату или момент времени и вид границы:
// Остатки с учётом всех движений за 31 января
ГраницаВключая = Новый Граница(КонецДня(Дата), ВидГраницы.Включая);
// Остатки на момент документа — без его собственных движений
ГраницаДоДокумента = Новый Граница(ДокументСсылка.МоментВремени(), ВидГраницы.Исключая);
Запрос.УстановитьПараметр("Период", ГраницаДоДокумента);
Сравнение вариантов:
| Что передано в параметр | Какие движения учтены |
|---|---|
Дата 31.01.2024 0:00:00 | всё, что было строго до этой секунды — 31 января не учтено |
КонецДня(31.01.2024) | всё до 23:59:59 31 января, кроме движений ровно в эту секунду |
Новый Граница(КонецДня(...), ВидГраницы.Включая) | весь день 31 января полностью |
Новый Граница(МоментВремени, ВидГраницы.Исключая) | всё, что было до документа, без его движений |
Новый Граница(МоментВремени, ВидГраницы.Включая) | всё, включая движения самого документа |
Вариант с МоментВремени() и Исключая — стандартный приём при контроле остатков в проведении. Он даёт «состояние склада перед этим документом», даже если в ту же секунду проведено ещё пять документов.
Момент времени — это пара «дата + ссылка на документ». Поэтому граница по моменту точнее границы по дате: она различает документы с одинаковым временем.
Период в виртуальных таблицах регистров
У виртуальных таблиц период задают не в ГДЕ, а в параметрах таблицы — в скобках после имени. Так платформа считает итоги сразу за нужный интервал и не тащит лишние строки.
Остатки на дату
"ВЫБРАТЬ
| ТоварыОстатки.Номенклатура,
| ТоварыОстатки.КоличествоОстаток
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(&Период, Склад = &Склад) КАК ТоварыОстатки"
Если параметр &Период не заполнен (пустая дата), таблица вернёт текущие остатки. Это удобно, но и опасно: забытый параметр не вызовет ошибку, а молча даст остатки «на сейчас».
Обороты за интервал
"ВЫБРАТЬ
| Продажи.Номенклатура,
| Продажи.Период,
| Продажи.КоличествоОборот
|ИЗ
| РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода, Месяц, ) КАК Продажи"
Параметры по порядку: начало, конец, периодичность, условие. Периодичность бывает Авто, Запись, Регистратор, День, Неделя, Декада, Месяц, Квартал, Полугодие, Год, Период. При периодичности Месяц в выборке появится поле Период с началом каждого месяца.
Остатки и обороты
У таблицы ОстаткиИОбороты пять параметров: начало, конец, периодичность, метод дополнения и условие. Метод дополнения Движения выводит только периоды, где были движения. ДвиженияИГраницыПериода добавляет строки на границах интервала, даже если движений там не было.
"ИЗ
| РегистрНакопления.ТоварыНаСкладах.ОстаткиИОбороты(
| &НачалоПериода,
| &КонецПериода,
| День,
| ДвиженияИГраницыПериода,
| Склад = &Склад) КАК ТоварыОО"
Сюда тоже можно передать Граница вместо даты, если концы интервала должны учитывать конкретный момент.
Регистры сведений: срез последних
Для периодического регистра сведений период — один параметр среза:
"ИЗ
| РегистрСведений.КурсыВалют.СрезПоследних(&Дата, Валюта = &Валюта) КАК Курсы"
СрезПоследних берёт последнюю запись с периодом меньше или равным переданной дате. Если курс установлен 31.01 в 0:00:00, а вы передали 31.01.2024, курс за 31.01 будет найден. СрезПервых работает зеркально — ищет первую запись начиная с даты.
Дата без времени и дата со временем: как не ошибиться
У реквизита типа Дата есть квалификатор состава: Дата, Время или ДатаВремя. Реквизит с составом Дата хранит время 0:00:00 всегда. Реквизит Дата у документа — всегда дата и время.
Отсюда правила:
- 📅 поле с составом
Датаможно сравнивать с датой без времени:Сотрудник.ДатаПриема = &Датасработает; - 🕐 поле
Документ.Датасравнивать на равенство с датой без времени бессмысленно — используйте интервал от начала до конца дня; - 🔁 при переходе на интервал
МЕЖДУ НАЧАЛОПЕРИОДА(&Дата, ДЕНЬ) И КОНЕЦПЕРИОДА(&Дата, ДЕНЬ)оба типа полей отбираются корректно.
Если дата приходит из поля формы со составом Дата, в ней нет времени. Перед передачей в запрос как конец периода её обязательно доводят КонецДня().
Группировка по периодам в отчётах тоже строится на этих функциях:
"ВЫБРАТЬ
| НАЧАЛОПЕРИОДА(Док.Дата, МЕСЯЦ) КАК Месяц,
| СУММА(Док.СуммаДокумента) КАК Сумма
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Док
|ГДЕ
| Док.Дата МЕЖДУ &НачалоПериода И &КонецПериода
|СГРУППИРОВАТЬ ПО
| НАЧАЛОПЕРИОДА(Док.Дата, МЕСЯЦ)"
А чтобы в итогах появились и месяцы без продаж, добавляют ИТОГИ ПО Месяц ПЕРИОДАМИ(МЕСЯЦ, &НачалоПериода, &КонецПериода). Пустые периоды появятся при обходе результата так: Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Месяц", "ВСЕ").
Как собрать период в коде динамически
На практике период приходит из формы: пользователь выбирает «Этот месяц», «Прошлый квартал» или произвольный интервал. Для этого у формы есть реквизит типа СтандартныйПериод.
// Реквизит формы ПериодОтчета, тип СтандартныйПериод
Запрос.УстановитьПараметр("НачалоПериода", ПериодОтчета.ДатаНачала);
Запрос.УстановитьПараметр("КонецПериода", ПериодОтчета.ДатаОкончания);
Сам объект СтандартныйПериод в запрос не передают — только его свойства ДатаНачала и ДатаОкончания. При выборе стандартного варианта вроде «Этот месяц» дата окончания уже содержит 23:59:59.
Типовые периоды относительно текущей даты:
| Нужный период | Начало | Конец |
|---|---|---|
| Сегодня | НачалоДня(ТекущаяДатаСеанса()) | КонецДня(ТекущаяДатаСеанса()) |
| Текущий месяц | НачалоМесяца(ТекущаяДатаСеанса()) | КонецМесяца(ТекущаяДатаСеанса()) |
| Прошлый месяц | НачалоМесяца(ДобавитьМесяц(ТекущаяДатаСеанса(), -1)) | КонецМесяца(ДобавитьМесяц(ТекущаяДатаСеанса(), -1)) |
| Текущий квартал | НачалоКвартала(ТекущаяДатаСеанса()) | КонецКвартала(ТекущаяДатаСеанса()) |
| С начала года | НачалоГода(ТекущаяДатаСеанса()) | КонецДня(ТекущаяДатаСеанса()) |
| Последние 7 дней | НачалоДня(ТекущаяДатаСеанса()) - 6 * 86400 | КонецДня(ТекущаяДатаСеанса()) |
Дата в 1С сдвигается на число секунд: Дата + 86400 — плюс сутки. Для месяцев и лет используйте ДобавитьМесяц(), потому что длина месяца разная.
На сервере берите ТекущаяДатаСеанса(), а не ТекущаяДата(): первая учитывает часовой пояс сеанса, вторая возвращает время компьютера, где выполняется код.
Необязательные границы
Если пользователь может оставить начало или конец пустым, условие строят так, чтобы пустая дата отключала отбор:
"ГДЕ
| (&НачалоПериода = ДАТАВРЕМЯ(1, 1, 1) ИЛИ Док.Дата >= &НачалоПериода)
| И (&КонецПериода = ДАТАВРЕМЯ(1, 1, 1) ИЛИ Док.Дата <= &КонецПериода)"
Работает, но на больших таблицах условие с ИЛИ хуже использует индекс. Альтернатива — подставлять крайние значения в коде:
Начало = ?(ЗначениеЗаполнено(ПериодОтчета.ДатаНачала), ПериодОтчета.ДатаНачала, Дата(1, 1, 1));
Конец = ?(ЗначениеЗаполнено(ПериодОтчета.ДатаОкончания), ПериодОтчета.ДатаОкончания, Дата(3999, 12, 31, 23, 59, 59));
Запрос.УстановитьПараметр("НачалоПериода", Начало);
Запрос.УстановитьПараметр("КонецПериода", Конец);
Тогда в тексте остаётся простое МЕЖДУ, и СУБД спокойно идёт по индексу.
Период в схеме компоновки данных
В СКД параметры &НачалоПериода и &КонецПериода из текста запроса появляются на вкладке «Параметры» автоматически. Обычно добавляют ещё параметр Период типа СтандартныйПериод и выводят в пользовательские настройки только его. Двум другим задают выражения &Период.ДатаНачала и &Период.ДатаОкончания и снимают у них признак доступности пользователю. Пользователь видит одно поле выбора периода, а запрос получает две даты.
Ошибки при отборе по датам и как их найти
Ещё одна ошибка встречается при сборке текста запроса строкой. Код вида "ГДЕ Док.Дата >= " + Строка(Дата) не работает: строковое представление даты зависит от настроек и не является выражением языка запросов. Если без вклейки не обойтись, используйте параметр или функцию ДАТАВРЕМЯ(Год, Месяц, День) с числами, собранными через Формат(Число, "ЧГ=0").