Почему кратно увеличивается расчетная сумма при соединении таблиц документа с регистром сведений в 1С?

Программист 1С v8.3 (Управляемые формы) Управленческий учет Промышленность, строительство и АПК
← К списку

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

Причина проблемы: задвоение строк при соединении

Основная причина кратного увеличения сумм заключается в задвоении или умножении строк на промежуточных этапах выполнения запроса. Это происходит, когда одна строка из одной таблицы соединяется с несколькими строками из другой таблицы, хотя по логике должна была бы соединиться только с одной. При последующей агрегации (например, при использовании функции СУММА) эти задвоенные строки приводят к некорректно завышенным результатам.

Рассмотрим подробнее типовые сценарии, приводящие к такой ситуации:

  1. Недостаточно специфичные условия соединения (JOIN): Если условия соединения между таблицами не обеспечивают уникальность связей, одна запись из "левой" таблицы может быть связана с множеством записей из "правой".
  2. Неполный учет измерений регистра сведений: При соединении с виртуальными таблицами регистров сведений, такими как РегистрСведений.ЦеныНоменклатуры.СрезПоследних, критически важно учитывать все ключевые измерения регистра в условиях соединения. Если какое-либо измерение (например, ВидЦен, ХарактеристикаНоменклатуры) отсутствует в условии, для одной и той же номенклатуры может быть найдено несколько цен, что приведет к умножению строк и, следовательно, сумм.
  3. Неправильный порядок группировки и агрегации: Если группировка и суммирование происходят на слишком позднем этапе запроса, уже после того, как произошло задвоение строк во вложенных или промежуточных таблицах, это также приведет к некорректным результатам.

Пошаговый анализ и методы отладки запроса

Когда вы сталкиваетесь с аномально большими суммами, необходимо методично подойти к поиску ошибки. Давайте разберем по шагам, как это сделать:

  1. Максимально упростите запрос: Удалите из запроса все лишние поля, соединения, группировки и условия, оставив только те части, которые потенциально могут вызывать задвоение.
  2. Сделайте отбор по одному документу (или одной ключевой сущности): Ограничьте выборку до одной записи (например, по конкретному ЗаказНаПроизводство или Номенклатуре). Это позволит нам легче анализировать промежуточные результаты.
  3. Уберите группировки и агрегирующие функции: Временно уберите все СУММА(), КОЛИЧЕСТВО() и секцию СГРУППИРОВАТЬ ПО. Вы увидите все строки, которые формируются до агрегации.
  4. Проанализируйте результат в консоли запросов: Используйте консоль запросов 1С для пошагового выполнения и анализа каждого вложенного запроса и соединения. Обратите внимание на количество строк и их содержимое. Если количество строк значительно больше, чем вы ожидаете для одной записи, вы нашли место задвоения.
  5. Сверьте данные в документе и регистре сведений: Убедитесь, что для выбранной вами номенклатуры и характеристик в регистре сведений действительно существует одна или несколько записей, и проверьте, как эти записи соотносятся с данными документа.

Разбираем типовые ошибки при работе с регистрами сведений и табличными частями документов

Проанализируем ситуацию на примере исходного запроса, где проблема возникла при соединении табличной части документа ЗаказНаПроизводство.Запасы с регистром сведений ЦеныНоменклатуры.СрезПоследних.

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


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

Здесь мы видим, что в виртуальной таблице ЦеныНоменклатуры.СрезПоследних используется отбор по ВидЦен = &УчетнаяЦена, а соединение происходит по Номенклатура и Характеристика. Несмотря на это, проблема задвоения все равно возникла. Это указывает на то, что даже с учетом ВидЦен, для одной и той же пары Номенклатура-Характеристика на указанную дату могли существовать несколько записей в срезе последних, которые удовлетворяли условию, или же сам по себе регистр мог содержать дубликаты по другим причинам, которые не были учтены в соединении.

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

Основное решение: правильный порядок группировки и соединений

В нашем случае, как и во многих подобных ситуациях, решение заключалось в изменении порядка выполнения операций в запросе. Проблема была в том, что задвоение строк происходило во вложенной таблице (например, МатериалаПлан) *до* того, как были применены окончательные группировки и агрегации. Это означает, что данные умножались, а затем уже умноженные значения суммировались.

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

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

