Как обработать результат запроса: выборка, таблица, группировки

· 9 мин

Содержание

Запрос вернул данные — а дальше их надо прочитать, сложить, сгруппировать и отдать в отчёт или в документ. Метод Выполнить() отдаёт объект РезультатЗапроса, и от того, как вы его разбираете, зависит и скорость кода, и то, не упадёт ли обработка на пустой базе.

Разберём на рабочих примерах: как перебрать строки выборкой, как выгрузить всё в таблицу значений, как обойти результат по группировкам с итогами, как считать суммы и количества, как не словить исключение и как не положить сервер при миллионе строк.

С чего начинается разбор: объект РезультатЗапроса

Запрос.Выполнить() возвращает РезультатЗапроса. Сам по себе он данные наружу не отдаёт — из него надо получить либо выборку (построчный обход), либо таблицу значений (всё сразу в память).

Минимальный каркас всегда один:

Запрос = Новый Запрос;
Запрос.Текст =
    "ВЫБРАТЬ
    |   Товары.Номенклатура КАК Номенклатура,
    |   Товары.Количество КАК Количество
    |ИЗ
    |   Документ.РеализацияТоваровУслуг.Товары КАК Товары";

Результат = Запрос.Выполнить();

Задавайте псевдонимы полям через КАК. По псевдониму вы потом обращаетесь к полю выборки: Выборка.Номенклатура. Без псевдонима имя поля платформа сформирует сама, и обращение к нему превратится в угадайку.

У РезультатЗапроса есть два ключевых метода: Выбрать() — получить выборку для обхода, и Выгрузить() — получить готовую коллекцию целиком. Какой выбрать — зависит от того, что вы делаете с данными.

Если в тексте запроса есть параметры (&ДатаНачала, &Организация), их значения задают через УстановитьПараметр() до вызова Выполнить():

Запрос.УстановитьПараметр("ДатаНачала", НачалоДня(Дата));
Запрос.УстановитьПараметр("Организация", ВыбраннаяОрганизация);

Незаданный параметр не всегда роняет запрос — иногда он молча воспринимается как NULL, и результат приходит пустым или неверным. Поэтому все параметры из текста запроса лучше проставлять явно, даже если значение — пустая ссылка.

Построчный обход: Выбрать и Следующий

Самый частый сценарий — пройти строки по одной. Для этого берём выборку и крутим цикл с Следующий():

Выборка = Результат.Выбрать();
Пока Выборка.Следующий() Цикл
    Сообщить(СтрШаблон("%1 — %2 шт.",
        Выборка.Номенклатура,
        Формат(Выборка.Количество, "ЧДЦ=3")));
КонецЦикла;

Следующий() сдвигает курсор на строку вперёд и возвращает Истина, пока строки есть, и Ложь, когда они кончились. Поля читаются по псевдониму: Выборка.Номенклатура.

Не склеивайте число со строкой напрямую — через Формат() или СтрШаблон(). Иначе разделители разрядов и дробная часть выведутся так, как вам не нужно, а на некоторых типах вы получите ошибку преобразования.

Пара моментов про выборку:

  • 📍 Повторно пройти ту же выборку нельзя — курсор одноразовый. Нужен второй проход — вызовите Выбрать() ещё раз или сразу выгружайте в таблицу.
  • 📍 Выборка.Количество() отдаёт число строк, не сдвигая курсор. Удобно, чтобы заранее прикинуть объём.
  • 📍 Выборка «ленивая» в пределах уже посчитанного результата — данные для неё платформа держит, а вот весь миллион строк в прикладные переменные вы не поднимаете, пока не переложите их сами.

Когда удобнее таблица значений

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

ТаблицаТоваров = Результат.Выгрузить();

Выгрузить() без параметров отдаёт ТаблицаЗначений с теми же колонками, что и поля запроса. Дальше работают все методы таблицы:

Что нужноМетод таблицы значений
Найти строки по условиюНайтиСтроки(Новый Структура("Номенклатура", Товар))
Свернуть и просуммироватьСвернуть("Номенклатура", "Количество")
ОтсортироватьСортировать("Количество Убыв")
Посчитать общий итогИтог("Количество")

Короткий рабочий пример — свернуть продажи по номенклатуре и найти позиции одного контрагента:

ТаблицаПродаж = Результат.Выгрузить();

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

// Все строки по нужному товару
НайденныеСтроки = ТаблицаПродаж.НайтиСтроки(
    Новый Структура("Номенклатура", ИскомыйТовар));

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

Разница с выборкой простая: выборка — поток, читаем один раз и экономим память; таблица значений — снимок в памяти, с которым можно делать что угодно и сколько угодно раз. Для отчёта на несколько тысяч строк таблица удобнее, для фоновой перемолотки миллионов записей — выборка с порционным обходом (о нём ниже).

