Как подсчитать количество строк, которое вернул запрос по группировке?

Программист 1С v7.7 IT и автоматизация бизнеса
← На главную

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

В этой статье мы подробно разберем, как решить эту задачу оптимальными способами. Мы рассмотрим решения как для платформы 1С:Предприятие 7.7 (где эта проблема решается с помощью встроенных функций запросов или манипуляций с таблицами значений), так и для актуальной платформы 1С:Предприятие 8.x (где применяются итоги запроса и механизмы СКД).

Почему простой перебор строк в цикле — это плохой подход?

В самом начале обсуждения часто возникает соблазн решить задачу «в лоб» — объявить переменную-счетчик, запустить цикл обхода группировок и прибавлять по единице на каждой итерации. Давайте проанализируем, почему этот метод не подходит для реальных баз данных.

Представим, что у нас есть 20 складов и более 20 000 позиций номенклатуры. Нам нужно вывести в шапку каждого склада общее число товаров на нем. Если мы будем использовать вложенные циклы Пока Запрос.Группировка() = 1 Цикл для подсчета элементов «на лету», серверу 1С придется выполнить колоссальное количество итераций на стороне клиента. Это приведет к долгому формированию отчета и нерациональному использованию оперативной памяти. Наша цель — переложить задачу подсчета на СУБД или оптимизировать работу с коллекциями в памяти.

---

Решения для платформы 1С:Предприятие 7.7

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

Способ 1. Использование агрегатной функции Счетчик() в запросе

В языке запросов 1С 7.7 существует мощная встроенная функция Счетчик(). Она позволяет автоматически подсчитывать количество записей на уровне группировок еще на этапе выполнения запроса базой данных.

Рассмотрим пример настройки такого запроса:


// Описываем переменные запроса
|Период с ВыбНачПериода по ВыбКонПериода;
|Товар = Регистр.Остатки.Номенклатура;
|Склад = Регистр.Остатки.Склад;
|// Объявляем функцию-счетчик
|Функция КолТоваров = Счетчик(Товар);
|// Настраиваем группировки
|Группировка Склад;
|Группировка Товар без групп;

Как это работает при обходе результатов? Когда мы позиционируемся на верхней группировке «Склад», переменная запроса Запрос.КолТоваров уже содержит итоговое количество уникальных товаров по этому складу. Нам не нужно открывать подчиненный цикл по товарам только для того, чтобы узнать их количество!


Пока Запрос.Группировка("Склад") = 1 Цикл
    // Выводим секцию склада. Переменная Запрос.КолТоваров вернет точное число позиций!
    Таб.ВывестиСекцию("Склад"); 
    
    // Подчиненный цикл по товарам пишем только если нужно вывести детальные строки
    Пока Запрос.Группировка("Товар") = 1 Цикл
         // Вывод детальных строк товара при необходимости
    КонецЦикла;
КонецЦикла;

Способ 2. Выгрузка в Таблицу Значений (ТЗ) и метод Свернуть()

Если структура запроса очень сложная или данные собираются из разных источников, можно выгрузить результат запроса в Таблицу Значений и обработать ее в памяти. Для этого применяется классический прием с созданием «фиктивной» колонки.

Разберем этот алгоритм по шагам:

  1. Выгружаем результаты выполнения запроса в объект ТаблицаЗначений.
  2. Добавляем новую числовую колонку, которая будет играть роль счетчика.
  3. Заполняем эту колонку единицами во всех строках таблицы.
  4. Сворачиваем таблицу по группировочному полю (например, «Склад»), указывая суммирование по нашей новой колонке. В результате строки схлопнутся, а единицы сложатся, показав реальное число позиций по каждому складу.

Посмотрим на практическую реализацию этого метода в коде:


// Создаем объект Таблицы Значений
ТЗ = СоздатьОбъект("ТаблицаЗначений");

// Выгружаем запрос. Второй и третий параметры равны 0 — 
// это исключает выгрузку итоговых служебных строк запроса
Запрос.Выгрузить(ТЗ, 0, 0);

