Почему двоятся остатки при периодичности Регистратор в запросе 1С и как это исправить

Программист 1С v8.3 (Управляемые формы) 1С:Управление торговлей Управленческий учет Торговля и дистрибуция
← На главную

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

В чем причина искажения остатков при детализации до регистратора

Проанализируем механику работы виртуальной таблицы ОстаткиИОбороты. Если периодичность не задана (или установлена в значение Период без детализации), платформа возвращает остаток на границу начала периода, обороты за период и остаток на конец периода для каждой уникальной комбинации измерений.

Однако, как только мы указываем периодичность Регистратор, виртуальная таблица возвращает остаток до и после каждого документа-регистратора. Остаток по своей экономической сути не является аддитивной величиной во времени. Его нельзя суммировать между разными моментами времени.

Посмотрим на наглядный пример расчета остатка товара на складе:

  1. Начальный остаток равен 0. Пришел документ №1 с приходом +1 шт. Остаток стал равен 1.
  2. Далее проводится документ №2 с приходом +3 шт. До его проведения остаток был 1, после проведения стал 4.

Если в запросе применить агрегатную функцию СУММА(КоличествоКонечныйОстаток) и сгруппировать строки по номенклатуре, то платформа сложит остаток после первого документа (1 шт.) и остаток после второго документа (4 шт.). В итоге мы получим 5 шт. вместо фактических 4 шт. Чем больше документов-регистраторов попадает в выборку за указанный период, тем сильнее будут завышены итоговые суммы остатков.

Типичная ошибка в тексте запроса

Рассмотрим структуру исходного запроса, приводящего к ошибке. Разработчик пытается в одной таблице рассчитать общие остатки и одновременно отфильтровать расход только по конкретному виду документа (например, по перемещениям):


ВЫБРАТЬ
    ТоварыВРозницеОстаткиИОбороты.Склад,
    ТоварыВРозницеОстаткиИОбороты.Номенклатура КАК Товар,
    СУММА(ТоварыВРозницеОстаткиИОбороты.КоличествоНачальныйОстаток) КАК КоличествоНачальныйОстаток,
    СУММА(ТоварыВРозницеОстаткиИОбороты.КоличествоКонечныйОстаток) КАК КоличествоКонечныйОстаток,
    СУММА(ТоварыВРозницеОстаткиИОбороты.КоличествоПриход) КАК КоличествоПриход,
    СУММА(ВЫБОР
            КОГДА ТоварыВРозницеОстаткиИОбороты.Регистратор ССЫЛКА Документ.ПеремещениеТоваров
                ТОГДА ТоварыВРозницеОстаткиИОбороты.КоличествоРасход
            ИНАЧЕ 0
        КОНЕЦ) КАК КоличествоРасход
ПОМЕСТИТЬ ВТ_РозОстатки
ИЗ
    РегистрНакопления.ТоварыВРознице.ОстаткиИОбороты({(&НачалоПериода)}, {(&КонецПериода)}, Регистратор, , ) КАК ТоварыВРозницеОстаткиИОбороты
СГРУППИРОВАТЬ ПО
    ТоварыВРозницеОстаткиИОбороты.Склад,
    ТоварыВРозницеОстаткиИОбороты.Номенклатура;

В данном фрагменте указание периодичности Регистратор потребовалось исключительно ради условия КОГДА ... Регистратор ССЫЛКА Документ.ПеремещениеТоваров. Но из-за группировки и суммирования полей КоличествоНачальныйОстаток и КоличествоКонечныйОстаток результат расчета остатков становится полностью недостоверным.

Решение 1. Разделение остатков и детальных оборотов через подзапрос или временную таблицу

Разберем по шагам правильную организацию выборки. Если нам нужны корректные начальные и конечные остатки за весь период, мы должны получать их из виртуальной таблицы без периодичности. Обороты же по конкретным документам-регистраторам следует получать отдельно.

Рассмотрим пример запроса, в котором обороты по перемещениям вынесены во вложенную выборку из таблицы Обороты с периодичностью Регистратор, а остатки берутся из общей таблицы ОстаткиИОбороты:


