Даты в языке запросов 1С: литералы, параметры, периоды и фильтры

· 10 мин

Содержание

Дату в запросе 1С указывают двумя способами. Константой — через ДАТАВРЕМЯ(2024, 1, 15). Или параметром — &ДатаНачала, значение которого передают из кода методом Запрос.УстановитьПараметр(). В рабочем коде почти всегда нужен второй вариант: он гибче и не ломается при смене периода.

Самая частая беда с датами в запросе — потерянный последний день периода. Конец периода приходит как 00:00:00, и документы за 31-е число не попадают в отчёт. Лечится это одной строкой, и про неё ниже по тексту.

Дата константой в тексте запроса: функция ДАТАВРЕМЯ

В языке запросов нет строкового литерала даты. Написать '15.01.2024' или "20240115" и сравнить с полем Дата не получится. Для константы есть функция ДАТАВРЕМЯ (англ. DATETIME).

Синтаксис:

ДАТАВРЕМЯ(Год, Месяц, День)
ДАТАВРЕМЯ(Год, Месяц, День, Час, Минута, Секунда)

Пример — документы реализации за 15 января 2024 года:

ВЫБРАТЬ
    Реализация.Ссылка,
    Реализация.Дата
ИЗ
    Документ.РеализацияТоваровУслуг КАК Реализация
ГДЕ
    Реализация.Дата МЕЖДУ ДАТАВРЕМЯ(2024, 1, 15) И ДАТАВРЕМЯ(2024, 1, 15, 23, 59, 59)

Что важно знать про ДАТАВРЕМЯ:

  • 🔢 Аргументы — только числовые константы. Переменную, параметр или поле туда подставить нельзя: запрос не пройдёт синтаксический контроль.
  • 🕛 Если время не указано, берётся 00:00:00. ДАТАВРЕМЯ(2024, 1, 31) — это начало суток 31 января, а не весь день.
  • ⬜ Пустая дата записывается как ДАТАВРЕМЯ(1, 1, 1). Так проверяют незаполненные реквизиты.

Пример отбора документов с незаполненной датой оплаты:

ГДЕ
    Счет.ДатаОплаты = ДАТАВРЕМЯ(1, 1, 1)

Константу оправдано писать в разовом запросе в консоли или в условии, которое никогда не меняется. Например, «дата начала учёта в базе». Во всех остальных случаях — параметр.

Параметры запроса: как передать дату из кода

Параметр в тексте запроса начинается с амперсанда: &НачалоПериода. Значение задают до выполнения запроса. Это основной способ указать дату в запросе 1С для отчётов, обработок и печатных форм.

Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
|    Реализация.Ссылка,
|    Реализация.Дата,
|    Реализация.СуммаДокумента
|ИЗ
|    Документ.РеализацияТоваровУслуг КАК Реализация
|ГДЕ
|    Реализация.Дата МЕЖДУ &НачалоПериода И &КонецПериода";

Запрос.УстановитьПараметр("НачалоПериода", НачалоДня(ДатаНач));
Запрос.УстановитьПараметр("КонецПериода", КонецДня(ДатаКон));

Выборка = Запрос.Выполнить().Выбрать();

Почему именно так:

  • ✅ НачалоДня() и КонецДня() срезают время на стороне встроенного языка. Если пользователь выбрал даты в поле ввода без времени, ДатаКон придёт как 00:00:00. Без КонецДня() документы последнего дня выпадут из выборки.
  • ✅ Один текст запроса работает на любом периоде. Его не нужно пересобирать склейкой строк.
  • ✅ Платформа сама передаёт значение в СУБД в нужном формате. Нет риска ошибиться с форматом даты конкретного сервера.

Не подставляйте дату в текст запроса через конкатенацию строк вроде "... ГДЕ Дата > " + Строка(Дата). Строковое представление даты зависит от локали и в запросе не распознаётся как дата.

Период из поля «Стандартный период»

Если на форме реквизит типа СтандартныйПериод, параметры берут из его свойств:

Запрос.УстановитьПараметр("НачалоПериода", Период.ДатаНачала);
Запрос.УстановитьПараметр("КонецПериода", Период.ДатаОкончания);

ДатаОкончания у стандартного периода уже приходит концом дня, дополнительно оборачивать её в КонецДня() не нужно, но и вреда не будет.

Дата в запросе СКД

