Вычисление итогов и сумм в языке запросов 1С 8.3
Содержание
Сумму в запросе 1С считает агрегатная функция СУММА(), а строки, по которым идёт сложение, задаёт секция СГРУППИРОВАТЬ ПО. Если нужны промежуточные итоги по уровням (по контрагенту, внутри — по номенклатуре), к этому добавляется отдельная секция ИТОГИ ... ПО. Это два разных механизма, и путать их не стоит.
Ниже — рабочие конструкции на языке запросов: как просуммировать поле, как группировать, как не получить пустой результат из-за NULL, когда вынести расчёт во временную таблицу и почему складывать суммы в цикле по выборке — почти всегда ошибка. Все примеры — для платформы 8.3.
Агрегатные функции: СУММА и её соседи
Агрегатная функция сворачивает множество строк в одно число. Для суммирования используется СУММА(), но рядом с ней работают и другие — их полезно знать, чтобы не считать то, что платформа посчитает сама.
| Функция | Что возвращает |
|---|---|
СУММА(Поле) | сумму значений по группе строк |
КОЛИЧЕСТВО(*) | число строк в группе |
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Поле) | число уникальных значений поля |
МАКСИМУМ(Поле) | наибольшее значение |
МИНИМУМ(Поле) | наименьшее значение |
СРЕДНЕЕ(Поле) | среднее арифметическое |
Самый простой случай — сумма по всей таблице, без разреза. Группировка не нужна, результат — одна строка:
ВЫБРАТЬ
СУММА(Продажи.Количество) КАК КоличествоВсего,
СУММА(Продажи.Сумма) КАК СуммаВсего
ИЗ
РегистрНакопления.Продажи КАК Продажи
Если в таблице нет ни одной строки, СУММА() вернёт NULL, а не ноль. Это важная деталь — к ней вернёмся в разделе про пустые значения.
Сложить можно и результат выражения, а не только поле. СУММА(Продажи.Цена * Продажи.Количество) корректно перемножит значения в каждой строке, а потом сложит произведения. Это надёжнее, чем считать сумму в коде.
Сумма по условию через ВЫБОР КОГДА
Часто нужна не вся сумма, а её часть — например, только положительные обороты или только по определённому виду операции. Условие заводится внутрь агрегата через конструкцию ВЫБОР:
ВЫБРАТЬ
СУММА(ВЫБОР
КОГДА Движения.Сумма > 0
ТОГДА Движения.Сумма
ИНАЧЕ 0
КОНЕЦ) КАК СуммаПоступлений,
СУММА(ВЫБОР
КОГДА Движения.Сумма < 0
ТОГДА -Движения.Сумма
ИНАЧЕ 0
КОНЕЦ) КАК СуммаСписаний
ИЗ
РегистрНакопления.ДвиженияДенег КАК Движения
Так одним проходом получаются две суммы — приход и расход — вместо двух отдельных запросов.
Группировка: разрез суммы по измерениям
Как только в выборку добавляется поле-измерение (номенклатура, контрагент, организация), начинается группировка. Правило жёсткое: каждое поле, не обёрнутое в агрегатную функцию, обязано присутствовать в секции СГРУППИРОВАТЬ ПО. Иначе платформа откажется выполнять запрос.
ВЫБРАТЬ
Продажи.Номенклатура КАК Номенклатура,
СУММА(Продажи.Количество) КАК Количество,
СУММА(Продажи.Сумма) КАК Сумма
ИЗ
РегистрНакопления.Продажи КАК Продажи
СГРУППИРОВАТЬ ПО
Продажи.Номенклатура
Результат — по строке на каждую номенклатуру с её итоговыми количеством и суммой.
Измерений в группировке может быть несколько. Чем их больше, тем детальнее разрез и тем больше строк на выходе:
СГРУППИРОВАТЬ ПО
Продажи.Организация,
Продажи.Номенклатура
Если забыть поле в СГРУППИРОВАТЬ ПО, запрос упадёт с ошибкой вида «Поле не входит в группировку и не является агрегатной функцией». Это не каприз платформы, а признак того, что вы просите неоднозначный результат: по какому из значений поля сворачивать строки — непонятно.
Отобрать группы по значению агрегата (например, только контрагентов с суммой больше 100 000) помогает не ГДЕ, а секция ИМЕЮЩИЕ — она работает уже после группировки:
СГРУППИРОВАТЬ ПО
Продажи.Контрагент
ИМЕЮЩИЕ
СУММА(Продажи.Сумма) > 100000
ГДЕ фильтрует строки до суммирования, ИМЕЮЩИЕ — готовые суммы после. Путаница между ними — частая причина неверных итогов.
Пустые значения: почему сумма пропадает и как спасает ЕСТЬNULL
NULL в запросе — это «значения нет». Он появляется в двух типичных ситуациях: при левом соединении, когда справа нет подходящей строки, и в ресурсах регистра, по которым не было движений.
Сама СУММА() строки с NULL просто пропускает — как и в SQL. А вот арифметика и сравнения с NULL дают NULL: выражение Остаток - Резерв превратится в пустоту, если хотя бы одно слагаемое пустое. Поэтому значение оборачивают в ЕСТЬNULL() — она подставляет ноль вместо пустоты:
ВЫБРАТЬ
Товары.Номенклатура КАК Номенклатура,
ЕСТЬNULL(Остатки.КоличествоОстаток, 0) КАК Остаток,
ЕСТЬNULL(Резервы.КоличествоРезерв, 0) КАК Резерв,
ЕСТЬNULL(Остатки.КоличествоОстаток, 0)
- ЕСТЬNULL(Резервы.КоличествоРезерв, 0) КАК Свободно
ИЗ
Справочник.Номенклатура КАК Товары
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки КАК Остатки
ПО Остатки.Номенклатура = Товары.Ссылка
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.Резервы.Остатки КАК Резервы
ПО Резервы.Номенклатура = Товары.Ссылка
Здесь ЕСТЬNULL стоит и на самих полях, и внутри вычитания. Без неё строки без остатка дали бы пустое «Свободно» вместо нуля — и дальше в отчёте или коде это аукнулось бы.
Если суммируете поле, которое само может целиком отсутствовать (а итог нужен числом, а не NULL), оберните результат: ЕСТЬNULL(СУММА(Продажи.Сумма), 0). Тогда даже на пустой таблице вернётся ноль.
Промежуточные итоги: конструкция ИТОГИ ПО
СГРУППИРОВАТЬ ПО даёт плоскую таблицу — только нижний уровень детализации. Когда нужны итоги по уровням (общая сумма по контрагенту, а внутри — разбивка по договорам), подключают секцию ИТОГИ ... ПО. Она не сворачивает строки, а достраивает к результату дополнительные итоговые записи.
ВЫБРАТЬ
Продажи.Контрагент КАК Контрагент,
Продажи.Договор КАК Договор,
Продажи.Сумма КАК Сумма
ИЗ
РегистрНакопления.Продажи КАК Продажи
ИТОГИ
СУММА(Сумма)
ПО
Контрагент,
Договор
На выходе — иерархический результат: строка итога по контрагенту, под ней итоги по договорам, под ними детальные записи. Обходят его не обычным Выбрать(), а по группировкам:
Результат = Запрос.Выполнить();
ВыборкаКонтрагент = Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаКонтрагент.Следующий() Цикл
// ВыборкаКонтрагент.Сумма — итог по контрагенту
ВыборкаДоговор = ВыборкаКонтрагент.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаДоговор.Следующий() Цикл
// ВыборкаДоговор.Сумма — итог по договору
КонецЦикла;
КонецЦикла;
Когда использовать ИТОГИ, а когда СГРУППИРОВАТЬ ПО:
- 📊 нужны только итоговые суммы в разрезе — хватит
СГРУППИРОВАТЬ ПО; - 🌳 нужны и детальные строки, и итоги по уровням в одном результате —
ИТОГИ ПО; - 📑 результат ляжет в многоуровневый отчёт или дерево значений —
ИТОГИ ПОзакроет это без ручного кода.
Многоэтапный расчёт через временные таблицы
Если итог собирается из нескольких источников — продажи из одного регистра, оплаты из другого, — считать всё одним запросом с соединениями рискованно: соединение «многие ко многим» задвоит суммы. Правильный приём — сначала свернуть каждый источник до итогов во временной таблице, а потом соединять уже готовые суммы.
ВЫБРАТЬ
Продажи.Контрагент КАК Контрагент,
СУММА(Продажи.Сумма) КАК СуммаПродаж
ПОМЕСТИТЬ ВремПродажи
ИЗ
РегистрНакопления.Продажи КАК Продажи
СГРУППИРОВАТЬ ПО
Продажи.Контрагент
;
ВЫБРАТЬ
Оплаты.Контрагент КАК Контрагент,
СУММА(Оплаты.Сумма) КАК СуммаОплат
ПОМЕСТИТЬ ВремОплаты
ИЗ
РегистрНакопления.Оплаты КАК Оплаты
СГРУППИРОВАТЬ ПО
Оплаты.Контрагент
;
ВЫБРАТЬ
ЕСТЬNULL(ВремПродажи.Контрагент, ВремОплаты.Контрагент) КАК Контрагент,
ЕСТЬNULL(ВремПродажи.СуммаПродаж, 0) КАК СуммаПродаж,
ЕСТЬNULL(ВремОплаты.СуммаОплат, 0) КАК СуммаОплат
ИЗ
ВремПродажи КАК ВремПродажи
ПОЛНОЕ СОЕДИНЕНИЕ ВремОплаты КАК ВремОплаты
ПО ВремПродажи.Контрагент = ВремОплаты.Контрагент
Пакет разделён точками с запятой. В каждой из первых двух частей сумма уже свёрнута по контрагенту — значит, в третьей части на одного контрагента приходится ровно одна строка слева и одна справа, задвоения быть не может. Полное соединение нужно, чтобы не потерять контрагентов, у которых есть продажи, но нет оплат, и наоборот.
Чтобы временные таблицы жили, запросу назначают менеджер временных таблиц:
Запрос = Новый Запрос(ТекстЗапроса);
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Результат = Запрос.Выполнить();
Без этой строки ПОМЕСТИТЬ в многопакетном запросе отработает, но таблица не будет доступна за пределами одного вызова.
Запрос против суммирования в цикле
Классическая ошибка новичка — выбрать строки запросом без агрегата и складывать их в переменную обходом выборки:
// так делать не надо
Выборка = Запрос.Выполнить().Выбрать();
Итого = 0;
Пока Выборка.Следующий() Цикл
Итого = Итого + Выборка.Сумма;
КонецЦикла;
Это работает, но платформа тянет из базы все строки, а сложение делает медленный встроенный язык. На тысячах записей разница заметна глазами. Сервер СУБД суммирует на порядки быстрее, поэтому расчёт переносят в запрос:
// так правильно
ВЫБРАТЬ
СУММА(Таблица.Сумма) КАК Итого
ИЗ
...
Правило простое: если итог можно получить агрегатной функцией, его считает запрос, а не цикл. Обход выборки нужен тогда, когда с каждой строкой надо что-то делать по отдельности (формировать движения, печатать), а не просто накапливать число.
Отдельно про сумму по колонке таблицы значений — там тоже не нужен цикл, есть метод Итог():
ОбщаяСумма = ТаблицаЗначений.Итог("Сумма");
Ещё одна родственная задача — отобрать несколько строк с наибольшими суммами, например топ-5 контрагентов по обороту. Сортировку задаёт УПОРЯДОЧИТЬ ПО по уже посчитанной сумме, а количество строк ограничивает ПЕРВЫЕ:
ВЫБРАТЬ ПЕРВЫЕ 5
Продажи.Контрагент КАК Контрагент,
СУММА(Продажи.Сумма) КАК Сумма
ИЗ
РегистрНакопления.Продажи КАК Продажи
СГРУППИРОВАТЬ ПО
Продажи.Контрагент
УПОРЯДОЧИТЬ ПО
Сумма УБЫВ
ПЕРВЫЕ применяется уже после группировки и сортировки, поэтому в выборку попадут именно пять строк с максимальными итогами, а не первые пять записей таблицы.
Оптимизация и типичные ошибки
Несколько вещей, из-за которых суммы либо считаются неверно, либо медленно.
- 🧮 Виртуальные таблицы вместо ручной свёртки. Для оборотов и остатков у регистров есть готовые виртуальные таблицы (
.Обороты,.Остатки,.ОстаткиИОбороты). Они уже возвращают просуммированные ресурсы на уровне СУБД. Суммировать записи основной таблицы движений вручную — медленнее и легко ошибиться с периодом. - 🚫 Функции на индексируемых полях в условии. Если обернуть поле отбора в функцию (
ГОД(Док.Дата) = 2026), индекс не сработает. Период задают границами:Док.Дата >= &НачалоГода И Док.Дата < &НачалоСледующегоГода. - 0️⃣ Потерянный
NULLв ресурсах. Виртуальная таблица остатков по позициям, где движений не было, отдаётNULL. БезЕСТЬNULLдальнейшая арифметика обнуляет не поле, а всю строку результата. - ⚖️ Тип результата суммы.
СУММА()по полю типа «Число» вернёт число с той же разрядностью. Если складываете денежные значения, следите, чтобы у исходного реквизита хватало знаков после запятой, — округление происходит на уровне типа поля, а не функции. - 🔗 Задвоение при соединении до группировки. Соединили два регистра, у одного контрагента две строки — и
СУММА()сложит каждую сумму дважды. Лечится свёрткой во временных таблицах до соединения (см. раздел выше).
Прежде чем складывать значения из соединённых таблиц, прикиньте кратность связи. Если справа на одну строку слева приходится несколько — сумма слева умножится на их число. Это самая коварная ошибка: запрос не падает, он молча возвращает завышенный итог.
Частые вопросы
Чем `СУММА` отличается от `КОЛИЧЕСТВО`?
СУММА складывает значения поля, КОЛИЧЕСТВО считает строки. КОЛИЧЕСТВО(*) — все строки группы, КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Поле) — число уникальных значений. Для подсчёта «сколько документов» берут КОЛИЧЕСТВО, для «на какую сумму» — СУММА.Почему итоговая сумма получилась больше, чем в реальности?
Как в одном запросе получить и сумму, и её долю в общем итоге?
Сумма * 100 / ОбщийИтог. Делить напрямую одну агрегатную функцию на другую в одной группировке нельзя — у них разный уровень детализации.Может ли `СУММА` вернуть `NULL`?
NULL). Чтобы гарантированно получить число, оберните результат: ЕСТЬNULL(СУММА(Поле), 0).Когда брать `ИТОГИ ПО`, а когда `СГРУППИРОВАТЬ ПО`?
СГРУППИРОВАТЬ ПО даёт плоскую таблицу итогов по разрезу. ИТОГИ ПО добавляет к детальным строкам итоги по уровням иерархии — это для отчётов и обхода по группировкам. Если детализация не нужна, а нужны только свёрнутые суммы — СГРУППИРОВАТЬ ПО.