Почему искажаются и задваиваются остатки в запросах 1С при соединении таблиц и как это исправить?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

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

Разберем по шагам, почему прямое соединение таблиц приводит к ошибке, и выясним причину такого поведения базы данных. Затем мы вместе решим эту задачу правильными методами.

Суть проблемы: Левое соединение и виртуальные таблицы

Посмотрим на пример исходного подхода, который вызывает ошибку. Разработчик берет виртуальную таблицу ОстаткиИОбороты и делает к ней прямое ЛЕВОЕ СОЕДИНЕНИЕ с виртуальной таблицей СрезПоследних. Для группировки результата используется секция ИТОГИ с функцией СУММА() по количеству.

Когда мы соединяем эти таблицы напрямую, база данных сначала выполняет само соединение. Если в присоединяемой таблице (например, по гарантиям) находится несколько записей для одной номенклатуры или нарушается уникальность ключа соединения, строка с остатком дублируется. И только после этого применяется оператор итога. В результате значения остатков многократно суммируются для каждой сдублированной строки, и цифры начальных и конечных остатков становятся "кривыми".

Кроме того, в исходной задаче была допущена логическая ошибка соединения по полю Регистратор. Важно понимать: поле Регистратор в срезе последних просто показывает документ, который сделал последнюю запись по измерению. Привязывать его к регистратору, двигающему остатки на складе, бессмысленно — это приведет к потере данных, если гарантию устанавливал не тот же самый документ, что приходовал товар.

Решение 1: Использование временных таблиц (Best Practice)

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

Посмотрим на пример правильного кода, который решает нашу проблему:


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

В этом варианте мы разделили логику. Сначала подготовили "чистые" данные во временных таблицах Гарантия и Остатки. Затем соединили их исключительно по полю Номенклатура. Это снимает излишнюю непредсказуемую нагрузку с СУБД и предотвращает некорректный расчет итогов, так как базовые данные остатков уже зафиксированы.

Решение 2: Роль поля ПериодСекунда

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

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

Решение 3: Использование Системы Компоновки Данных (СКД)

Рассмотрим альтернативный, более современный вариант. Если ваш запрос предназначен для формирования отчета для пользователей, настоятельно рекомендуется не писать жесткие секции ИТОГИ в коде запроса, а использовать Систему Компоновки Данных (СКД).

Механизмы СКД умеют решать проблему задвоения остатков "из коробки". Разберем, как это настроить:

  1. В наборе данных СКД добавьте ваши таблицы и укажите связи между ними.
  2. Перейдите на вкладку настройки полей и обратите внимание на колонку "Роли".
  3. Для полей номенклатуры и склада назначьте роль Измерение.
  4. Для полей начального и конечного остатка назначьте роли НачальныйОстаток и КонечныйОстаток соответственно, обязательно связав их друг с другом в настройках роли.

При такой настройке, даже если при соединении возникнет дублирование строк из-за сложной связи со справочниками или свойствами, алгоритмы СКД "поймут" структуру данных. СКД корректно сгруппирует остатки по измерениям и не будет суммировать дублирующиеся строки, выдавая математически верный результат без завышения цифр.

Применяйте временные таблицы для изоляции данных перед соединением, следите за логикой связей (не соединяйте по Регистратор со срезом последних без явной необходимости) и делегируйте расчет итогов СКД там, где это возможно. Это сделает ваш код надежным и защитит данные от искажений.

← На главную