В схеме компоновки данных параметр пишут так же — &НачалоПериода. Его значение пользователь задаёт в настройках отчёта. Частый приём: завести в СКД параметр Период типа СтандартныйПериод, а у параметров НачалоПериода и КонецПериода указать выражения &Период.ДатаНачала и &Период.ДатаОкончания и снять им флаг доступности для пользователя.

Функции языка запросов для работы с датами

Внутри запроса дату можно разобрать на части, округлить до периода и сдвинуть. Основные функции:

ФункцияЧто делаетПример
НАЧАЛОПЕРИОДА(Дата, Период)Начало минуты, часа, дня, недели, месяца, квартала, года, декады, полугодияНАЧАЛОПЕРИОДА(Дата, МЕСЯЦ)
КОНЕЦПЕРИОДА(Дата, Период)Последняя секунда того же периодаКОНЕЦПЕРИОДА(&Дата, ДЕНЬ)
ДОБАВИТЬКДАТЕ(Дата, Период, Число)Сдвиг даты на N периодов, N может быть отрицательнымДОБАВИТЬКДАТЕ(&Дата, ДЕНЬ, -30)
РАЗНОСТЬДАТ(Дата1, Дата2, Период)Разница между датами в секундах, минутах, часах, днях, месяцах, кварталах или годахРАЗНОСТЬДАТ(Дата, &ТекДата, ДЕНЬ)
ГОД(), КВАРТАЛ(), МЕСЯЦ()Номер года, квартала, месяцаМЕСЯЦ(Дата) = 12
ДЕНЬ(), ДЕНЬГОДА()День месяца и порядковый день в годуДЕНЬ(Дата)
НЕДЕЛЯ(), ДЕНЬНЕДЕЛИ()Номер недели в году и день недели (1 — понедельник, 7 — воскресенье)ДЕНЬНЕДЕЛИ(Дата) В (6, 7)
ЧАС(), МИНУТА(), СЕКУНДА()Части времениЧАС(Дата) >= 18

Группировка продаж по месяцам

Классическая задача — свернуть документы по месяцам. Дату округляют до начала месяца и группируют по результату:

ВЫБРАТЬ
    НАЧАЛОПЕРИОДА(Реализация.Дата, МЕСЯЦ) КАК Месяц,
    СУММА(Реализация.СуммаДокумента) КАК Сумма
ИЗ
    Документ.РеализацияТоваровУслуг КАК Реализация
ГДЕ
    Реализация.Дата МЕЖДУ &НачалоПериода И &КонецПериода
    И Реализация.Проведен
СГРУППИРОВАТЬ ПО
    НАЧАЛОПЕРИОДА(Реализация.Дата, МЕСЯЦ)
УПОРЯДОЧИТЬ ПО
    Месяц

Поле Месяц в результате — это дата 01.ММ.ГГГГ 00:00:00. Её удобно выводить с форматом ДФ='ММММ гггг' и сортировать без лишних преобразований.

Месяцы без продаж: ИТОГИ ПО ПЕРИОДАМИ

Обычная группировка не покажет месяц, в котором не было ни одного документа. Если в отчёте нужна сплошная шкала, используют ИТОГИ ... ПО ... ПЕРИОДАМИ:

ИТОГИ
    СУММА(Сумма)
ПО
    Месяц ПЕРИОДАМИ(МЕСЯЦ, &НачалоПериода, &КонецПериода)

Пустые периоды появятся при обходе результата с типом обхода ОбходРезультатаЗапроса.ПоГруппировкам и выборке с параметром "ВСЕ" для группировки по периоду. Этот приём часто забывают, а потом дорисовывают пропуски в коде.

Отбор за период и сравнение дат

Для сравнения дат в условии ГДЕ подходят обычные операторы: =, <>, <, >, <=, >= и конструкция МЕЖДУ ... И ....

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

МЕЖДУ включает обе границы. Поэтому МЕЖДУ &Начало И &Конец с &Конец = 31.01.2024 00:00:00 отберёт документы до полуночи 31 января и пропустит весь день 31-го.

Надёжный вариант — полуоткрытый интервал. Нижняя граница включается, верхняя нет:

ГДЕ
    Реализация.Дата >= &НачалоПериода
    И Реализация.Дата < &НачалоСледующегоПериода
Запрос.УстановитьПараметр("НачалоПериода", НачалоМесяца(Дата));
Запрос.УстановитьПараметр("НачалоСледующегоПериода", ДобавитьМесяц(НачалоМесяца(Дата), 1));

