Как обработать результат запроса: выборка, таблица, группировки
Содержание
Запрос вернул данные — а дальше их надо прочитать, сложить, сгруппировать и отдать в отчёт или в документ. Метод Выполнить() отдаёт объект РезультатЗапроса, и от того, как вы его разбираете, зависит и скорость кода, и то, не упадёт ли обработка на пустой базе.
Разберём на рабочих примерах: как перебрать строки выборкой, как выгрузить всё в таблицу значений, как обойти результат по группировкам с итогами, как считать суммы и количества, как не словить исключение и как не положить сервер при миллионе строк.
С чего начинается разбор: объект РезультатЗапроса
Запрос.Выполнить() возвращает РезультатЗапроса. Сам по себе он данные наружу не отдаёт — из него надо получить либо выборку (построчный обход), либо таблицу значений (всё сразу в память).
Минимальный каркас всегда один:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Товары.Номенклатура КАК Номенклатура,
| Товары.Количество КАК Количество
|ИЗ
| Документ.РеализацияТоваровУслуг.Товары КАК Товары";
Результат = Запрос.Выполнить();
Задавайте псевдонимы полям через КАК. По псевдониму вы потом обращаетесь к полю выборки: Выборка.Номенклатура. Без псевдонима имя поля платформа сформирует сама, и обращение к нему превратится в угадайку.
У РезультатЗапроса есть два ключевых метода: Выбрать() — получить выборку для обхода, и Выгрузить() — получить готовую коллекцию целиком. Какой выбрать — зависит от того, что вы делаете с данными.
Если в тексте запроса есть параметры (&ДатаНачала, &Организация), их значения задают через УстановитьПараметр() до вызова Выполнить():
Запрос.УстановитьПараметр("ДатаНачала", НачалоДня(Дата));
Запрос.УстановитьПараметр("Организация", ВыбраннаяОрганизация);
Незаданный параметр не всегда роняет запрос — иногда он молча воспринимается как 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.Нужно ли проверять `Пустой()`, если дальше идёт цикл `Пока Выборка.Следующий()`?
Можно ли изменить данные прямо в выборке?
Выборка.Ссылка.ПолучитьОбъект()), меняйте и записывайте его — в транзакции и с контролем блокировок.Почему суммы в результате больше, чем в документах?
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ …) там, где считаете уникальные объекты.