Пример изменения логики для части МатериалаПлан (концептуально):

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

Решение: Сначала убедимся, что мы получаем уникальную цену для каждой Номенклатуры/Характеристики, или, что более эффективно, если проблема в задвоении строк самого документа при соединении с другими таблицами, то сначала группируем данные документа по уникальным связям.

Если проблема была в задвоении строк *внутри* подзапроса МатериалаПлан, то до присоединения цен, мы должны были бы сгруппировать его по ключевым полям. Однако, в данном случае, вероятнее всего, задвоение происходило из-за того, что для одной строки документа находилось несколько цен, или же из-за того, что после соединения с ценами, при последующих соединениях с другими таблицами (МатериалыФакт, Работы и РасходнаяНакладная), условия этих соединений также не были достаточно уникальными, и строки, уже содержащие цену, продолжали умножаться.

Наиболее эффективным подходом является группировка данных на самом раннем этапе, где потенциально могут возникнуть дубликаты. В данном случае, если ЦеныНоменклатурыСрезПоследних возвращает уникальную цену для каждой комбинации Номенклатура и Характеристика (что предполагается), то проблема может быть в том, что последующие соединения (например, с МатериалыФакт или Работы) также создают дубликаты.

Финальное решение, которое помогло автору, заключалось в том, чтобы "Сперва сгруппируй МатериалыПлан, потом присоедини цены, а потом суммируй". Это означает, что во вложенном запросе МатериалаПлан сначала нужно было выполнить агрегацию или убедиться в уникальности строк, прежде чем соединять его с ценами и другими таблицами. Если в ЗаказНаПроизводство.Запасы были дубликаты по Номенклатура и Характеристика, или если из-за последующих соединений с другими таблицами (например, МатериалыФакт, Работы) происходило умножение, то группировка МатериалаПлан по его ключевым полям (ЗаказНаПроизводство, Номенклатура, Характеристика) должна была быть выполнена раньше.

Пример (концептуально, как мог быть изменен подзапрос МатериалаПлан для предотвращения дублирования до соединения с ценами):


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

ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&ДатаКон, ВидЦен = &УчетнаяЦена) КАК ЦеныНоменклатурыСрезПоследних
ПО СгруппированныеЗапасыПлан.Номенклатура = ЦеныНоменклатурыСрезПоследних.Номенклатура
    И СгруппированныеЗапасыПлан.Характеристика = ЦеныНоменклатурыСрезПоследних.Характеристика

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

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

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

Для предотвращения подобных ошибок в будущем и для эффективной работы со сложными запросами в 1С, мы рекомендуем следующие подходы:

  1. Использование временных таблиц: Для очень сложных запросов с множеством вложенных запросов и соединений, используйте временные таблицы. Это позволяет изолировать логику, проверить промежуточные результаты на каждом шаге и часто значительно улучшает читаемость и производительность запроса. Методы ПОМЕСТИТЬ и ИЗ в запросах 1С незаменимы для этого.
  2. Тщательный анализ полей соединения: Всегда перепроверяйте, что все поля, по которым осуществляется соединение (ПО), действительно обеспечивают необходимую уникальность или соответствие. Особое внимание уделяйте составным ключам, включающим несколько полей.
  3. Глубокое понимание логики регистра сведений: Перед написанием запроса четко уясните структуру регистра сведений: его измерения (которые определяют уникальность записи), ресурсы (которые хранят значения) и реквизиты. Понимание, как регистр заполняется и для каких целей используется, поможет правильно определить условия для срезов и соединений.
  4. Корректное использование параметров виртуальных таблиц: При использовании виртуальных таблиц, таких как СрезПоследних или Остатки, важно правильно указывать все параметры (период, условия отбора), чтобы получить только нужные записи. Например, в условии СрезПоследних(&ДатаКон, ВидЦен = &УчетнаяЦена) убедитесь, что параметр ВидЦен действительно является частью ключа уникальности или отбора, и что он используется корректно.

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

← К списку