Как в запросе 1С получить записи за период до достижения определенного оборота (нарастающий итог в обратном порядке)?

Программист 1С:Комплексная автоматизация Управленческий учет IT и автоматизация бизнеса
← На главную

В практике разработки на платформе 1С:Предприятие 8.2 (особенно в таких конфигурациях, как «Комплексная автоматизация 1.1» или «Управление производственным предприятием») часто возникает задача анализа «глубины» оборотов. Например, нам нужно определить, какими именно документами поступления были сформированы последние 10 проданных аккумуляторов. Это классическая задача на нарастающий итог, но с условием движения «назад во времени».

Рассмотрим подробнее, как реализовать этот алгоритм, используя только средства языка запросов 1С, и проанализируем ситуацию, когда эффективнее будет использовать программный обход выборки.

Метод 1. Реализация через левое соединение таблицы с самой собой

Основная идея заключается в том, чтобы для каждой строки нашей выборки найти сумму всех строк, которые находятся «после» неё (если мы идем от конца периода к началу). Разберем по шагам создание такого запроса.

Для начала подготовим данные. Чтобы запрос работал корректно, нам нужно обеспечить уникальность и последовательность записей. В 1С для этого лучше всего подходит МоментВремени. Выгрузим необходимые движения во временную таблицу.


ВЫБРАТЬ
    ТоварыНаСкладах.Период КАК Период,
    ТоварыНаСкладах.Регистратор КАК Регистратор,
    ТоварыНаСкладах.Количество КАК Количество,
    ТоварыНаСкладах.МоментВремени КАК МоментВремени
ПОМЕСТИТЬ ВТ_Движения
ИЗ
    РегистрНакопления.ТоварыНаСкладах КАК ТоварыНаСкладах
ГДЕ
    ТоварыНаСкладах.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход)
    И ТоварыНаСкладах.Номенклатура = &Номенклатура
    И ТоварыНаСкладах.Склад = &Склад
;

Теперь проанализируем ключевой этап: расчет итога. Мы соединим таблицу ВТ_Движения (назовем её Т1) с этой же таблицей (назовем её Т2). Условие соединения будет таким, чтобы для каждой записи из Т1 в Т2 попали все записи, у которых МоментВремени больше или равен текущему.


ВЫБРАТЬ
    Т1.Регистратор,
    Т1.Количество КАК КоличествоДокумента,
    СУММА(Т2.Количество) КАК НакопленныйИтог
ИЗ
    ВТ_Движения КАК Т1
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Движения КАК Т2
        ПО Т1.МоментВремени <= Т2.МоментВремени
СГРУППИРОВАТЬ ПО
    Т1.Регистратор,
    Т1.Количество,
    Т1.МоментВремени
ИМЕЮЩИЕ
    СУММА(Т2.Количество) - Т1.Количество < &НужныйОборот
УПОРЯДОЧИТЬ ПО
    Т1.МоментВремени УБЫВ

Обратите внимание на секцию ИМЕЮЩИЕ. Если мы напишем СУММА(Т2.Количество) <= &НужныйОборот, мы можем потерять последнюю накладную, которая «закрывает» нашу потребность, но превышает её. Условие СУММА(Т2.Количество) - Т1.Количество < &НужныйОборот позволяет включить ту самую запись, на которой мы достигли или перешагнули порог в 10 единиц.

Метод 2. Учет «граничного значения» и точность момента времени

Выясним причину, по которой обычное сравнение по дате может подвести. В 1С 8.2 в пределах одной секунды может быть записано несколько документов. Если использовать в соединении только Т1.Период <= Т2.Период, то итоги по документам внутри одной секунды перемешаются.

Чтобы избежать этого, в условиях соединения и группировки следует использовать МоментВремени. Если ваша версия платформы или особенности метаданных не позволяют эффективно сравнивать МоментВремени в соединении, можно использовать комбинацию Период и Ссылка:

Правильное условие сравнения периодов:
Т1.Период < Т2.Период ИЛИ (Т1.Период = Т2.Период И Т1.Ссылка <= Т2.Ссылка)

Метод 3. Программный обход (использование объектной модели)

Рассмотрим ситуацию, когда данных очень много. Метод самосоединения (Self-join) имеет квадратичную зависимость от количества записей. Если документов тысячи, сервер СУБД будет тратить колоссальные ресурсы на выполнение запроса.

В этом случае мы рекомендуем выполнить простой запрос на выборку данных в обратном порядке и обработать результат в коде 1С:


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

Выборка = Запрос.Выполнить().Выбрать();
НужныйОборот = 10;
Накоплено = 0;
ТаблицаРезультат = Новый ТаблицаЗначений;
ТаблицаРезультат.Колонки.Добавить("Документ");

Пока Выборка.Следующий() И Накоплено < НужныйОборот Цикл
    Накоплено = Накоплено + Выборка.Количество;
    НоваяСтрока = ТаблицаРезультат.Добавить();
    НоваяСтрока.Документ = Выборка.Регистратор;
КонецЦикла;

Этот способ работает значительно быстрее на больших объемах данных, так как требует всего одного прохода по данным (линейная сложность $O(n)$ против квадратичной в запросе).

Особенности применения в КА 1.1

Поскольку вы работаете в конфигурации «Комплексная автоматизация 1.1», важно помнить о режиме проведения документов. Если в системе включен РАУЗ (Расширенная аналитика учета затрат), данные об оборотах следует брать из регистра УчетЗатрат или УчетЗатратРегл. Если же используется классический партионный учет, то вашими источниками будут регистры ПартииТоваровНаСкладах.

Практические советы:

  1. Всегда индексируйте поля Номенклатура, Склад и Период во временных таблицах перед выполнением соединения.
  2. Если вам нужно получить данные по нескольким товарам сразу, расчет нарастающего итога в одном запросе станет крайне сложным — в этом случае лучше использовать временные таблицы и пакетные запросы.
  3. Для получения именно «оборотов» за день, а не по конкретным документам, используйте виртуальную таблицу Обороты с периодичностью ДЕНЬ, это существенно сократит количество строк для соединения.

Таким образом, мы посмотрели два принципиально разных подхода. Запрос с самосоединением удобен тем, что его можно использовать внутри сложного отчета (например, в СКД), а программный обход незаменим в высоконагруженных процедурах проведения или заполнения документов.

← На главную