Такое условие не зависит от того, есть ли у даты время, и не теряет документы, проведённые в 23:59:59.

Сравнение двух полей-дат

Даты разных полей сравнивают так же, как числа. Пример — заказы, отгруженные позже обещанного срока, с числом дней задержки:

ВЫБРАТЬ
    Заказ.Ссылка,
    РАЗНОСТЬДАТ(Заказ.ДатаОтгрузки, Реализация.Дата, ДЕНЬ) КАК ДнейЗадержки
ИЗ
    Документ.ЗаказКлиента КАК Заказ
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг КАК Реализация
    ПО Реализация.ЗаказКлиента = Заказ.Ссылка
ГДЕ
    Реализация.Дата > КОНЕЦПЕРИОДА(Заказ.ДатаОтгрузки, ДЕНЬ)

Имена объектов и реквизитов здесь условные: в вашей конфигурации документ заказа и реквизит связи могут называться иначе. Смысл приёма — КОНЕЦПЕРИОДА превращает дату обещания в конец дня, чтобы отгрузка в тот же день вечером не считалась просрочкой.

РАЗНОСТЬДАТ(Д1, Д2, ДЕНЬ) возвращает Д2 − Д1. Если перепутать порядок аргументов, задержки станут отрицательными.

Плавающий период: «последние 30 дней», «текущий месяц», «прошлый квартал»

Скользящие периоды задают двумя путями. Либо вычисляют границы в коде и передают параметрами. Либо передают одну опорную дату, а границы считают в самом запросе.

Опорную дату почти всегда передают параметром. Функции «текущая дата» в языке запросов нет, а брать её в коде лучше через ТекущаяДатаСеанса(): она учитывает часовой пояс сеанса. ТекущаяДата() возвращает время компьютера, на котором выполняется код, — в клиент-серверном варианте это время сервера 1С.

Примеры границ, вычисляемых в запросе от параметра &ТекДата:

ПериодНачалоКонец
Последние 30 днейДОБАВИТЬКДАТЕ(НАЧАЛОПЕРИОДА(&ТекДата, ДЕНЬ), ДЕНЬ, -29)КОНЕЦПЕРИОДА(&ТекДата, ДЕНЬ)
Текущий месяцНАЧАЛОПЕРИОДА(&ТекДата, МЕСЯЦ)КОНЕЦПЕРИОДА(&ТекДата, МЕСЯЦ)
Прошлый месяцНАЧАЛОПЕРИОДА(ДОБАВИТЬКДАТЕ(&ТекДата, МЕСЯЦ, -1), МЕСЯЦ)КОНЕЦПЕРИОДА(ДОБАВИТЬКДАТЕ(&ТекДата, МЕСЯЦ, -1), МЕСЯЦ)
Прошлый кварталНАЧАЛОПЕРИОДА(ДОБАВИТЬКДАТЕ(&ТекДата, КВАРТАЛ, -1), КВАРТАЛ)КОНЕЦПЕРИОДА(ДОБАВИТЬКДАТЕ(&ТекДата, КВАРТАЛ, -1), КВАРТАЛ)
С начала годаНАЧАЛОПЕРИОДА(&ТекДата, ГОД)КОНЕЦПЕРИОДА(&ТекДата, ДЕНЬ)

Проверим первую строку. Если &ТекДата — 15 марта, начало периода — 15 марта минус 29 дней, то есть 15 февраля в високосном году и 14 февраля в обычном. Вместе с сегодняшним днём это ровно 30 календарных дней.

Пример — контрагенты без отгрузок 90 дней и дольше:

ВЫБРАТЬ
    Реализация.Контрагент,
    МАКСИМУМ(Реализация.Дата) КАК ПоследняяОтгрузка
ИЗ
    Документ.РеализацияТоваровУслуг КАК Реализация
ГДЕ
    Реализация.Проведен
СГРУППИРОВАТЬ ПО
    Реализация.Контрагент
ИМЕЮЩИЕ
    МАКСИМУМ(Реализация.Дата) < ДОБАВИТЬКДАТЕ(&ТекДата, ДЕНЬ, -90)

Если границы периода нужны и в запросе, и в заголовке отчёта, считайте их в коде. Тогда «Продажи за март» в шапке и цифры в таблице гарантированно относятся к одному периоду.

Строка в дату и дата в строку

Прямого преобразования строки в дату в языке запросов нет. ВЫРАЗИТЬ(Поле КАК ДАТА) строку "15.01.2024" в дату не превратит. Поэтому строку переводят в дату до запроса, во встроенном языке.

