Почему запрос в 1С возвращает неверные суммы и количество документов?

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

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

Причины расхождения сумм и количества документов

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

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

Анализ исходного кода и типичных ошибок

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


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

В данном примере запрос итогов выбирает вообще все проведенные документы контрагента за период, в то время как расшифровка ниже по коду отбирает документы с учетом условий Гонорар = ИСТИНА и проверки оплаты через ДокументПКО. В результате в шапке группировки отображаются одни цифры, а в детальных строках — совершенно другие.

Правильное решение: единый запрос с секцией ИТОГИ

Разберем по шагам, как избавиться от запросов в цикле и получить корректные сводные и детальные данные одновременно. Для этого воспользуемся секцией ИТОГИ ... ПО, объединив агрегатные функции и детальные поля в одном запросе.


Процедура СформироватьОтчет()
    ДатНач = НачалоДня(ЭлементыФормы.НачалоПериода.Значение);
    ДатКон = КонецДня(ЭлементыФормы.КонецПериода.Значение);
    Контрик = ЭлементыФормы.Дропшипер.Значение;
    
    ЭлементыФормы.ПолеОтчёт.Очистить();
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |    РасходнойДокумент.Контрагент КАК Контрагент,
        |    РасходнойДокумент.Ссылка КАК Док,
        |    РасходнойДокумент.Дата КАК Дата,
        |    РасходнойДокумент.СумаДокумента КАК СумаДокумента,
        |    РасходнойДокумент.СумаГонорара КАК СумаГонорара,
        |    РасходнойДокумент.НомерЕН КАК НомерЕН,
        |    РасходнойДокумент.ИнфаКонтр КАК ИнфаКонтр,
        |    РасходнойДокумент.Гонорар КАК Гонорар
        |ИЗ
        |    Документ.РасходнойДокумент КАК РасходнойДокумент
        |ГДЕ
        |    РасходнойДокумент.Контрагент В ИЕРАРХИИ (&Контр)
        |    И РасходнойДокумент.Дата МЕЖДУ &ДатНач И &ДатКон
        |    И РасходнойДокумент.Проведен
        |    И (&ВсеПоОплате ИЛИ РасходнойДокумент.ДокументПКО <> ЗНАЧЕНИЕ(Документ.ПриходнойКассовыйОрдер.ПустаяСсылка))
        |    И РасходнойДокумент.Гонорар = &Гонорар
        |ИТОГИ
        |    КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Док),
        |    СУММА(СумаДокумента),
        |    СУММА(СумаГонорара)
        |ПО
        |    Контрагент";
        
    РодительПоУмолчанию = Справочники.Контрагенты.НайтиПоКоду("000000005");
    Запрос.УстановитьПараметр("Контр", ?(Контрик.Пустая(), РодительПоУмолчанию, Контрик));
    Запрос.УстановитьПараметр("ДатНач", ДатНач);
    Запрос.УстановитьПараметр("ДатКон", ДатКон);
    Запрос.УстановитьПараметр("ВсеПоОплате", НЕ ЭлементыФормы.ТолькоОплаченные.Значение);
    Запрос.УстановитьПараметр("Гонорар", ЭлементыФормы.Гонорар.Значение);
    
    РезультатЗапроса = Запрос.Выполнить();
    
    // Обход группировок (итоги по контрагентам)
    ВыборкаГруппировка = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
    Пока ВыборкаГруппировка.Следующий() Цикл
        ЭлементыФормы.ПолеОтчёт.ДобавитьСтроку();
        ЭлементыФормы.ПолеОтчёт.ТекущаяСтрока.Дропшипер = ВыборкаГруппировка.Контрагент;
        ЭлементыФормы.ПолеОтчёт.ТекущаяСтрока.РасходнойДокумент = ВыборкаГруппировка.Док;
        ЭлементыФормы.ПолеОтчёт.ТекущаяСтрока.СумаДокумента = ВыборкаГруппировка.СумаДокумента;
        ЭлементыФормы.ПолеОтчёт.ТекущаяСтрока.Гонорар = ВыборкаГруппировка.СумаГонорара;
        
        // Обход детальных записей внутри текущей группировки
        ВыборкаДетали = ВыборкаГруппировка.Выбрать();
        Пока ВыборкаДетали.Следующий() Цикл
            ЭлементыФормы.ПолеОтчёт.ДобавитьСтроку();
            ЭлементыФормы.ПолеОтчёт.ТекущаяСтрока.РасходнойДокумент = ВыборкаДетали.Док;
            ЭлементыФормы.ПолеОтчёт.ТекущаяСтрока.ДатаДокумента = ВыборкаДетали.Дата;
            ЭлементыФормы.ПолеОтчёт.ТекущаяСтрока.СумаДокумента = ВыборкаДетали.СумаДокумента;
            ЭлементыФормы.ПолеОтчёт.ТекущаяСтрока.НомерТТН = ВыборкаДетали.НомерЕН;
            ЭлементыФормы.ПолеОтчёт.ТекущаяСтрока.ГонорарГалочка = ВыборкаДетали.Гонорар;
            ЭлементыФормы.ПолеОтчёт.ТекущаяСтрока.Гонорар = ВыборкаДетали.СумаГонорара;
            ЭлементыФормы.ПолеОтчёт.ТекущаяСтрока.Покупатель = ВыборкаДетали.ИнфаКонтр;
        КонецЦикла;
    КонецЦикла;
КонецПроцедуры

Рекомендации по оптимизации и написанию запросов

Рассмотрим правила, которые позволяют избежать логических и платформенных ошибок при работе с запросами:

  1. Использование оператора ЗНАЧЕНИЕ: Для проверки пустых ссылок, перечислений и предопределенных элементов используйте конструкцию ЗНАЧЕНИЕ(...) непосредственно в тексте запроса вместо передачи пустых ссылок через параметры из встроенного языка.
  2. Исключение избыточных условий: Условие И НЕ Документ.ПометкаУдаления является избыточным, если в запросе уже проверяется условие И Документ.Проведен, так как помеченный на удаление документ не может быть проведен.
  3. Проверка дат: Для корректной фильтрации за период используйте оператор МЕЖДУ в сочетании с обработкой границ через НачалоДня() и КонецДня() (или функцию запроса КОНЕЦПЕРИОДА(&ДатКон, ДЕНЬ)).
  4. Применение Системы компоновки данных (СКД): Для построения отчетов со сложными иерархическими группировками и итогами рекомендуется использовать СКД, которая автоматически строит оптимальные запросы к СУБД и рассчитывает агрегаты без ручного программирования обхода выборок.
← На главную