Работа с периодами в запросах 1С:Предприятие — одна из самых востребованных задач при разработке отчетов, обработок и аналитических систем. Неправильно заданный интервал может привести к искажению данных, замедлению выполнения запроса или даже ошибкам в бухгалтерской отчетности. Эта статья поможет разобраться, как корректно указывать периоды в разных сценариях: от простых отборов по дате до сложных динамических условий с использованием виртуальных таблиц.
Особенность работы с периодами в 1С заключается в том, что система оперирует не только календарными датами, но и специальными объектами типа Дата, МоментВремени, а также предопределенными интервалами (например, НачалоДня() или КонецМесяца()). Мы рассмотрим все варианты — от ручного ввода дат до программного расчета границ периода на основе текущей сессии или пользовательских настроек.
Статья будет полезна как начинающим разработчикам, так и опытным программистам 1С, которые хотят оптимизировать свои запросы. Все примеры приведены для актуальных версий платформы 1С:Предприятие 8.3 (включая последние релизы), но большинство подходов применимы и к более ранним версиям.
Базовый синтаксис указания периода в запросе
Самый простой способ ограничить выборку по дате — использовать конструкцию ГДЕ с сравнением полей типа Дата. Например, чтобы получить документы за конкретный день, достаточно написать:
ВЫБРАТЬ
Документ.Ссылка КАК Ссылка,
Документ.Дата КАК Дата
ИЗ
Документ.ПоступлениеТоваров КАК Документ
ГДЕ
Документ.Дата = &ДатаНачала
Где &ДатаНачала — параметр запроса, который можно передать из кода или интерактивно запросить у пользователя. Однако такой подход имеет ограничения:
- 📅 Не учитывает время (если поле содержит
ДатаВремя, сравнение будет точным до секунды). - 🔍 Не включает документы с датой позднее указанной (только точные совпадения).
- ⚡ Может работать медленно на больших базах из-за отсутствия индексов по дате.
Для интервалов чаще используют диапазон с операторами >= и <=:
ВЫБРАТЬ
Документ.Номер,
Документ.Дата
ИЗ
Документ.РеализацияТоваровУслуг КАК Документ
ГДЕ
Документ.Дата >= &ДатаНачала
И Документ.Дата <= &ДатаОкончания
Такой запрос вернет все документы, попадающие в указанный промежуток включительно. Обратите внимание, что для полей типа ДатаВремя границы периода лучше задавать с учетом начала и конца дня:
ГДЕ
Документ.Дата >= НачалоДня(&ДатаНачала)
И Документ.Дата <= КонецДня(&ДатаОкончания)
Если в запросе участвуют виртуальные таблицы (например, Документ.РеализацияТоваровУслуг.Обороты), всегда проверяйте, поддерживает ли таблица отбор по дате на уровне СУБД. В противном случае фильтрация будет выполняться на стороне 1С, что значительно замедлит работу.
Использование предопределенных функций для периодов
Платформа 1С:Предприятие предоставляет ряд встроенных функций для работы с датами, которые упрощают задание периодов. Их можно использовать как в самих запросах, так и при формировании параметров перед выполнением.
| Функция | Описание | Пример использования |
|---|---|---|
НачалоДня() |
Возвращает дату с временем 00:00:00 | Дата >= НачалоДня(&ТекущаяДата) |
КонецДня() |
Возвращает дату с временем 23:59:59 | Дата <= КонецДня(&ТекущаяДата) |
НачалоМесяца() |
Первый день месяца с временем 00:00:00 | Дата >= НачалоМесяца(&ТекущаяДата) |
КонецМесяца() |
Последний день месяца с временем 23:59:59 | Дата <= КонецМесяца(&ТекущаяДата) |
ДобавитьМесяц() |
Смещает дату на указанное количество месяцев | Дата >= ДобавитьМесяц(ТекущаяДата(), -1) |
Пример запроса, который получает данные за текущий месяц:
ВЫБРАТЬ
Товар.Наименование,
СУММА(ДокументОбороты.Количество) КАК Количество
ИЗ
Документ.РеализацияТоваровУслуг.Обороты(&ДатаНачала, &ДатаОкончания) КАК ДокументОбороты
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Номенклатура КАК Товар
ПО ДокументОбороты.Номенклатура = Товар.Ссылка
ГДЕ
ДокументОбороты.Дата >= НачалоМесяца(ТекущаяДата())
И ДокументОбороты.Дата <= КонецМесяца(ТекущаяДата())
СГРУППИРОВАТЬ ПО
Товар.Наименование
Особенно полезны эти функции при работе с виртуальными таблицами оборотов и остатков, где даты часто используются для расчета сальдо на начало или конец периода. Например, чтобы получить остатки товаров на конец предыдущего месяца:
ВЫБРАТЬ
Остатки.Номенклатура КАК Товар,
Остатки.КоличествоОстаток КАК Остаток
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(
&КонецПредыдущегоМесяца,
Номенклатура В (
ВЫБРАТЬ
Товары.Ссылка
ИЗ
Справочник.Номенклатура КАК Товары
ГДЕ
Товары.ЭтоГруппа = ЛОЖЬ
)
) КАК Остатки
Динамические периоды: относительные даты
В многих случаях период нужно задавать не жестко, а относительно текущей даты или других параметров. Например, получить данные за последние 30 дней, за прошлый квартал или с начала года. Для этого используют арифметические операции с датами и функции смещения.
Пример запроса, который выбирает документы за последние 7 дней:
ВЫБРАТЬ
Документ.Дата,
Документ.СуммаДокумента
ИЗ
Документ.ПоступлениеДенежныхСредств КАК Документ
ГДЕ
Документ.Дата >= НачалоДня(ТекущаяДата() - 7)
И Документ.Дата <= КонецДня(ТекущаяДата())
Для квартальных или годовых отчетов удобно использовать комбинацию функций НачалоКвартала() и КонецКвартала() (доступны в последних версиях платформы). Если таких функций нет, их можно эмулировать:
// Начало текущего квартала
НачалоКвартала = НачалоМесяца(ДобавитьМесяц(ТекущаяДата(), - (Месяц(ТекущаяДата()) - 1) % 3));
// Конец текущего квартала
КонецКвартала = КонецМесяца(ДобавитьМесяц(НачалоКвартала, 2));
Аналогично можно получить начало года:
НачалоГода = Дата(Год(ТекущаяДата()), 1, 1);
Например, запрос остатков "на начало года" будет выполняться дольше, чем запрос "на конец прошлого месяца", если в базе накоплено много данных.
Как рассчитать начало финансового года, если он не совпадает с календарным?
Если финансовый год начинается, например, с 1 октября, используйте конструкцию:
Если Месяц(ТекущаяДата()) >= 10 Тогда
НачалоФинГода = Дата(Год(ТекущаяДата()), 10, 1);
Иначе
НачалоФинГода = Дата(Год(ТекущаяДата()) - 1, 10, 1);
КонецЕсли;
Периоды в виртуальных таблицах: особенности и ошибки
Виртуальные таблицы (Обороты, Остатки, ОборотноСальдоваяВедомость и др.) требуют особого подхода к указанию периодов. Здесь даты используются не только для отбора, но и для расчета агрегированных данных.
Типичная ошибка — передача в виртуальную таблицу некорректных границ периода. Например, если указать дату начала позже даты конца, запрос либо вернет пустой результат, либо завершится с ошибкой. Всегда проверяйте логику:
// Правильно:
Обороты = Документ.РеализацияТоваровУслуг.Обороты(НачалоДня(&ДатаНачала), КонецДня(&ДатаОкончания));
// Ошибка (если &ДатаНачала > &ДатаОкончания):
Обороты = Документ.РеализацияТоваровУслуг.Обороты(&ДатаОкончания, &ДатаНачала);
Еще одна распространенная проблема — неучет временной зоны при работе с датами в распределенных базах или облачных решениях. Если сервер и клиент находятся в разных часовых поясах, ТекущаяДата() может возвращать неожиданные значения. В таких случаях лучше явно указывать дату в UTC или использовать серверное время:
НачалоДня(НачалоДня(ТекущаяДатаСеанса()))
При работе с виртуальными таблицами оборотов полезно помнить, что:
- 📊 Таблица
Обороты()возвращает движения документов внутри указанного периода. - 📈 Таблица
Остатки()возвращает остатки на конец периода (если не указан второй параметр). - ⚠️ Таблица
ОборотноСальдоваяВедомость()может требовать указания периода с запасом (например, с начала года), даже если нужны данные только за месяц.
Пример корректного запроса остатков на конкретную дату:
ВЫБРАТЬ
Остатки.Номенклатура КАК Товар,
Остатки.КоличествоОстаток КАК Остаток
ИЗ
РегистрНакопления.Товары.Остатки(&ДатаКонтроля) КАК Остатки
Дата начала <= дата окончания
Указаны начало и конец дня (для полей ДатаВремя)
Период не выходит за пределы доступных данных в базе
Для виртуальных таблиц учтены особенности расчета (например, остатки "на дату" vs "за период")
-->
Периоды из пользовательских настроек и форм
В реальных приложениях даты часто берутся не из жестко заданных значений, а из форм ввода, настроек пользователя или регламентных задач. Рассмотрим, как передавать такие периоды в запросы.
Допустим, у нас есть форма с полями ПериодС и ПериодПо. Чтобы использовать их в запросе, нужно:
- Получить значения из элементов формы.
- Проверить корректность (например, что дата начала не позже даты окончания).
- Передать их в запрос как параметры.
Пример кода на встроенном языке:
Процедура СформироватьОтчет(Команда)
ДатаНачала = НачалоДня(ЭлементыФормы.ПериодС.Значение);
ДатаОкончания = КонецДня(ЭлементыФормы.ПериодПо.Значение);
Если ДатаНачала > ДатаОкончания Тогда
ПоказатьПредупреждение("Дата начала не может быть позже даты окончания!");
Возврат;
КонецЕсли;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Документ.Дата КАК Дата,
| Документ.Сумма КАК Сумма
|ИЗ
| Документ.ПоступлениеДенежныхСредств КАК Документ
|ГДЕ
| Документ.Дата >= &ДатаНачала
| И Документ.Дата <= &ДатаОкончания";
Запрос.УстановитьПараметр("ДатаНачала", ДатаНачала);
Запрос.УстановитьПараметр("ДатаОкончания", ДатаОкончания);
Результат = Запрос.Выполнить();
КонецПроцедуры
Если периоды хранятся в настройках пользователя (например, в регистре сведений ПользовательскиеНастройки), их можно получить так:
Настройки = РегистрыСведений.ПользовательскиеНастройки.ПолучитьПоследние(
Новый Структура("Пользователь, ТипНастройки",
ТекущийПользователь(), "ПериодОтчета"));
ДатаНачала = Настройки.ПериодС;
ДатаОкончания = Настройки.ПериодПо;
Для регламентных задач (например, ночных расчетов) период часто задается относительно даты выполнения задачи:
// Рассчитать данные за вчерашний день
ДатаНачала = НачалоДня(ТекущаяДата() - 1);
ДатаОкончания = КонецДня(ТекущаяДата() - 1);
Всегда валидируйте периоды, полученные из внешних источников (форм, файлов, веб-сервисов). Даже если пользователь не может ввести некорректную дату в поле формы, проверка защитит от ошибок при программном изменении значений.
Оптимизация запросов с периодами
Неправильно составленные условия по датам могут значительно замедлить выполнение запроса, особенно на больших базах. Вот ключевые правила оптимизации:
- Используйте индексы. Убедитесь, что поля, по которым идет отбор по дате, проиндексированы в конфигурации. Для регистров накопления индексы создаются автоматически, но для документов может потребоваться ручная настройка.
- Избегайте функций над полями. Запрос вида
ГДЕ ГОД(Документ.Дата) = 2023не сможет использовать индексы. Лучше писатьГДЕ Документ.Дата >= Дата(2023, 1, 1) И Документ.Дата <= Дата(2023, 12, 31). - Ограничивайте период. Не запрашивайте данные за "всю историю", если нужны только последние несколько месяцев. Чем уже период, тем быстрее выполнится запрос.
- Используйте виртуальные таблицы с умом. Например, если нужны остатки на конкретную дату, лучше взять таблицу
Остатки(), чем рассчитывать их вручную через обороты.
Пример неоптимального запроса:
ВЫБРАТЬ
Документ.Дата,
Документ.Контрагент
ИЗ
Документ.РеализацияТоваровУслуг КАК Документ
ГДЕ
МЕСЯЦ(Документ.Дата) = 5 // Не использует индексы!
И ГОД(Документ.Дата) = 2023
Оптимизированный вариант:
ВЫБРАТЬ
Документ.Дата,
Документ.Контрагент
ИЗ
Документ.РеализацияТоваровУслуг КАК Документ
ГДЕ
Документ.Дата >= Дата(2023, 5, 1)
И Документ.Дата <= Дата(2023, 5, 31)
Для сложных отчетов, где периоды используются в нескольких запросах, имеет смысл вынести расчет границ в отдельную функцию:
Функция ПолучитьПериодОтчета(ТипПериода, ТекущаяДата = Неопределено)
Если ТекущаяДата = Неопределено Тогда
ТекущаяДата = ТекущаяДата();
КонецЕсли;
Если ТипПериода = "Месяц" Тогда
Возврат Новый Структура("С, По",
НачалоМесяца(ТекущаяДата),
КонецМесяца(ТекущаяДата));
ИначеЕсли ТипПериода = "Квартал" Тогда
Возврат Новый Структура("С, По",
НачалоКвартала(ТекущаяДата),
КонецКвартала(ТекущаяДата));
КонецЕсли;
КонецФункции
Если запрос все равно выполняется долго, проверьте план выполнения в консоли запросов (меню "Все функции" → "План запроса"). Это поможет выявить узкие места, например, полное сканирование таблиц вместо использования индексов.
Типичные ошибки и их решения
Даже опытные разработчики иногда сталкиваются с проблемами при работе с периодами в запросах. Рассмотрим самые распространенные ошибки и способы их исправления.
⚠️ Внимание: Если в запросе используются виртуальные таблицы оборотов или остатков, убедитесь, что передаваемые даты попадают в диапазон доступных данных в базе. Например, запрос остатков на дату, предшествующую дате начального остатка, вернет некорректные результаты.
| Ошибка | Причина | Решение |
|---|---|---|
| Запрос возвращает пустой результат при корректных данных | Не учтено время в полях ДатаВремя (например, сравнение с ТекущаяДата() без обрезки времени) |
Использовать НачалоДня()/КонецДня() для границ периода |
| Ошибка "Некорректный период" | В виртуальную таблицу переданы даты в обратном порядке (ДатаНачала > ДатаОкончания) |
Проверять порядок дат перед выполнением запроса |
| Запрос выполняется слишком долго | Отсутствуют индексы по полям дат или используются функции над полями в условии | Переписать условие без функций (например, заменить ГОД(Дата) = 2023 на диапазон дат) |
| Несовпадение данных с бухгалтерскими отчетами | В запросе не учтены документы с датой равной границе периода (например, Дата < КонецМесяца вместо Дата <= КонецМесяца) |
Всегда использовать >= и <= для включения границ |
| Ошибка при работе с распределенной базой | Не учтена разница во временных зонах между сервером и клиентом | Использовать ТекущаяДатаСеанса() или явно указывать даты в UTC |
Еще одна распространенная проблема — несоответствие периодов в связанных запросах. Например, если в основном запросе используется период с 1 по 31 января, а во вложенном подзапросе — с 1 по 30 января, результаты могут оказаться несогласованными. Всегда синхронизируйте границы периодов во всех частях запроса.
Пример ошибки с временными зонами:
// На клиенте (Москва, UTC+3):
ДатаНачала = ТекущаяДата(); // 2023-10-01 12:00:00
// На сервере (UTC):
// Та же дата будет интерпретирована как 2023-10-01 09:00:00
Решение — явно привести дату к нужной зоне:
ДатаНачала = НачалоДня(ТекущаяДатаСеанса()); // Использует дату сеанса
FAQ: Частые вопросы по периодам в запросах 1С
Как получить данные за "последний полный месяц"?
Используйте комбинацию функций НачалоМесяца() и ДобавитьМесяц():
ДатаНачала = НачалоМесяца(ДобавитьМесяц(ТекущаяДата(), -1));
ДатаОкончания = КонецМесяца(ДобавитьМесяц(ТекущаяДата(), -1));
Это вернет период с 1 по последнее число прошлого месяца.
Почему запрос с периодом работает медленно, хотя данных мало?
Возможные причины:
- Отсутствуют индексы по полю даты в таблице.
- В условии используются функции над полем (например,
ГОД(Дата)), что блокирует использование индексов. - Запрос обращается к виртуальным таблицам без ограничения по дате на уровне СУБД.
Проверьте план выполнения запроса в консоли запросов.
Как передать период в запрос из отчета СКД?
В схеме компоновки данных период можно получить из параметров или пользовательских полей. Пример:
- В настройках СКД добавьте параметры
ДатаНачалаиДатаОкончания. - В запросе СКД используйте их как
&ДатаНачалаи&ДатаОкончания. - При формировании отчета передайте значения параметров из формы.
Пример кода:
Отчет.КомпоновщикНастроек.Настройки.ПараметрыДанных.УстановитьЗначениеПараметра("ДатаНачала", НачалоМесяца(ТекущаяДата()));
Отчет.КомпоновщикНастроек.Настройки.ПараметрыДанных.УстановитьЗначениеПараметра("ДатаОкончания", КонецМесяца(ТекущаяДата()));
Можно ли в одном запросе использовать разные периоды для разных таблиц?
Да, но это может привести к неожиданным результатам. Например:
ВЫБРАТЬ
Товары.Наименование,
Обороты.Количество
ИЗ
Справочник.Номенклатура КАК Товары
ЛЕВОЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг.Обороты(
&ДатаНачала1, &ДатаОкончания1) КАК Обороты
ПО Товары.Ссылка = Обороты.Номенклатура
ГДЕ
Обороты.Дата >= &ДатаНачала2 // Другой период!
В этом случае условие ГДЕ будет применено после соединения, что может исказить результаты. Лучше использовать одинаковые периоды или разбивать запрос на части.
Как работать с периодами в распределенных базах (УРИБ, УРБД)?
В распределенных базах данные могут быть разнесены по узлам с разными временными зонами. Рекомендации:
- Используйте
ТекущаяДатаСеанса()вместоТекущаяДата(). - Храните даты в UTC и преобразуйте их при отображении.
- Для регламентных задач синхронизируйте время выполнения между узлами.
Пример преобразования:
// Преобразовать локальную дату в UTC
ДатаUTC = Дата(Год(ЛокальнаяДата), Месяц(ЛокальнаяДата), День(ЛокальнаяДата),
Час(ЛокальнаяДата) - СмещениеЧасов, Минута(ЛокальнаяДата), 0);