Отбор по датам в языке запросов 1С 8.3: от МЕЖДУ до Граница

· 9 мин

Содержание

Период в запросе 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").

Частые вопросы

Можно ли передать в запрос сразу период, а не две даты?
Нет. Объект СтандартныйПериод в параметр запроса не передают — используйте его свойства ДатаНачала и ДатаОкончания. В СКД период разбирают выражениями параметров.
Включает ли МЕЖДУ крайние значения?
Да, обе границы включаются. Запись Дата МЕЖДУ &Н И &К равна Дата >= &Н И Дата <= &К.
Чем отличается условие в параметрах виртуальной таблицы от условия в ГДЕ?
Условие в параметрах применяется до расчёта итогов, поэтому работает быстрее и даёт правильные остатки. Условие в ГДЕ отбирает уже рассчитанные строки.
Как получить остатки на конец каждого дня за период?
Используйте таблицу ОстаткиИОбороты с периодичностью День и методом дополнения ДвиженияИГраницыПериода, поле КонечныйОстаток даст остаток на конец дня.
Как отобрать документы за конкретный день?
Передайте НачалоДня(Дата) и КонецДня(Дата) или в тексте запроса используйте МЕЖДУ НАЧАЛОПЕРИОДА(&Дата, ДЕНЬ) И КОНЕЦПЕРИОДА(&Дата, ДЕНЬ).