
Есть класс ошибок, который дороже падения. Отчёт формируется, цифры выводятся, никто ничего не подозревает, и только через месяц кто-то замечает, что итог по группе не равен сумме строк под ним.
Вот такой случай. Кросс-таблица: по строкам номенклатура внутри категории, по колонкам годы. Считается прирост количества год к году, причём приростом считается только тот случай, когда в предыдущем году было не ноль. По номенклатуре всё верно. На уровне категории - мусор.
Выражение ресурса для категории выглядит так:
СУММА(ВычислитьВыражениеСГруппировкойМассив( "ВЫБОР КОГДА ВычислитьВыражение(""ЕстьNULL(Сумма(Количество), 0)"", ""Год"", , ""Предыдущая"", ""Предыдущая"", ""Год"") <> 0 ТОГДА ЕстьNULL(Сумма(Количество), 0) - ВычислитьВыражение(""ЕстьNULL(Сумма(Количество), 0)"", ""Год"", , ""Предыдущая"", ""Предыдущая"", ""Год"") ИНАЧЕ 0 КОНЕЦ", "Номенклатура"))
Идея понятная: пройти массивом по номенклатуре внутри категории, для каждой позиции посчитать отклонение от прошлого года и сложить. Компилируется. Отрабатывает. Даёт не то.
Разбор ниже: что именно возвращает эта конструкция, почему платформа на ней не ругается, как проверить у себя за пять минут и чем лечится.
Сразу про источник, чтобы не было вопросов. Случай пришёл с форума Инфостарта, тема 343053, где я отвечал в августе. Разбор механизма мой, воспроизведение и проверка на своих наборах - автора темы, он там под ником rossin. Я на этом настаиваю потому, что дальше будет тезис, который я же там и снял, а такое лучше рассказывать самому.
Что возвращает массив
Первое, что стоит сделать при разборе, - убрать внешнюю СУММА и посмотреть на сам массив. Автор темы так и поступил.
Ожидание: массив из отклонений по каждой номенклатуре категории. Скажем, две позиции: у первой было 10, стало 14, отклонение +4; у второй было 100, стало 100, отклонение 0. Итог по категории должен быть 4.
Что приходит на деле: массив из одинаковых элементов, и каждый равен величине, посчитанной по всей категории целиком. Не по позиции. То есть если в категории за прошлый год суммарно 110, то вложенный вызов вернёт 110 и для первой номенклатуры, и для второй.
Дальше арифметика беспощадна: (14 - 110) + (100 - 110) = -106 вместо ожидаемых 4. Знак другой, порядок другой, и это ещё удачный случай - расхождение видно глазом. При других данных разница может выйти правдоподобной, и тогда её никто не поймает.
Механизм: две функции сужают контекст по разным осям
Теперь почему так.
ВычислитьВыражениеСГруппировкойМассив действительно перебирает записи названной ей группировки. В нашем случае - "Номенклатура". Для каждой записи она вычисляет переданное выражение и складывает результаты в массив. Пока всё как ожидается.
А вот вложенное ВычислитьВыражение со смещением "Предыдущая" отсчитывает позицию не внутри этого перебора. Оно отсчитывает её по структуре той группировки, для которой пишется само выражение ресурса. Ресурс мы пишем для категории, значит и смещение считается в контексте категории.
Отсюда одинаковые элементы: перебор идёт по номенклатуре, а вложенный вызов на каждом шаге отвечает одно и то же, потому что для него ничего не менялось.
Ключевое следствие, ради которого стоит это запомнить:
Пока обе функции работают по одной оси, разницы не видно. Если группировка, которую перебирает ВВГМ, и группировка, по которой смещается вложенный вызов, лежат на одной ветке структуры, контекст сужается согласованно и результат совпадает с ожиданиями. Именно поэтому конструкция годами живёт в отчётах и считается рабочей.
В кросс-таблице оси разные. Номенклатура в строках, год в колонках. Перебор идёт по одной оси, смещение по другой, и вложенный вызов просто остаётся снаружи перебора.
Почему компиляция это пропускает
Вопрос законный: если конструкция бессмысленна, почему платформа не ругается.
Потому что она не бессмысленна синтаксически. Первый аргумент ВВГМ - строка. Внутри неё лежит выражение на языке СКД, и это выражение само по себе корректно: ВычислитьВыражение с существующей группировкой и допустимыми видами границ. Компилятор проверяет, что группировка "Год" в отчёте есть, что смещение "Предыдущая" - валидное значение, что скобки сходятся. Всё сходится.
Вопрос "а к какому контексту относится это смещение при переборе" компилятором не задаётся вовсе. Он и не должен: контекст определяется в момент вычисления, а не в момент разбора.
Отсюда общее наблюдение, которое шире этого случая. В языке выражений СКД строковый аргумент - это дыра в проверках. Всё, что вы передаёте текстом, проверяется поверхностно, а ошибки контекста вылезают только на данных. Ровно поэтому вложенные конструкции в ресурсах стоит держать простыми: сложную вложенность никто, кроме вас, не проверит.
Как проверить у себя
Рецепт на пять минут, без наших данных и без нашей конфигурации.
- Сделайте отчёт на запросе из четырёх строк литералами: две номенклатуры, два года, количества 10 и 14 для первой, 100 и 100 для второй.
- Структура - таблица: в строках категория и вложенная в неё номенклатура, в колонках год.
- Ресурс отклонения задайте дважды: для группировки "Номенклатура" - обычным
ВычислитьВыражениесо смещением, для группировки "Категория" - тем же выражением, завёрнутым в ВВГМ по номенклатуре. - Сформируйте и сравните строку категории с суммой строк под ней.
Если ваш итог по категории равен 4 - у вас всё сходится, и дальше можно не читать. Если -106, вы воспроизвели ровно тот случай.
Отдельно советую убрать внешнюю СУММА и вывести сам массив: одинаковые элементы в нём - самый наглядный признак того, что перебор идёт вхолостую.
Чем лечится
Коротко: расчёт переносится в запрос, и СКД перестаёт быть местом, где считается логика.
Схема, которую я предложил в той теме и которую автор в итоге внедрил:
- Собрать полный крест измерений: список всех лет периода на список всей номенклатуры. Это обязательный шаг, а не украшение. В исходном наборе строк с нулём просто нет, и случай "закупали 5, стало 0" без креста в отчёт не попадёт вообще.
- Двумя левыми соединениями подтянуть количество текущего года и количество предыдущего, условие соединения по году со сдвигом на единицу.
- Прямо в запросе посчитать поле отклонения:
ВЫБОР КОГДА ЕСТЬNULL(КоличествоПред, 0) <> 0 ТОГДА ЕСТЬNULL(Количество, 0) - ЕСТЬNULL(КоличествоПред, 0) ИНАЧЕ 0 КОНЕЦ КАК Отклонение
После этого ресурс на всех уровнях один и тот же: СУММА(Отклонение). По номенклатуре он даст отклонение позиции, по категории сложит их сам, по общему итогу тоже. Ни ВВГМ, ни смещений, ни ролей, ни "рассчитывать по" - складывать числа СКД умеет без подсказок.
Побочно решается вторая проблема, которая в такой схеме обязательно выстрелит позже. Правило "прирост считаем, только если предыдущий не ноль" делает показатель несовместимым с автоматическим итогом, пока он живёт в выражении ресурса: итог по определению не равен сумме слагаемых, потому что часть слагаемых обнулена по условию. Когда показатель посчитан построчно в запросе, итог становится обычной суммой, и вопрос снимается.
Цена решения честная: запрос усложняется, появляется крест и два соединения. Взамен уходит вся конструкция из ресурсов и вместе с ней класс ошибок, который не ловится ни компиляцией, ни глазами.
Гипотеза, которую я снял
Обещанное в начале. Разобрав дельту, я предположил, что рядом лежит вторая ошибка того же рода: процент отклонения остался считаться через ВычислитьВыражение, и мне казалось, что числитель и знаменатель там разъедутся. Логика была такая: числитель это полная разница, включая позиции, стартовавшие с нуля, а знаменатель их не учитывает, значит на уровне категории колонка процента посчитается от другого набора позиций, чем колонка дельты.
Автор темы возразил, и возражение оказалось верным. В его формуле числитель берётся полем из запроса, где правило обнуления уже применено. При таком числителе всё сходится и на категории: позиции, у которых в прошлом году ноль, в знаменатель ничего не добавляют, поэтому знаменатель сам совпадает с тем набором, по которому посчитан числитель.
Гипотезу я снял там же, в теме, дословно: "Согласен, снимаю". Пишу об этом здесь по двум причинам. Во-первых, если бы я промолчал, разбор выглядел бы стройнее, чем есть, а разбор с подчищенной историей - плохой разбор. Во-вторых, из этой ошибки получается практическое правило: прежде чем объявлять конструкцию с ВычислитьВыражение сломанной, посмотрите, откуда берётся её числитель. Половина подозрительных выражений на поверку опирается на поле из запроса и работает верно.
Что показал наш собственный прогон
Воспроизвести случай я решил программно: собрать схему компоновки прямо из кода и скомпоновать на тестовой УТ 11.5, платформа 8.3.27.1606. Данные те же четыре строки литералами. Вышло не то, что ожидалось, и это стоит рассказать целиком.
Сам ВВГМ работает как написано в документации. Ресурс СУММА(ВычислитьВыражениеСГруппировкойМассив("Сумма(Количество)", "Номенклатура")) на уровне категории даёт 224 - это 24 по первой позиции плюс 200 по второй, то есть перебор действительно идёт по номенклатуре и складывается правильно.
Вложенный вызов без второго аргумента тоже работает. Если внутрь положить ВычислитьВыражение("Сумма(Количество)"), результат тот же 224. Значит вложенный вызов честно сужается контекстом перебора, пока ему не назвали чужую группировку.
А вот с именем группировки компоновка падает:
Выражение не может быть вычислено "Сумма(Н.Кол), Год"
Смотрите, что здесь произошло. Первый аргумент внутреннего вызова склеился со вторым через запятую, кавычки между ними исчезли при разборе строки. То есть до вычисления контекста дело даже не дошло, сломался разбор.
Что это означает и чего не означает. Это не опровержение механизма: в исходном случае конструкция вычислялась и возвращала массив одинаковых чисел, автор темы видел этот массив своими глазами, убрав внешнюю сумму. Это граница программной сборки: выражение, положенное в схему из кода, разбирается не так, как то же выражение, введённое руками в конфигураторе.
Практический вывод для тех, кто собирает СКД программно: вложенные вызовы с указанием группировки в выражениях ресурсов из кода не задавайте. Проверить это у себя дешевле, чем ловить потом: полминуты на схему из четырёх строк.
Границы применимости
Случай разобран на кросс-таблице. Утверждение "оси разные, поэтому не сужается" проверено на структуре, где номенклатура в строках, а год в колонках. Прочие сочетания - вложенность на одной ветке, несколько уровней колонок, группировки с иерархией - в этом разборе не проверялись.
Механизм я вывел разбором, а не своим прогоном на этой конструкции. Автор темы подтвердил его своими наборами данных и наблюдением за массивом. Свой прогон у меня есть, но он показал другое, и об этом раздел выше.
Про роли полей. В исходном отчёте у поля года стояла роль периода. Влияет ли она на поведение вложенного смещения, я не проверял, и если у кого-то без роли получается иначе - это интересный факт, напишите.
Версия. Разговор шёл в августе 2026 года на актуальной на тот момент платформе. Что будет на 8.3.28, никто не знает, и проверять это должен тот, кто на неё перейдёт.
Открытый вопрос
Меня в этой истории занимает не столько сам механизм, сколько то, что его нигде нет. Автор темы написал прямо: перечитал всё, от ИТС и документации до тем на Инфостарте и Мисте, и нигде не сказано ни что так работает, ни что так не работает. Я тоже не нашёл. Каждая функция описана сама по себе, а про их композицию не написано ничего.
Вопрос к тем, кто много работает с СКД: где вообще проходит граница разумного во вложенных выражениях ресурсов? Есть позиция "в ресурсах вообще ничего сложнее суммы делать не надо, всё в запрос" - и в ней много правды, но она стоит усложнения запроса на каждый чих. Есть противоположная: СКД для того и даёт эти функции, чтобы не плодить соединения. Где вы для себя проводите черту и по какому признаку?
Другие наши инструменты:
- Трансформатор SQL в 1С - когда отчёт не только врёт, но и тормозит: переводит запрос из профайлера в имена справочников и регистров.
- УНИЧТОЖИТЬ в конце пакета - разбирает пакетный запрос и показывает, где временная таблица висит дольше, чем нужна. Как раз про схему с крестом и двумя соединениями.
- Чек-ап СУБД под 1С - если после переноса расчёта в запрос отчёт стал тяжелее, вопросы уже к настройкам сервера.
- Карта объёмов базы 1С - что занимает место в базе, если крест по всем измерениям вдруг оказался слишком большим.
Вступайте в нашу телеграмм-группу Инфостарт