Давайте подробно разберем одну из самых известных и классических проблем, с которой сталкиваются программисты при разработке на платформе 1С:Предприятие 7.7. Ситуация выглядит следующим образом: мы пишем стандартный запрос к документу, используем встроенную функцию Сумма() для подсчета итогов по строкам, и все работает корректно, пока в документе не появляются две или более строки с абсолютно одинаковой номенклатурой. Как только это происходит, механизм запросов начинает вести себя непредсказуемо — суммы задваиваются, затраиваются, либо умножаются на количество дублирующихся строк.
Проанализируем ситуацию: почему так происходит и что мы делаем не так? Мы выясним истинную причину такого поведения движка 1С 7.7 (так называемого "черного запроса") и подробно рассмотрим несколько способов решения этой задачи — от базового исправления исходного кода до профессиональной оптимизации с использованием компоненты 1С++.
Для начала давайте разберемся, почему сумма вообще умножается. Проблема кроется во внутренних механизмах обработки данных платформы 1С 7.7. Когда в табличной части документа присутствуют несколько строк с одинаковым значением (например, одна и та же номенклатура), а наш запрос группирует данные по этому измерению без жесткой привязки к уникальному идентификатору конкретной строки, происходит непредвиденное внутреннее соединение таблиц.
В терминологии баз данных это называется декартовым произведением. Движок "не понимает", как правильно разделить одинаковую номенклатуру по разным физическим строкам документа, и неявно пытается их "схлопнуть" или перемножить. В результате агрегатная функция Сумма() отрабатывает некорректно. Итоговый результат умножается на количество одинаковых строк.
Рассмотрим пример кода, в котором была допущена типичная ошибка. В попытках решить проблему разработчик добавил в текст запроса группировку Группировка НомСтр;, но штатная функция расчета сумм все равно не заработала, из-за чего пришлось писать громоздкие циклы и считать итоги вручную переменными вроде ИтКво = ИтКво + Запрос.Кво;.
Посмотрим на ошибочный фрагмент запроса:
ТекстЗапроса =
"//{{ЗАПРОС(Сформировать)
|Период с ДатаС по ДатаПо;
|ТекДок = Документ.Требование.ТекущийДокумент;
|МестоХранения = Документ.Требование.МестоХранения;
...
|Кво = Документ.Требование.Кво;
|ТМЦ = Документ.Требование.ТМЦ ;
|Функция КвоСумма = Сумма(Кво);
|Группировка ТекДок;
|Группировка ТМЦ без групп;
|Группировка НомСтр;
|" ;
В чем здесь кроется ошибка? Механизм 1С 7.7 требует четкого объявления всех переменных, которые участвуют в запросе. Мы видим, что в конце текста запроса указана Группировка НомСтр;, но в верхнем блоке объявления переменных (где мы привязываем переменные к реквизитам базы данных) эта переменная полностью отсутствует. Движок просто игнорирует эту группировку, так как не знает, откуда брать данные для НомСтр.
Давайте исправим этот код. Чтобы агрегатные функции начали считать корректно и перестали задваивать значения, нам нужно явно объявить системный атрибут НомерСтроки (или СтрокаДокумента) в секции переменных. Разберем исправленный и полностью рабочий вариант:
Запрос = СоздатьОбъект("Запрос");
ТекстЗапроса =
"//{{ЗАПРОС(Сформировать)
|Период с ДатаС по ДатаПо;
|ТекДок = Документ.Требование.ТекущийДокумент;
|ТМЦ = Документ.Требование.ТМЦ;
|Кво = Документ.Требование.Кво;
|СуммаБезНДС = Документ.Требование.СуммаБезНДС;
|
|// ОБЯЗАТЕЛЬНО объявляем переменную номера строки
|НомСтр = Документ.Требование.НомерСтроки;
|
|Функция КвоСумма = Сумма(Кво);
|Функция СуммаБезНДССумма = Сумма(СуммаБезНДС);
|
|Группировка ТекДок;
|Группировка ТМЦ без групп;
|Группировка НомСтр; // Теперь эта группировка сработает!
|" ;
Если Запрос.Выполнить(ТекстЗапроса) = 0 Тогда
Возврат;
КонецЕсли;
Пока Запрос.Группировка(1) = 1 Цикл
Пока Запрос.Группировка(2) = 1 Цикл
// Здесь уже не нужно считать суммы вручную!
// Значения Запрос.КвоСумма и Запрос.СуммаБезНДССумма
// будут абсолютно корректными, без задвоений.
Сообщить("ТМЦ: " + Запрос.ТМЦ + " Количество: " + Запрос.КвоСумма);
КонецЦикла;
КонецЦикла;
Благодаря этому небольшому исправлению мы избавляемся от необходимости прописывать третий вложенный цикл по строкам и суммировать переменные "на лету". Стандартный механизм функций 1С теперь корректно изолирует каждую строку документа при подсчете.
Рассмотрим более элегантный и производительный подход, который часто рекомендуют опытные разработчики. Суть метода заключается в полном отказе от использования встроенных функций запроса Сумма() в тексте запроса. Вместо этого мы вытаскиваем "сырые" данные и обрабатываем их с помощью объекта ТаблицаЗначений.
Этот подход позволяет радикально сократить объем кода, избавиться от громоздких конструкций Пока Запрос.Группировка()... и полностью нивелировать баги агрегации. Давайте разберем по шагам, как это реализовать.
Функция.Свернуть() для получения идеальных итогов.Посмотрим на практический пример кода:
Запрос = СоздатьОбъект("Запрос");
ТекстЗапроса =
"//{{ЗАПРОС(Сформировать)
|Период с ДатаС по ДатаПо;
|ТекДок = Документ.Требование.ТекущийДокумент;
|ТМЦ = Документ.Требование.ТМЦ;
|Кво = Документ.Требование.Кво;
|СуммаБезНДС = Документ.Требование.СуммаБезНДС;
|Условие(МестоХранения В ВыбСклад);
|Группировка ТекДок;
|Группировка Документ.Требование.Строка; // Детализация до строки
|" ;
Если Запрос.Выполнить(ТекстЗапроса) = 0 Тогда
Возврат;
КонецЕсли;
ТЗ = СоздатьОбъект("ТаблицаЗначений");
// Выгружаем результаты запроса напрямую в ТЗ без циклов!
// Параметры (ТЗ, 0, 0) означают выгрузку всех группировок
Запрос.Выгрузить(ТЗ, 0, 0);
// Теперь у нас в ТЗ есть дублирующиеся строки номенклатуры.
// Избавляемся от них магией метода Свернуть:
// Первый параметр - колонки группировки.
// Второй параметр - колонки суммирования.
ТЗ.Свернуть("ТекДок, ТМЦ", "Кво, СуммаБезНДС");
// Выводим готовую, отфильтрованную таблицу без задвоений
ТЗ.ВыбратьСтроки();
Пока ТЗ.ПолучитьСтроку() = 1 Цикл
Сообщить("Документ: " + ТЗ.ТекДок + " | ТМЦ: " + ТЗ.ТМЦ + " | Итого Кво: " + ТЗ.Кво);
КонецЦикла;
Этот шаблон является золотым стандартом для решения подобных проблем в штатных запросах 7.7. Метод Свернуть() работает исключительно быстро и берет на себя всю работу по сложению сумм одинаковых позиций внутри документа.
Нельзя обойти стороной самый современный подход к разработке на 1С 7.7. На сегодняшний день в профессиональном сообществе признано, что штатный объект Запрос морально устарел, работает нестабильно в сложных ситуациях и потребляет слишком много ресурсов сервера. Стандартом де-факто для решения любых проблем с выборкой данных является использование внешней компоненты 1С++.
С помощью этой компоненты мы можем обращаться напрямую к базе данных (через технологию ADO или встроенный класс ODBCRecordset), используя стандартный синтаксис языка T-SQL. Выполняя обычный SQL-запрос, мы полностью минуем "черный ящик" движка 1С и его баги с декартовым произведением.
Посмотрим на пример реализации такого решения. Мы используем конструкцию GROUP BY, которая гарантированно даст абсолютно точные математические суммы.
// Предполагаем, что компонента 1C++ уже загружена в системе
БД = СоздатьОбъект("ODBCRecordset");
ТекстSQL = "
|SELECT
| Журнал.ИДДок AS [Документ $Документ.Требование],
| Строки.ТМЦ AS [ТМЦ $Справочник.Номенклатура],
| SUM(Строки.Кво) AS Количество,
| SUM(Строки.СуммаБезНДС) AS СуммаБезНДС
|FROM
| $ДокументСтроки.Требование AS Строки
|INNER JOIN
| _1SJOURN AS Журнал ON Журнал.ИДДок = Строки.ИДДок
|WHERE
| Журнал.DATE_TIME_IDDOC BETWEEN :НачДата AND :КонДата~
| AND Журнал.Closed & 1 = 1 -- Только проведенные документы
|GROUP BY
| Журнал.ИДДок,
| Строки.ТМЦ
|ORDER BY
| Журнал.ИДДок
|";
БД.УстановитьТекстовыйПараметр("НачДата", ВыбНачПериода);
БД.УстановитьТекстовыйПараметр("КонДата", ВыбКонПериода);
ТЗ = БД.ВыполнитьИнструкцию(ТекстSQL);
// В результате переменная ТЗ содержит готовую таблицу с идеальными итогами.
// Скорость выполнения такого запроса в десятки раз выше, чем у штатного!
Подведем итоги. Мы проанализировали причины задвоения сумм в 1С 7.7 и рассмотрели три рабочих метода решения проблемы. Если вам нужно быстро поправить существующий код, добавьте системную переменную НомерСтроки и выведите ее в группировку. Если код перегружен вложенными циклами — перепишите его на выгрузку в ТаблицаЗначений с последующим применением метода Свернуть(). Ну а если вы стремитесь к максимальной производительности и надежности — переходите на прямые SQL-запросы с использованием 1С++, это навсегда избавит вас от сюрпризов стандартного движка.