Таблицу значений нельзя передать с сервера на клиент напрямую в управляемом приложении, если в колонках лежат непримитивные значения вне контекста формы. Для передачи на клиент используйте реквизит формы с типом «Таблица значений» или сериализуйте через ЗначениеВСтрокуВнутр()/адрес во временном хранилище.

Группировки и агрегатные функции

Чаще всего результат нужно не просто перечислить, а свернуть: сумма по контрагенту, количество документов, средний чек. Есть два уровня — свёртка в самом запросе и обход результата по группировкам.

Свёртка в тексте запроса. Агрегаты считает СУБД, это быстро:

"ВЫБРАТЬ
|   Продажи.Контрагент КАК Контрагент,
|   СУММА(Продажи.Сумма) КАК СуммаПродаж,
|   КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Продажи.Регистратор) КАК ЧислоДокументов
|ИЗ
|   РегистрНакопления.Продажи КАК Продажи
|СГРУППИРОВАТЬ ПО
|   Продажи.Контрагент"

Доступные агрегаты: СУММА, КОЛИЧЕСТВО, КОЛИЧЕСТВО(РАЗЛИЧНЫЕ …), МАКСИМУМ, МИНИМУМ, СРЕДНЕЕ. Все неагрегированные поля обязаны попасть в СГРУППИРОВАТЬ ПО, иначе запрос не соберётся.

Обход по группировкам с итогами. Когда нужны и свёрнутые итоги, и детализация под ними, добавляют секцию ИТОГИ … ПО и обходят результат в режиме ПоГруппировкам:

Запрос.Текст =
    "ВЫБРАТЬ
    |   Продажи.Контрагент КАК Контрагент,
    |   Продажи.Регистратор КАК Регистратор,
    |   Продажи.Сумма КАК Сумма
    |ИЗ
    |   РегистрНакопления.Продажи КАК Продажи
    |ИТОГИ
    |   СУММА(Сумма)
    |ПО
    |   Контрагент";

ВыборкаКонтр = Запрос.Выполнить().Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаКонтр.Следующий() Цикл
    Сообщить(СтрШаблон("%1: итого %2",
        ВыборкаКонтр.Контрагент,
        Формат(ВыборкаКонтр.Сумма, "ЧДЦ=2")));

    ВыборкаДок = ВыборкаКонтр.Выбрать();
    Пока ВыборкаДок.Следующий() Цикл
        Сообщить(СтрШаблон("    %1 — %2",
            ВыборкаДок.Регистратор,
            Формат(ВыборкаДок.Сумма, "ЧДЦ=2")));
    КонецЦикла;
КонецЦикла;

Здесь верхний цикл идёт по контрагентам (в поле Сумма лежит уже итог по группе), а вложенный ВыборкаКонтр.Выбрать() отдаёт строки-детализацию внутри текущей группы.

Режим ОбходРезультатаЗапроса.ПоГруппировкам работает только если в запросе есть секция ИТОГИ … ПО. Поставите режим без ИТОГИ — обход пойдёт как обычный прямой, вложенная выборка будет пустой, а ошибки не будет. Это ловит многих.

Для иерархических справочников есть ПоГруппировкамСИерархией — он разворачивает ещё и вложенность групп самого справочника.

Если результат с итогами нужен не для обхода, а целиком — как готовая структура для отчёта или дерева на форме, — вместо Выбрать() вызывайте Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам). Он вернёт ДеревоЗначений: верхние строки — группировки с итогами, подчинённые — детализация. Такое дерево напрямую связывается с реквизитом формы типа «Дерево значений» и показывается пользователю без ручной сборки.

Пара практических правил по агрегатам:

  • 🧮 КОЛИЧЕСТВО(*) считает все строки группы, КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Поле) — только уникальные значения поля. Для «сколько документов у контрагента» нужен второй вариант.
  • 🧮 СУММА по полю, где есть NULL (после левого соединения), пропускает NULL, а не обнуляет строку. Но лучше заранее обернуть поле в ЕСТЬNULL(Поле, 0), чтобы результат был предсказуем.
  • 🧮 Нельзя группировать по полю неограниченной длины (строка без указанной длины, хранилище значения) — такие поля в СГРУППИРОВАТЬ ПО и ИТОГИ ПО не ставят.

Проверка на пустой результат и обработка ошибок

Две причины, по которым обработка результата падает или ведёт себя странно: результат пустой, а код этого не ждёт, и сам Выполнить() бросает исключение.

Пустой результат. Проверяйте Пустой() до разбора — так вы не делаете лишних действий и не ломаетесь на обращении к полям:

Результат = Запрос.Выполнить();
Если Результат.Пустой() Тогда
    Возврат;
КонецЕсли;

Пустой() возвращает Истина, если в результате нет ни одной строки. Для простого цикла это не обязательно — Следующий() сразу вернёт Ложь. Но если дальше по коду вы, например, берёте первую строку или строите таблицу с шапкой, проверка экономит нервы и защищает от обращения к несуществующим данным.

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

