Второй цикл по выборке запроса пустой: как перечитать данные без нового запроса
Содержание
Чтобы обойти выборку ещё раз, вызовите у неё Выборка.Сбросить() перед вторым циклом — позиция вернётся перед первой записью, и Следующий() снова начнёт с начала. Второй вариант — заново вызвать РезультатЗапроса.Выбрать(): результат запроса хранится в памяти, повторно к базе вы не обращаетесь.
Сам запрос выполнять второй раз не нужно. Это частая причина медленного кода: разработчик видит, что второй цикл ничего не обходит, и копирует Запрос.Выполнить() перед ним. Повторный запрос нужен только тогда, когда данные в базе могли измениться между проходами.
Как устроена выборка и почему второй цикл ничего не делает
РезультатЗапроса — это набор строк, который платформа уже получила от СУБД. ВыборкаИзРезультатаЗапроса — лёгкий курсор поверх этого набора. Он не хранит данные, он хранит только текущую позицию.
Что происходит в типичном цикле:
- После
Выбрать()курсор стоит перед первой записью. - Каждый вызов
Следующий()сдвигает его на одну запись вперёд и возвращаетИстина. - Когда записи кончились,
Следующий()возвращаетЛожь, и циклПоказавершается. - Курсор остаётся за последней записью. Назад он сам не едет.
Поэтому второй Пока Выборка.Следующий() Цикл сразу получает Ложь и не выполняется ни разу. Ошибки платформа не выдаёт: с её точки зрения всё корректно, записи просто закончились. Именно это и сбивает с толку — код «молча» не работает
.
Курсор движется только вперёд. Перейти к предыдущей записи или к записи с номером N метода нет. Нужен произвольный доступ — выгружайте результат в таблицу значений.
Какие методы выборки влияют на позицию
| Метод | Что делает с позицией |
|---|---|
Следующий() | Сдвигает на одну запись вперёд |
Сбросить() | Возвращает перед первой записью |
НайтиСледующий(...) | Ищет вперёд от текущей позиции, на найденной записи останавливается |
СледующийПоЗначениюПоля(Поле) | Переходит к следующей записи с другим значением поля |
Количество() | Позицию не меняет, возвращает число записей |
Выбрать() у выборки | Создаёт подчинённую выборку, позиция родителя не меняется |
Где на практике возникает повторный обход
Ситуация «нужно пройти те же строки ещё раз» встречается чаще, чем кажется. Вот типичные сценарии:
- 🔁 Два прохода по одним данным. Сначала посчитать итог или максимум, потом во втором проходе вычислить долю каждой строки.
- 🔍 Поиск после обхода. Цикл уже прошёл выборку до конца, а потом вызывается
НайтиСледующий()— и ничего не находит, потому что искать дальше конца некуда. - 📦 Выборка передаётся в процедуру. Одна процедура обходит выборку для проверки, вторая — для записи. Вторая получает курсор в конце.
- ⏹ Прерывание цикла. Цикл вышел по
Прерватьна середине, дальше код рассчитывает на обход «с начала», а продолжает с места остановки. - 🗂 Отчёт с группировками. Внешний цикл по группам, внутренний по детальным записям — и внутренний цикл пишут по той же выборке, а не по подчинённой.
Во всех случаях причина одна: курсор не там, где его ждёт автор кода.
Три способа обойти выборку повторно
Способ 1. Метод Сбросить()
Самый короткий путь, когда переменная выборки у вас под рукой.
Результат = Запрос.Выполнить();
Выборка = Результат.Выбрать();
Сумма = 0;
Пока Выборка.Следующий() Цикл
Сумма = Сумма + Выборка.Сумма;
КонецЦикла;
Выборка.Сбросить();
Пока Выборка.Следующий() Цикл
Доля = ?(Сумма = 0, 0, Выборка.Сумма / Сумма * 100);
Сообщить(Строка(Выборка.Номенклатура) + ": " + Формат(Доля, "ЧДЦ=2") + " %");
КонецЦикла;
Проверка деления на ноль в примере нужна: если все суммы нулевые, без неё второй цикл упадёт.
Способ 2. Новая выборка из того же результата
РезультатЗапроса.Выбрать() можно вызывать сколько угодно раз. Каждый вызов даёт независимый курсор. Удобно, когда два прохода живут в разных процедурах: передавайте туда результат, а не выборку.
Процедура ПроверитьДанные(Результат)
Выборка = Результат.Выбрать();
Пока Выборка.Следующий() Цикл
// проверки
КонецЦикла;
КонецПроцедуры
Процедура ЗаписатьДанные(Результат)
Выборка = Результат.Выбрать();
Пока Выборка.Следующий() Цикл
// запись
КонецЦикла;
КонецПроцедуры
Так процедуры не зависят друг от друга: порядок вызова можно менять, и ни одна не получит «пустую» выборку.
Способ 3. Выгрузка в таблицу значений
Если данные нужно обходить многократно, сортировать, искать по ключу или обращаться по индексу — выгрузите результат:
Таблица = Результат.Выгрузить();
Таблица.Индексы.Добавить("Номенклатура");
Строки = Таблица.НайтиСтроки(Новый Структура("Номенклатура", Ссылка));
Таблица значений занимает больше памяти, чем выборка, зато даёт произвольный доступ, Итог(), Свернуть(), НайтиСтроки().
Какой способ выбрать
| Способ | Когда подходит | Минусы |
|---|---|---|
Сбросить() | Два прохода подряд в одной процедуре | Нужна та же переменная выборки |
Новый Выбрать() | Проходы в разных процедурах | Надо передавать результат, а не выборку |
Выгрузить() | Поиск, сортировка, доступ по индексу | Больше памяти на больших объёмах |
Повторный Запрос.Выполнить() | Только если данные в базе могли измениться между проходами | Лишнее обращение к СУБД |
Как найти место, где выборка «опустела»
Когда второй цикл не работает в чужом коде, источник не всегда очевиден: выборку могли пройти тремя вызовами выше.
Отладчик
- Поставьте точку останова перед циклом, который не выполняется (
F9в конфигураторе). - Запустите отладку и дождитесь остановки.
- Откройте вычисление выражения (
Shift+F9) и проверьтеВыборка.Количество(). Если там больше нуля, данные есть, проблема в позиции курсора. - Пройдите выше по стеку вызовов и найдите, кто последним вызывал
Следующий()для этой переменной.
Не вызывайте Выборка.Следующий() в окне вычисления выражений «для проверки». Курсор сдвинется по-настоящему, и дальше код отработает уже на смещённой выборке. Смотрите Количество() — оно позицию не трогает.
Глобальный поиск
Найдите в модуле все упоминания переменной выборки (Ctrl+F по имени). Ищите места, где выборка уходит параметром в другую процедуру или функцию: это самый частый источник «съеденного» курсора.
Замер производительности
Если вы подозреваете, что проблему «лечили» повторным выполнением запроса, включите замер производительности в меню отладки конфигуратора и посмотрите, сколько раз выполняется строка Запрос.Выполнить(). Два выполнения одного запроса в одной процедуре — повод заменить второй на Сбросить().
Выборки с группировками: обход по уровням без повторов
В запросах с ИТОГИ ПО повторный обход часто пытаются сделать «вручную»: идут по общей выборке, запоминают группу, потом ищут её строки. Так не нужно. Платформа сама строит дерево подчинённых выборок.
Выборка = Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока Выборка.Следующий() Цикл
// итог по группе, например по контрагенту
Детали = Выборка.Выбрать();
Пока Детали.Следующий() Цикл
// записи внутри группы
КонецЦикла;
КонецЦикла;
Здесь каждая подчинённая выборка Детали — свой курсор. Внешний цикл не страдает от того, что внутренний дошёл до конца.
Что ещё полезно знать:
- 🧭
Выборка.ТипЗаписи()возвращает значениеТипЗаписиЗапроса: детальная запись, итог по группировке, итог по иерархии или общий итог. По нему удобно отделять строки итогов при прямом обходе. - 🌳
ОбходРезультатаЗапроса.ПоГруппировкамСИерархиейнужен, когда группировка идёт по иерархическому справочнику и нужны промежуточные итоги по папкам. - 🎯 В
Выбрать()вторым параметром можно передать имена группировок, чтобы получить выборку только по нужной. - 🔢
СледующийПоЗначениюПоля("Поле")помогает при прямом обходе отсортированного результата перескакивать от группы к группе, не перебирая каждую строку.
Если итоговые строки вам не нужны, а нужно просто разбить данные на группы, проще отсортировать запрос по полю группы и обходить прямо, отслеживая смену значения.
Повторный обход в управляемых формах и веб-клиенте
Выборка и результат запроса существуют только на сервере. На клиент — тонкий или веб — их передать нельзя: ни возвратом функции, ни параметром. Поэтому вопрос «как обойти выборку ещё раз на клиенте» решается не методами выборки, а подготовкой данных на сервере.
Варианты по задаче:
- Вывести данные в форму. Выгрузите результат в таблицу значений и поместите её в реквизит формы через
ЗначениеВРеквизитФормы(). На клиенте с реквизитом можно работать сколько угодно раз. - Вернуть данные для клиентской логики. Соберите массив структур на сервере и верните его. Массив и структура с примитивными значениями и ссылками передаются на клиент.
- Сохранить между серверными вызовами. Поместите таблицу значений во временное хранилище (
ПоместитьВоВременноеХранилище()) и передавайте на клиент только адрес. Следующий серверный вызов достанет данные черезПолучитьИзВременногоХранилища()без повторного запроса.
&НаСервере
Функция ДанныеДляКлиента()
Выборка = Запрос.Выполнить().Выбрать();
Массив = Новый Массив;
Пока Выборка.Следующий() Цикл
Массив.Добавить(Новый Структура("Ссылка, Сумма", Выборка.Ссылка, Выборка.Сумма));
КонецЦикла;
Возврат Массив;
КонецФункции
Не открывайте серверный вызов на каждую строку из клиентского цикла. В веб-клиенте каждый такой вызов — отдельный HTTP-запрос, и сто строк превращаются в сто сетевых обращений. Соберите всё за один вызов.
Если форма должна показывать актуальные данные после записи, повторный запрос как раз оправдан: данные в базе изменились, старый результат устарел.
Как писать код, чтобы повторный обход не понадобился
Лучший второй проход — тот, который не нужен. Несколько приёмов:
- 🧮 Считайте итоги в запросе. Сумму, количество, долю строки можно получить в самом запросе: вложенным запросом или временной таблицей с итогом. Тогда хватит одного цикла.
- 📑 Пакетный запрос вместо двух. Если нужны и детальные данные, и сводка, соберите их одним пакетом:
Запрос.ВыполнитьПакет()вернёт массив результатов, каждый обходится своей выборкой. - 🧱 Менеджер временных таблиц. Промежуточный набор кладите во временную таблицу через
МенеджерВременныхТаблици используйте в нескольких запросах. Это лучше, чем выгружать данные в память и снова передавать параметром. - 🚫 Никаких запросов в цикле. Классическая ошибка — внутри обхода выборки выполнять ещё один запрос по каждой строке. Соедините данные в одном запросе или выберите всё сразу и ищите в таблице значений.
- 📏 Проверяйте пустоту без обхода.
Результат.Пустой()отвечает, есть ли строки, не трогая курсор. Не нужно делатьСледующий()ради проверки, а потом думать, как вернуться назад.
Пример: доля каждой строки без второго прохода, всё в запросе.
ВЫБРАТЬ
Продажи.Номенклатура КАК Номенклатура,
СУММА(Продажи.Сумма) КАК Сумма
ПОМЕСТИТЬ ВТ_Продажи
ИЗ
РегистрНакопления.Продажи КАК Продажи
ГДЕ
Продажи.Период МЕЖДУ &Начало И &Конец
СГРУППИРОВАТЬ ПО
Продажи.Номенклатура
;
ВЫБРАТЬ
ВТ.Номенклатура КАК Номенклатура,
ВТ.Сумма КАК Сумма,
ВЫБОР
КОГДА Итог.Всего = 0 ТОГДА 0
ИНАЧЕ ВТ.Сумма / Итог.Всего * 100
КОНЕЦ КАК Доля
ИЗ
ВТ_Продажи КАК ВТ,
(ВЫБРАТЬ СУММА(Т.Сумма) КАК Всего ИЗ ВТ_Продажи КАК Т) КАК Итог
Имя регистра и его поля в примере условные — подставьте свои. Важна схема: временная таблица, итог отдельным подзапросом и соединение без условия, потому что итоговая строка одна.
Ошибки, которые встречаются в коде с повторным обходом
| Что делают | Чем плохо | Как правильно |
|---|---|---|
Повторяют Запрос.Выполнить() ради второго цикла | Лишний запрос к СУБД, при большом объёме заметно медленнее | Сбросить() или новый Выбрать() |
Вызывают Следующий() для проверки, есть ли данные | Первая запись «съедается», цикл начинается со второй | Результат.Пустой() |
| Выгружают в таблицу значений «на всякий случай» | Лишняя память на миллионах строк | Выгружать только при поиске и произвольном доступе |
| Передают выборку в несколько процедур | Каждая следующая получает курсор не с начала | Передавать РезультатЗапроса |
Ищут НайтиСледующий() в цикле по каждой строке другой выборки | Квадратичное время: каждый поиск — заново с начала | Соединить данные в запросе или выгрузить и индексировать таблицу |
Последний пункт стоит разобрать отдельно. Схема «для каждой строки А сбросить выборку Б и найти в ней значение» работает, но на 10 000 строк в каждой выборке даёт до 100 миллионов сравнений. Соединение в запросе или индекс таблицы значений решают ту же задачу на порядки быстрее.