Варианты во встроенном языке:

  • 📅 Формат ГГГГММДД или ГГГГММДДччммсс конструктор понимает напрямую: Дата("20240115") или Дата("20240115143000").
  • 📅 Строку вида 15.01.2024 разбирают на части и собирают через Дата(Год, Месяц, День):
Части = СтрРазделить("15.01.2024", ".");
ДатаЗначение = Дата(Число(Части[2]), Число(Части[1]), Число(Части[0]));
Запрос.УстановитьПараметр("Дата", ДатаЗначение);
  • 📅 Дату из XML и JSON в формате 2024-01-15T00:00:00 читают через XMLЗначение(Тип("Дата"), Строка).

Обратное преобразование — дату в строку — тоже удобнее делать после выполнения запроса:

Формат(Выборка.Дата, "ДФ=dd.MM.yyyy")

В отчёте на СКД формат задают в оформлении поля, в табличном документе — в свойствах ячейки или через Формат(). Внутри запроса части даты можно получить числами (ГОД(), МЕСЯЦ(), ДЕНЬ()), но склеивать из них строку ради вывода смысла нет.

Если в запросе нужно сравнить с датой, хранящейся в строковом реквизите, — это повод исправить структуру данных. Хранить дату в строке и разбирать её в каждом запросе медленно и ненадёжно.

Как не замедлить запрос условием на дату

Условие на дату — самое частое в отчётах, и от его формы зависит скорость. У таблиц документов платформа сама строит индекс по полю Дата. Регистры индексируются по Период. Индекс используется, только если поле сравнивается «как есть».

МедленноБыстро
ГОД(Дата) = 2024Дата >= &НачалоГода И Дата < &НачалоСледующегоГода
НАЧАЛОПЕРИОДА(Дата, ДЕНЬ) = &ДеньДата МЕЖДУ НАЧАЛОПЕРИОДА(&День, ДЕНЬ) И КОНЕЦПЕРИОДА(&День, ДЕНЬ)
МЕСЯЦ(Дата) = 3 И ГОД(Дата) = 2024Дата МЕЖДУ &НачалоМарта И &КонецМарта
Условие ГДЕ Период МЕЖДУ ... к виртуальной таблице оборотовПериод в параметрах виртуальной таблицы

Функция над полем заставляет СУБД вычислить её для каждой строки — индекс не помогает. Функция над параметром вычисляется один раз, поэтому правый столбец быстрый.

Виртуальные таблицы регистров

Для регистров период передают в параметры виртуальной таблицы, а не в ГДЕ:

ВЫБРАТЬ
    Продажи.Номенклатура,
    Продажи.СуммаОборот
ИЗ
    РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода, , ) КАК Продажи

Так платформа сразу берёт данные из таблицы итогов за нужный период. Условие в ГДЕ применится уже к готовому результату, и СУБД перелопатит лишнее.

Остатки на дату: граница

С остатками есть ловушка. Остатки(&Дата) возвращает остаток на начало указанного момента. Если &Дата = 31.01.2024 00:00:00, движения за 31 января не попадут.

Два правильных варианта:

// остаток на конец дня
Запрос.УстановитьПараметр("Дата", КонецДня(Дата));

// или граница, включающая сам момент
Запрос.УстановитьПараметр("Дата", Новый Граница(КонецДня(Дата), ВидГраницы.Включая));

Когда нужен остаток именно на момент документа — например, при проведении — передают Новый Граница(Документ.МоментВремени(), ВидГраницы.Исключая). Тогда учитываются все движения до документа, включая другие документы того же дня и секунды, но без движений самого документа.

Проверить план выполнения можно средствами СУБД. В MS SQL — через SQL Server Profiler или фактический план в Management Studio, в PostgreSQL — через EXPLAIN ANALYZE для запроса, пойманного в технологическом журнале. Если в плане по таблице документов сканирование всей таблицы, а в запросе функция над датой, — причина найдена.

Ошибки с датами, которые чаще всего ломают выборку

Диагностика: выберите симптом

Ещё одна тонкость — тип параметра. Если передать в &Дата значение Неопределено (например, поле на форме не заполнено), условие Дата >= &Дата не отберёт ничего, и пользователь увидит пустой отчёт без ошибки. Перед выполнением проверяйте, что дата заполнена, и выводите понятное сообщение.

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

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