Параметр вместо вписанного значения
До сих пор все условия в наших запросах были написаны намертво: конкретный склад, конкретная дата. Такой запрос годится один раз. Чтобы один и тот же текст работал для марта и для июня, для Севера и для Юга, в него ставят параметр — место, куда снаружи подставят значение.
ВЫБРАТЬ
Док.Номер КАК Номер,
Док.СуммаДокумента КАК Сумма
ИЗ
Документ.РеализацияТоваровУслуг КАК Док
ГДЕ
Док.Проведён
И Док.Дата >= &Начало
И Док.Дата <= &Конец
И Док.Склад = &Склад
Параметр пишется знаком & и именем. Имя своё, любое понятное; регистр букв не важен, но принято писать так же, как называется то, что подставляется. Значение задаётся снаружи: в коде — Запрос.УстановитьПараметр("Начало", НачалоМесяца), в отчёте на компоновке — настройкой или самой компоновкой, в консоли запросов — в отдельной табличке под текстом.
Второе, и это важнее скорости: у параметра есть тип. Передали дату — сравнивается дата, передали ссылку — сравнивается ссылка. Если тип не совпал с типом поля, запрос не выполнится и скажет об этом. Это хорошо: ошибка громкая и находится сразу, а не превращается в пустой результат.
Незаполненный параметр не ошибка, а частая причина странного результата. Если &Склад не установили вовсе, платформа подставит неопределённое значение, и сравнение с ним не даст ни одной строки. Отчёт при этом построится и покажет пустоту — а человек скажет «отчёт сломался». Поэтому в коде параметры устанавливают все и сразу, а в отчётах следят, чтобы у настройки было значение по умолчанию.
Параметр можно передать и списком — тогда он работает со словом В: Док.Склад В (&Склады). В коде туда кладут массив или список значений, в отчёте — множественный отбор. Один и тот же текст запроса при этом обслуживает и «один склад», и «пять складов», и «все».
А теперь важное ограничение, о которое спотыкаются все, кто пришёл из других языков: параметром подставляется значение, и только значение. Имя таблицы, имя поля, ключевое слово, знак сравнения — всё это часть текста запроса, и параметром их не заменить. Написать «ИЗ &Таблица» нельзя; написать «ГДЕ Поле &Знак &Значение» тоже нельзя.
Причина простая и та же, что у запрета на склейку: запрос разбирается один раз, до подстановки значений. Разобрать текст, в котором неизвестно даже, откуда брать строки, невозможно. Если таблица или условие действительно должны меняться, текст запроса собирают в коде — и вот там склейка законна, потому что склеиваются части текста, а не значения. Значения при этом всё равно остаются параметрами.
Дата со временем и конец периода
Теперь про даты — и начнём сразу с ошибки, которую делают все без исключения.
Дата в 1С всегда содержит время. Даже когда его не видно. Когда человек выбирает в отчёте «по 31 марта», платформа передаёт 31.03.2026 00:00:00 — полночь, то есть самое начало дня. И условие Док.Дата <= &Конец честно отсекает всё, что случилось позже полуночи: то есть весь рабочий день тридцать первого марта.
Выглядит это так: отчёт за март показывает сумму меньше, чем в типовом, ровно на последний день. Человек сверяет две цифры, не находит разницы в устройстве отчёта и уходит с ощущением, что платформа врёт. Причина — двадцать три часа пятьдесят девять минут, которых не хватило условию.
Лечится это одним из двух способов, и оба правильные.
Первый: дотянуть конец до конца дня.
ГДЕ
Док.Дата МЕЖДУ &Начало И КОНЕЦПЕРИОДА(&Конец, ДЕНЬ)
Функция КОНЕЦПЕРИОДА превращает 31.03 00:00:00 в 31.03 23:59:59. Обратите внимание: функция применяется к параметру, а не к полю, — значит, вычисляется она один раз, и правило «слева от сравнения должно стоять поле» не нарушено.
Второй способ: взять строго меньше начала следующего дня.
ГДЕ
Док.Дата >= &Начало
И Док.Дата < ДОБАВИТЬКДАТЕ(&Конец, ДЕНЬ, 1)
Здесь граница не включается, и последняя секунда никого не волнует. Многие предпочитают именно этот вариант: 23:59:59 — это не совсем конец дня, ведь между 23:59:59 и полуночью помещается ещё 999 миллисекунд, и документ, записанный в эту щель, пропадёт. Случается такое редко, но в обмене и в автоматических документах — случается.
Что выбрать — вопрос вкуса и правил конкретной команды. Выбрать что-то одно и держаться — обязательно.
Из того же корня растёт вторая ошибка, поменьше: равенство с датой почти никогда не срабатывает. Условие Док.Дата = &Дата найдёт только те документы, что записаны ровно в переданную секунду, — то есть, скорее всего, ни одного. «За такой-то день» — это всегда диапазон:
ГДЕ
Док.Дата МЕЖДУ НАЧАЛОПЕРИОДА(&Дата, ДЕНЬ)
И КОНЕЦПЕРИОДА(&Дата, ДЕНЬ)
Обе функции применены к параметру, и обе вычисляются один раз. Такой отбор база выполняет по индексу, а условие читается ровно так, как задумано: от первого момента дня до последнего.
И третье, о чём стоит знать заранее: дата документа и дата движения — не всегда одно и то же. У документа своя дата, у записи в регистре — своя, обычно равная дате документа, но не обязательно: документ может делать движения будущим или прошлым числом. Поэтому отчёт «по документам» и отчёт «по движениям» за один и тот же период могут честно расходиться, и первый вопрос при разборе такого расхождения — по какой дате отбирали.
Функции работы с датами
Кроме двух функций из предыдущей главы, в запросах регулярно встречаются ещё несколько. Их немного, и запоминаются они быстро.
НАЧАЛОПЕРИОДА(дата, часть) и КОНЕЦПЕРИОДА(дата, часть) — начало и конец периода, в который попадает дата. Часть — это ГОД, ПОЛУГОДИЕ, КВАРТАЛ, МЕСЯЦ, ДЕКАДА, НЕДЕЛЯ, ДЕНЬ, ЧАС, МИНУТА, СЕКУНДА. Именно этими функциями считают «с начала месяца» и «по конец квартала», не набирая даты руками.
ДОБАВИТЬКДАТЕ(дата, часть, сколько) — сдвиг. Срок оплаты через тридцать дней, следующий месяц, предыдущий год. Число может быть отрицательным: ДОБАВИТЬКДАТЕ(&Начало, МЕСЯЦ, -1) — тот же период месяцем раньше, и это ровно то, что нужно для колонки «прошлый период» в отчёте.
РАЗНОСТЬДАТ(дата1, дата2, часть) — сколько частей между двумя датами. Так считают просрочку в днях, возраст задолженности, время между отгрузкой и оплатой. Порядок аргументов важен: из второй даты вычитается первая, и если получить нужно положительное число, раньшая дата пишется первой.
ДАТАВРЕМЯ(год, месяц, день) — дата, написанная прямо в запросе. Так пишут заведомо постоянные границы: ДАТАВРЕМЯ(2026, 1, 1). Можно добавить час, минуту и секунду шестью числами. Отдельный случай — ДАТАВРЕМЯ(1, 1, 1): это «нулевая» дата, то самое значение незаполненного реквизита даты, о котором шла речь в прошлом уроке.
И три функции, выдёргивающие часть даты: ГОД(дата), МЕСЯЦ(дата), ДЕНЬ(дата). Ими удобно строить группировки по годам и месяцам — и именно их не стоит писать в условиях отбора, потому что функция по полю лишает базу индекса. Разница простая: в списке полей ГОД считает колонку, в ГДЕ — портит скорость.
С группировкой по месяцам связана ловушка, которая стоит отдельного абзаца. Казалось бы, естественно взять две колонки — ГОД(Дата) и МЕСЯЦ(Дата) — и группировать по ним. Работает это ровно до момента, когда понадобится сортировка или график: месяцы окажутся числами от 1 до 12, и январь следующего года встанет перед декабрём предыдущего. Правильный приём другой:
ВЫБРАТЬ
НАЧАЛОПЕРИОДА(Док.Дата, МЕСЯЦ) КАК Месяц,
СУММА(Док.СуммаДокумента) КАК Сумма
Колонка «Месяц» остаётся датой — первым числом месяца, — и сортируется как дата, и на графике ложится по оси времени правильно. Показать её человеку в виде «март 2026» — дело оформления отчёта, а не запроса. Тот же приём работает для дней, недель и кварталов.
Есть и функции поменьше, которые пригождаются реже: НЕДЕЛЯ(дата) — номер недели в году, ДЕНЬНЕДЕЛИ(дата) — номер дня недели от понедельника, ДЕНЬГОДА(дата) — номер дня с начала года. Ими строят отчёты вроде «продажи по дням недели» — и к ним относится ровно то же правило: в списке полей можно, в отборе не нужно.
Даты в параметрах виртуальных таблиц
У виртуальных таблиц регистров даты живут не в ГДЕ, а в параметрах самой таблицы, и это первое, что стоит помнить.
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(
&НаДату,
Склад = &Склад) КАК Ост
Здесь &НаДату — это момент, на который считать остаток, и он тоже дата со временем. Остаток «на 31 марта» означает «на 31 марта в полночь», то есть до всех документов этого дня. Хотели «на конец марта» — пишите КОНЕЦПЕРИОДА(&Конец, ДЕНЬ), и это та же ошибка, что и в прошлой главе, только выглядит она иначе: не «не хватает последнего дня», а «остаток не сходится с документами».
У таблицы оборотов период задаётся двумя параметрами, и границы там включаются обе. Значит, правило про конец дня действует и здесь: Обороты(&Начало, КОНЕЦПЕРИОДА(&Конец, ДЕНЬ), , ). Пустые места между запятыми — пропущенные параметры (периодичность и отбор), и пропускать их запятыми обязательно, иначе платформа примет отбор за периодичность.
Раз уж речь зашла о пропущенных параметрах — стоит сказать, что там пропускается. У таблицы оборотов третий параметр называется периодичность, и он решает, одной строкой прийдёт оборот за весь период или разложится по частям: ДЕНЬ, НЕДЕЛЯ, МЕСЯЦ, КВАРТАЛ, ГОД, РЕГИСТРАТОР, ЗАПИСЬ. Пропущенная периодичность означает «итог за весь период одной строкой», и это самый частый случай.
ИЗ
РегистрНакопления.Продажи.Обороты(
&Начало, КОНЕЦПЕРИОДА(&Конец, ДЕНЬ),
МЕСЯЦ, ) КАК Об
С периодичностью МЕСЯЦ у результата появляется колонка «Период» — и это опять дата, первое число месяца, ровно как в приёме из прошлой главы. Отсюда полезный вывод: помесячную динамику не нужно считать самому. Таблица оборотов умеет это сама, и считает она быстрее, чем группировка по документам, потому что читает уже свёрнутые итоги.
Периодичность РЕГИСТРАТОР раскладывает оборот по документам — так делают отчёты «из чего сложилась цифра». Это самая подробная и самая тяжёлая периодичность: строк получается столько же, сколько документов, и на годовом периоде такой отчёт лучше не открывать без отбора.
Отдельная тонкость — граница и момент времени. Когда нужен остаток «после всех движений такого-то документа», датой обойтись нельзя: в одну секунду может попасть несколько документов. Для этого в коде существует объект «Момент времени», а в запрос передаётся граница — значение, которое учитывает и дату, и порядок записи. Подробно это второй уровень; на первом достаточно знать, что такой случай существует и что «на дату» и «сразу после документа» — не одно и то же.
И последнее, практическое: где считать границы периода — в коде или в запросе? Обе привычки живы. Считать в коде и передавать готовые &Начало и &Конец — честнее: запрос тогда не знает ничего лишнего, а границы видны в отладчике. Считать в запросе функциями периода — короче, когда отчёт строится от одной даты. Плохо только третье — считать наполовину там и наполовину здесь: тогда при разборе «почему не сходится» приходится искать истину в двух местах сразу.
| что нужно | как записать |
|---|---|
| Остаток на конец дня | Остатки(КОНЕЦПЕРИОДА(&Конец, ДЕНЬ), …) |
| Обороты за месяц | Обороты(&Начало, КОНЕЦПЕРИОДА(&Конец, ДЕНЬ), , ) |
| Период месяцем раньше | ДОБАВИТЬКДАТЕ(&Начало, МЕСЯЦ, -1) |
| Просрочка в днях | РАЗНОСТЬДАТ(Док.Дата, &Сегодня, ДЕНЬ) |