// Добавляем числовую колонку для будущего суммирования
ТЗ.НоваяКолонка("КолПоз", "Число");

// Заполняем добавленную колонку единицами
ТЗ.Заполнить(1, , , "КолПоз");

// Сворачиваем таблицу. Первым параметром указываем группировки, 
// вторым — колонку, значение которой нужно просуммировать
ТЗ.Свернуть("Склад", "КолПоз");

// Теперь ТЗ содержит уникальный список складов и количество позиций в колонке КолПоз
ТЗ.ВыбратьСтроки();
Пока ТЗ.ПолучитьСтроку() = 1 Цикл
    Сообщить("На складе " + ТЗ.Склад + " найдено позиций: " + ТЗ.КолПоз);
КонецЦикла;

---

Решения для платформы 1С:Предприятие 8.x

В платформе 1С 8.x архитектура построения запросов существенно изменилась. Здесь разработчики часто совершают типичную ошибку: пытаются использовать агрегатную функцию КОЛИЧЕСТВО() в основном списке выборки совместно со СГРУППИРОВАТЬ ПО.

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


// НЕПРАВИЛЬНЫЙ ВАРЯНТ (вызывает ошибки расчета в итогах)
ВЫБРАТЬ
    Остатки.Склад КАК Склад,
    Остатки.Номенклатура КАК Номенклатура,
    КОЛИЧЕСТВО(Остатки.Номенклатура) КАК КоличествоSKU // Вернет 1 на детальном уровне
ИЗ
    РегистрНакопления.ОстаткиНоменклатуры.Остатки КАК Остатки
СГРУППИРОВАТЬ ПО
    Остатки.Склад,
    Остатки.Номенклатура

Выясним причину этой ошибки: СУБД сначала группирует детальные записи (где связка Склад + Номенклатура всегда уникальна и дает ровно 1 запись), а затем считает количество по этой группировке. Чтобы решить эту проблему правильно, нужно использовать секцию ИТОГИ или Систему компоновки данных (СКД).

Способ 1. Использование секции ИТОГИ в запросе

Чтобы получить верное количество уникальных позиций номенклатуры (SKU) на уровне группировки склада, необходимо использовать расчет итогов с функцией КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ...).

Рассмотрим правильную структуру запроса:


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

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


ВыборкаСклад = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Склад");
Пока ВыборкаСклад.Следующий() Цикл
    // Выводим шапку группировки Склад. КоличествоSKU уже рассчитано!
    Сообщить("Склад: " + ВыборкаСклад.Склад + ", Всего уникальных товаров: " + ВыборкаСклад.КоличествоSKU);
    
    // Переходим к детальным записям внутри этого склада
    ВыборкаДетали = ВыборкаСклад.Выбрать();
    Пока ВыборкаДетали.Следующий() Цикл
         // Обработка конкретных товаров
    КонецЦикла;
КонецЦикла;

Способ 2. Настройка ресурсов в СКД (Система компоновки данных)

Если вы разрабатываете отчет на базе СКД, вам вообще не нужно писать сложные конструкции подсчета количества в самом тексте запроса. СКД спроектирована так, чтобы брать рутинные расчеты итогов на себя.

Выполним настройку по шагам:

  1. Напишите простой запрос, выбирающий Склад, Номенклатуру и КоличествоОстаток (без группировок и итогов в тексте запроса).
  2. Перейдите на вкладку «Ресурсы» в схеме компоновки данных.
  3. Перетащите поле Номенклатура в список ресурсов.
  4. В колонке «Выражение» напишите формулу расчета: Количество(Различные Номенклатура).
  5. В колонке «Рассчитывать по» укажите группировку Склад.

При выводе отчета СКД автоматически посчитает уникальные позиции номенклатуры и выведет красивый итог прямо в строку группировки склада. Пользователю не придется разворачивать дерево строк, чтобы увидеть общее количество позиций.

← На главную