ВЫБРАТЬ
    ТоварыОстатки.Склад КАК Склад,
    ТоварыОстатки.Номенклатура КАК Номенклатура,
    ТоварыОстатки.КоличествоНачальныйОстаток КАК КоличествоНачальныйОстаток,
    ТоварыОстатки.КоличествоКонечныйОстаток КАК КоличествоКонечныйОстаток,
    ТоварыОстатки.КоличествоПриход КАК КоличествоПриход,
    ЕСТЬNULL(ПеремещенияРасход.КоличествоРасход, 0) КАК КоличествоРасход
ИЗ
    РегистрНакопления.ТоварыВРознице.ОстаткиИОбороты(&НачалоПериода, &КонецПериода, , , ) КАК ТоварыОстатки
    ЛЕВОЕ СОЕДИНЕНИЕ
        (ВЫБРАТЬ
            Обороты.Склад КАК Склад,
            Обороты.Номенклатура КАК Номенклатура,
            СУММА(Обороты.КоличествоРасход) КАК КоличествоРасход
        ИЗ
            РегистрНакопления.ТоварыВРознице.Обороты(&НачалоПериода, &КонецПериода, Регистратор, ) КАК Обороты
        ГДЕ
            Обороты.Регистратор ССЫЛКА Документ.ПеремещениеТоваров
        СГРУППИРОВАТЬ ПО
            Обороты.Склад,
            Обороты.Номенклатура) КАК ПеремещенияРасход
    ПО ТоварыОстатки.Склад = ПеремещенияРасход.Склад
       И ТоварыОстатки.Номенклатура = ПеремещенияРасход.Номенклатура;

В этом варианте таблица ТоварыВРознице.ОстаткиИОбороты возвращает ровно одну строку на комбинацию склада и номенклатуры с правильными остатками на границы интервала, а подзапрос возвращает строго свернутый оборот по нужному документу.

Решение 2. Оптимальная архитектура через ОБЪЕДИНИТЬ ВСЕ

Проанализируем ситуацию глубже: использование ЛЕВОЕ СОЕДИНЕНИЕ несет в себе скрытый риск. Если по товару не было начального и конечного остатка (например, товар поступил и был сразу полностью перемещен внутри выбранного периода), то в левой таблице остатков запись может отсутствовать (или потребовать полного соединения), что приведет к потере оборотов.

Стандартным и наиболее производительным шаблоном платформы 1С для сведения остатков и оборотов является объединение потоков данных через ОБЪЕДИНИТЬ ВСЕ с последующей единой группировкой:

  1. В первой ветке объединения выбираем начальные и конечные остатки из виртуальной таблицы ОстаткиИОбороты без детализации по регистратору. В поля оборотов перемещения подставляем 0.
  2. Во второй ветке обращаемся напрямую к физической таблице движений регистра накопления РегистрНакопления.ТоварыВРознице, накладывая условие на период, вид движения (Расход) и тип регистратора (ССЫЛКА Документ.ПеремещениеТоваров). Поля остатков заполняем нулями.
  3. В третьей ветке (при необходимости) выбираем данные продаж.
  4. Помещаем объединенный поток во временную таблицу и выполняем финальную группировку с суммированием числовых колонок.

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

Дополнительные рекомендации по оптимизации запросов

При написании выборок по регистрам накопления важно учитывать следующие правила:

  1. Избегайте разыменования составных полей. Конструкции вида ПродажиОбороты.Регистратор.Склад заставляют СУБД формировать внешние соединения со всеми возможными таблицами документов-регистраторов. Если измерение отсутствует в регистре, надежнее связать запрос напрямую с реальной таблицей конкретного документа по полю Ссылка.
  2. Используйте параметры виртуальных таблиц. Фильтрация в секции ГДЕ (например, ГДЕ Склад.Наименование ПОДОБНО ...) выполняется уже после того, как платформа рассчитала итоги по всей базе. Условия по измерениям следует передавать строго в параметры виртуальной таблицы.
  3. Использование механизма СКД. Если задача решается в рамках схемы компоновки данных, дублирование остатков при детализации до регистратора штатно предотвращается настройкой ролей полей («Начальный остаток», «Конечный остаток») и указанием правильных выражений для ресурсов, рассчитываемых по соответствующим периодам.
← На главную