Попытка
    Результат = Запрос.Выполнить();
Исключение
    ЗаписьЖурналаРегистрации("Продажи.Отчёт",
        УровеньЖурналаРегистрации.Ошибка, , ,
        ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
    Возврат;
КонецПопытки;

ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()) — текущий способ получить полный текст ошибки со стеком. В журнал регистрации пишите именно его, а пользователю показывайте короткое понятное сообщение, а не технический дамп.

Как ускорить обработку больших выборок

Если результат — сотни тысяч строк, проблема обычно не в обходе, а в том, что вся выборка материализуется в памяти рабочего процесса. В языке запросов 1С нет LIMIT/OFFSET, зато есть ВЫБРАТЬ ПЕРВЫЕ N и приём с «последней обработанной ссылкой».

Обрабатываем порциями, упорядочивая по ссылке и двигая границу:

ПоследняяСсылка = Справочники.Номенклатура.ПустаяСсылка();

Пока Истина Цикл
    Запрос.Текст =
        "ВЫБРАТЬ ПЕРВЫЕ 1000
        |   Ном.Ссылка КАК Ссылка
        |ИЗ
        |   Справочник.Номенклатура КАК Ном
        |ГДЕ
        |   Ном.Ссылка > &ПоследняяСсылка
        |УПОРЯДОЧИТЬ ПО
        |   Ссылка";
    Запрос.УстановитьПараметр("ПоследняяСсылка", ПоследняяСсылка);

    Выборка = Запрос.Выполнить().Выбрать();
    Если Выборка.Количество() = 0 Тогда
        Прервать;
    КонецЕсли;

    Пока Выборка.Следующий() Цикл
        // обработка одной строки
        ПоследняяСсылка = Выборка.Ссылка;
    КонецЦикла;
КонецЦикла;

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

  • 🚀 Никаких запросов в цикле. Если внутри обхода вы на каждую строку выполняете ещё один запрос — вынесите эти данные в основной запрос соединением или во временную таблицу через МенеджерВременныхТаблиц.
  • 🚀 Считайте агрегаты в запросе, а не в коде. СУММА()/СГРУППИРОВАТЬ ПО на стороне СУБД быстрее ручного суммирования в цикле на порядок.
  • 🚀 Выбирайте только нужные поля. Лишние колонки — это лишний трафик и память; ВЫБРАТЬ * в прикладном коде не используют.
  • 🚀 Фильтруйте в ГДЕ, а не отбрасывайте строки в обходе. Пусть лишние записи даже не доедут до выборки.

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

Типичные ошибки при разборе результата

Большинство проблем с результатом запроса — не падения, а тихо неверные данные. Вот что встречается чаще всего и как это распознать:

Диагностика: выберите симптом

Отдельно про логику: если в запросе есть ЛЕВОЕ СОЕДИНЕНИЕ, непришедшие поля будут NULL, а не пустой ссылкой. Проверяйте их через ЕСТЬNULL(Поле, 0) прямо в запросе — так в выборку уже придёт нормальное значение, и в коде не придётся ловить NULL.

Частые вопросы

Чем `Выбрать()` отличается от `Выгрузить()`?
Выбрать() даёт поток для обхода по одной строке — память почти не расходуется, но пройти можно один раз. Выгрузить() поднимает весь результат в таблицу значений — её можно сортировать, искать, сворачивать и проходить многократно, но вся она лежит в памяти.
Как получить только первую строку результата?
Проще всего Выгрузить() и обратиться к Таблица[0], предварительно проверив, что таблица не пуста. Либо один раз вызвать Выборка.Следующий() и прочитать поля. Если по смыслу нужна именно одна запись — добавьте в запрос ВЫБРАТЬ ПЕРВЫЕ 1.
Нужно ли проверять `Пустой()`, если дальше идёт цикл `Пока Выборка.Следующий()`?
Для самого цикла не обязательно — на пустом результате он просто не выполнится ни разу. Проверка нужна, когда до цикла или после вы делаете что-то, что предполагает наличие данных (строите шапку отчёта, берёте первую строку).
Можно ли изменить данные прямо в выборке?
Нет. Выборка и выгруженная таблица — это копия данных на момент выполнения запроса. Чтобы изменить объекты базы, получите по ссылке из результата объект (Выборка.Ссылка.ПолучитьОбъект()), меняйте и записывайте его — в транзакции и с контролем блокировок.
Почему суммы в результате больше, чем в документах?
Почти всегда это размножение строк соединением: например, шапка документа соединена с табличной частью, и сумма шапки повторилась на каждую строку. Сворачивайте агрегатами и используйте КОЛИЧЕСТВО(РАЗЛИЧНЫЕ …) там, где считаете уникальные объекты.