При разработке отчетов по закупкам или продажам программисты часто сталкиваются с задачей получения «самой первой» или «самой последней» цены за определенный период. Казалось бы, достаточно использовать агрегатную функцию МИНИМУМ(Период) или МАКСИМУМ(Период), сгруппировав данные по номенклатуре. Однако на практике возникает проблема: в одну и ту же секунду может быть проведено несколько документов (например, два поступления от разных поставщиков или разные партии товара). В этом случае стандартная группировка по дате вернет обе записи, что приведет к задвоению данных в отчете.
В этой статье мы подробно разберем, как корректно получить только одну, действительно первую запись для каждой позиции номенклатуры, используя механизмы платформы 1С.
Рассмотрим стандартный запрос, который часто пытаются использовать новички. В нем мы выбираем минимальную дату для каждой номенклатуры, помещаем её во временную таблицу, а затем соединяем с исходными данными:
ВЫБРАТЬ
ЗакупкиОбороты.Номенклатура,
МИНИМУМ(ЗакупкиОбороты.Период) КАК Период
ПОМЕСТИТЬ МинимальныйПериод
ИЗ
РегистрНакопления.Закупки.Обороты(&НачалоПериода, &КонецПериода, Регистратор, ) КАК ЗакупкиОбороты
СГРУППИРОВАТЬ ПО
ЗакупкиОбороты.Номенклатура;
Проанализируем ситуацию: если по номенклатуре "Кирпич красный" 01.01.2023 в 12:00:00 было два документа поступления (№1 с ценой 10 руб. и №2 с ценой 12 руб.), то условие ЗакупкиОбороты.Период = МинимальныйПериод.Период выберет оба этих документа. В результате мы получим две строки вместо одной «начальной» цены. Чтобы этого избежать, нам нужно найти уникальный идентификатор, который позволит отличить один документ от другого даже в пределах одной секунды.
Самый надежный способ определить порядок документов в 1С — это использование поля МоментВремени. Объект МоментВремени включает в себя не только дату и время, но и ссылку на сам документ. При сравнении двух моментов времени система сначала сравнивает время, а при равенстве времени — уникальные идентификаторы ссылок (GUID). Это гарантирует, что один документ всегда будет «раньше» или «позже» другого.
Разберем реализацию этого метода через подзапрос. Рассмотрим пример кода, который позволяет выбрать по одному регистратору для каждой номенклатуры:
ВЫБРАТЬ
РН_Закупки.Регистратор,
РН_Закупки.Номенклатура,
РН_Закупки.МоментВремени
ПОМЕСТИТЬ ВТ_Закупки
ИЗ
РегистрНакопления.Закупки КАК РН_Закупки
ГДЕ
РН_Закупки.Активность
И РН_Закупки.Период МЕЖДУ &НачалоПериода И &КонецПериода;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ РАЗЛИЧНЫЕ
ВТ_Закупки.Номенклатура,
ВТ_Закупки.Регистратор
ИЗ
ВТ_Закупки КАК ВТ_Закупки
ГДЕ
ВТ_Закупки.Регистратор В
(ВЫБРАТЬ ПЕРВЫЕ 1
Отбор.Регистратор
ИЗ
ВТ_Закупки КАК Отбор
ГДЕ
Отбор.Номенклатура = ВТ_Закупки.Номенклатура
УПОРЯДОЧИТЬ ПО
Отбор.МоментВремени ВОЗР)
В этом примере мы используем конструкцию ВЫБРАТЬ ПЕРВЫЕ 1 внутри коррелированного подзапроса. Для каждой строки основной таблицы ВТ_Закупки система ищет один самый ранний регистратор, упорядоченный по МоментВремени.
Хотя предыдущее решение работает корректно, использование подзапроса в условии ГДЕ может существенно замедлить выполнение запроса на больших базах данных (десятки тысяч строк и более). Для оптимизации мы рекомендуем использовать метод с предварительным получением минимального момента времени.
Проанализируем шаги оптимизированного решения:
МоментВремени для каждой номенклатуры во временную таблицу.Номенклатура и МоментВремени.
ВЫБРАТЬ
Закупки.Номенклатура,
МИНИМУМ(Закупки.МоментВремени) КАК МинМомент
ПОМЕСТИТЬ ВТ_МинМоменты
ИЗ
РегистрНакопления.Закупки КАК Закупки
ГДЕ
Закупки.Период МЕЖДУ &НачалоПериода И &КонецПериода
СГРУППИРОВАТЬ ПО
Закупки.Номенклатура;
////////////////////////////////////////////////////////////////////////////////
ИНДЕКСИРОВАТЬ ПО
ВТ_МинМоменты.Номенклатура,
ВТ_МинМоменты.МинМомент;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Закупки.Номенклатура,
Закупки.Регистратор,
Закупки.Цена,
Закупки.Количество
ИЗ
ВТ_МинМоменты КАК МинМоменты
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрНакопления.Закупки КАК Закупки
ПО МинМоменты.Номенклатура = Закупки.Номенклатура
И МинМоменты.МинМомент = Закупки.МоментВремени
Важный нюанс: Поле МоментВремени является составным типом данных на уровне базы данных. Использование МИНИМУМ(МоментВремени) поддерживается платформой 1С и позволяет эффективно находить самую раннюю запись.
Иногда возникают ситуации, когда одна и та же номенклатура указана в документе несколько раз (например, с разной ценой или по разным характеристикам). В этом случае даже МоментВремени не поможет, так как он одинаков для всех строк одного документа-регистратора.
Для решения этой специфической задачи нам потребуется поле НомерСтроки. Рассмотрим алгоритм действий:
МоментВремени (как в Решении 2).НомерСтроки.Посмотрим на пример логики в запросе:
ВЫБРАТЬ
Закупки.Номенклатура,
Закупки.Регистратор,
МИНИМУМ(Закупки.НомерСтроки) КАК МинНомерСтроки
ПОМЕСТИТЬ ВТ_ПервыеСтроки
ИЗ
ВТ_МинМоменты КАК МинМоменты
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрНакопления.Закупки КАК Закупки
ПО МинМоменты.Номенклатура = Закупки.Номенклатура
И МинМоменты.МинМомент = Закупки.МоментВремени
СГРУППИРОВАТЬ ПО
Закупки.Номенклатура,
Закупки.Регистратор;
Мы рассмотрели несколько способов получения первых записей в запросе. Выбор конкретного метода зависит от ваших требований к производительности и сложности данных:
ВЫБРАТЬ ПЕРВЫЕ 1 в подзапросе (Решение 1).МоментВремени (Решение 2).НомерСтроки (Решение 3).Также стоит помнить, что если ваша цель — получение актуальных цен, эффективнее использовать Регистры сведений и их виртуальные таблицы СрезПервых или СрезПоследних, которые на уровне платформы оптимизированы именно под такие задачи.