Второй цикл по выборке запроса пустой: как перечитать данные без нового запроса

· 9 мин

Содержание

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

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

Как устроена выборка и почему второй цикл ничего не делает

РезультатЗапроса — это набор строк, который платформа уже получила от СУБД. ВыборкаИзРезультатаЗапроса — лёгкий курсор поверх этого набора. Он не хранит данные, он хранит только текущую позицию.

Что происходит в типичном цикле:

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

Поэтому второй Пока Выборка.Следующий() Цикл сразу получает Ложь и не выполняется ни разу. Ошибки платформа не выдаёт: с её точки зрения всё корректно, записи просто закончились. Именно это и сбивает с толку — код «молча» не работает .

Курсор движется только вперёд. Перейти к предыдущей записи или к записи с номером N метода нет. Нужен произвольный доступ — выгружайте результат в таблицу значений.

Какие методы выборки влияют на позицию

МетодЧто делает с позицией
Следующий()Сдвигает на одну запись вперёд
Сбросить()Возвращает перед первой записью
НайтиСледующий(...)Ищет вперёд от текущей позиции, на найденной записи останавливается
СледующийПоЗначениюПоля(Поле)Переходит к следующей записи с другим значением поля
Количество()Позицию не меняет, возвращает число записей
Выбрать() у выборкиСоздаёт подчинённую выборку, позиция родителя не меняется

Где на практике возникает повторный обход

Ситуация «нужно пройти те же строки ещё раз» встречается чаще, чем кажется. Вот типичные сценарии:

  • 🔁 Два прохода по одним данным. Сначала посчитать итог или максимум, потом во втором проходе вычислить долю каждой строки.
  • 🔍 Поиск после обхода. Цикл уже прошёл выборку до конца, а потом вызывается НайтиСледующий() — и ничего не находит, потому что искать дальше конца некуда.
  • 📦 Выборка передаётся в процедуру. Одна процедура обходит выборку для проверки, вторая — для записи. Вторая получает курсор в конце.
  • ⏹ Прерывание цикла. Цикл вышел по Прервать на середине, дальше код рассчитывает на обход «с начала», а продолжает с места остановки.
  • 🗂 Отчёт с группировками. Внешний цикл по группам, внутренний по детальным записям — и внутренний цикл пишут по той же выборке, а не по подчинённой.

Во всех случаях причина одна: курсор не там, где его ждёт автор кода.

Три способа обойти выборку повторно

Способ 1. Метод Сбросить()

Самый короткий путь, когда переменная выборки у вас под рукой.

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

Сумма = 0;
Пока Выборка.Следующий() Цикл
    Сумма = Сумма + Выборка.Сумма;
КонецЦикла;

Выборка.Сбросить();

Пока Выборка.Следующий() Цикл
    Доля = ?(Сумма = 0, 0, Выборка.Сумма / Сумма * 100);
    Сообщить(Строка(Выборка.Номенклатура) + ": " + Формат(Доля, "ЧДЦ=2") + " %");
КонецЦикла;

Проверка деления на ноль в примере нужна: если все суммы нулевые, без неё второй цикл упадёт.

Способ 2. Новая выборка из того же результата

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

Процедура ПроверитьДанные(Результат)
    Выборка = Результат.Выбрать();
    Пока Выборка.Следующий() Цикл
        // проверки
    КонецЦикла;
КонецПроцедуры

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

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

Способ 3. Выгрузка в таблицу значений

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

Таблица = Результат.Выгрузить();
Таблица.Индексы.Добавить("Номенклатура");

Строки = Таблица.НайтиСтроки(Новый Структура("Номенклатура", Ссылка));

Таблица значений занимает больше памяти, чем выборка, зато даёт произвольный доступ, Итог(), Свернуть(), НайтиСтроки().

Какой способ выбрать

СпособКогда подходитМинусы
Сбросить()Два прохода подряд в одной процедуреНужна та же переменная выборки
Новый Выбрать()Проходы в разных процедурахНадо передавать результат, а не выборку
Выгрузить()Поиск, сортировка, доступ по индексуБольше памяти на больших объёмах
Повторный Запрос.Выполнить()Только если данные в базе могли измениться между проходамиЛишнее обращение к СУБД

Как найти место, где выборка «опустела»

Когда второй цикл не работает в чужом коде, источник не всегда очевиден: выборку могли пройти тремя вызовами выше.

Отладчик

  1. Поставьте точку останова перед циклом, который не выполняется (F9 в конфигураторе).
  2. Запустите отладку и дождитесь остановки.
  3. Откройте вычисление выражения (Shift+F9) и проверьте Выборка.Количество(). Если там больше нуля, данные есть, проблема в позиции курсора.
  4. Пройдите выше по стеку вызовов и найдите, кто последним вызывал Следующий() для этой переменной.

Не вызывайте Выборка.Следующий() в окне вычисления выражений «для проверки». Курсор сдвинется по-настоящему, и дальше код отработает уже на смещённой выборке. Смотрите Количество() — оно позицию не трогает.

Глобальный поиск

Найдите в модуле все упоминания переменной выборки (Ctrl+F по имени). Ищите места, где выборка уходит параметром в другую процедуру или функцию: это самый частый источник «съеденного» курсора.

Замер производительности

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

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

Выборки с группировками: обход по уровням без повторов

В запросах с ИТОГИ ПО повторный обход часто пытаются сделать «вручную»: идут по общей выборке, запоминают группу, потом ищут её строки. Так не нужно. Платформа сама строит дерево подчинённых выборок.

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

Здесь каждая подчинённая выборка Детали — свой курсор. Внешний цикл не страдает от того, что внутренний дошёл до конца.

Что ещё полезно знать:

  • 🧭 Выборка.ТипЗаписи() возвращает значение ТипЗаписиЗапроса: детальная запись, итог по группировке, итог по иерархии или общий итог. По нему удобно отделять строки итогов при прямом обходе.
  • 🌳 ОбходРезультатаЗапроса.ПоГруппировкамСИерархией нужен, когда группировка идёт по иерархическому справочнику и нужны промежуточные итоги по папкам.
  • 🎯 В Выбрать() вторым параметром можно передать имена группировок, чтобы получить выборку только по нужной.
  • 🔢 СледующийПоЗначениюПоля("Поле") помогает при прямом обходе отсортированного результата перескакивать от группы к группе, не перебирая каждую строку.

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

Повторный обход в управляемых формах и веб-клиенте

Выборка и результат запроса существуют только на сервере. На клиент — тонкий или веб — их передать нельзя: ни возвратом функции, ни параметром. Поэтому вопрос «как обойти выборку ещё раз на клиенте» решается не методами выборки, а подготовкой данных на сервере.

Варианты по задаче:

  1. Вывести данные в форму. Выгрузите результат в таблицу значений и поместите её в реквизит формы через ЗначениеВРеквизитФормы(). На клиенте с реквизитом можно работать сколько угодно раз.
  2. Вернуть данные для клиентской логики. Соберите массив структур на сервере и верните его. Массив и структура с примитивными значениями и ссылками передаются на клиент.
  3. Сохранить между серверными вызовами. Поместите таблицу значений во временное хранилище (ПоместитьВоВременноеХранилище()) и передавайте на клиент только адрес. Следующий серверный вызов достанет данные через ПолучитьИзВременногоХранилища() без повторного запроса.
&НаСервере
Функция ДанныеДляКлиента()
    Выборка = Запрос.Выполнить().Выбрать();
    Массив = Новый Массив;
    Пока Выборка.Следующий() Цикл
        Массив.Добавить(Новый Структура("Ссылка, Сумма", Выборка.Ссылка, Выборка.Сумма));
    КонецЦикла;
    Возврат Массив;
КонецФункции

Не открывайте серверный вызов на каждую строку из клиентского цикла. В веб-клиенте каждый такой вызов — отдельный HTTP-запрос, и сто строк превращаются в сто сетевых обращений. Соберите всё за один вызов.

Если форма должна показывать актуальные данные после записи, повторный запрос как раз оправдан: данные в базе изменились, старый результат устарел.

Как писать код, чтобы повторный обход не понадобился

Лучший второй проход — тот, который не нужен. Несколько приёмов:

  • 🧮 Считайте итоги в запросе. Сумму, количество, долю строки можно получить в самом запросе: вложенным запросом или временной таблицей с итогом. Тогда хватит одного цикла.
  • 📑 Пакетный запрос вместо двух. Если нужны и детальные данные, и сводка, соберите их одним пакетом: Запрос.ВыполнитьПакет() вернёт массив результатов, каждый обходится своей выборкой.
  • 🧱 Менеджер временных таблиц. Промежуточный набор кладите во временную таблицу через МенеджерВременныхТаблиц и используйте в нескольких запросах. Это лучше, чем выгружать данные в память и снова передавать параметром.
  • 🚫 Никаких запросов в цикле. Классическая ошибка — внутри обхода выборки выполнять ещё один запрос по каждой строке. Соедините данные в одном запросе или выберите всё сразу и ищите в таблице значений.
  • 📏 Проверяйте пустоту без обхода. Результат.Пустой() отвечает, есть ли строки, не трогая курсор. Не нужно делать Следующий() ради проверки, а потом думать, как вернуться назад.

Пример: доля каждой строки без второго прохода, всё в запросе.

ВЫБРАТЬ
    Продажи.Номенклатура КАК Номенклатура,
    СУММА(Продажи.Сумма) КАК Сумма
ПОМЕСТИТЬ ВТ_Продажи
ИЗ
    РегистрНакопления.Продажи КАК Продажи
ГДЕ
    Продажи.Период МЕЖДУ &Начало И &Конец
СГРУППИРОВАТЬ ПО
    Продажи.Номенклатура
;
ВЫБРАТЬ
    ВТ.Номенклатура КАК Номенклатура,
    ВТ.Сумма КАК Сумма,
    ВЫБОР
        КОГДА Итог.Всего = 0 ТОГДА 0
        ИНАЧЕ ВТ.Сумма / Итог.Всего * 100
    КОНЕЦ КАК Доля
ИЗ
    ВТ_Продажи КАК ВТ,
    (ВЫБРАТЬ СУММА(Т.Сумма) КАК Всего ИЗ ВТ_Продажи КАК Т) КАК Итог

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

Ошибки, которые встречаются в коде с повторным обходом

Что делаютЧем плохоКак правильно
Повторяют Запрос.Выполнить() ради второго циклаЛишний запрос к СУБД, при большом объёме заметно медленнееСбросить() или новый Выбрать()
Вызывают Следующий() для проверки, есть ли данныеПервая запись «съедается», цикл начинается со второйРезультат.Пустой()
Выгружают в таблицу значений «на всякий случай»Лишняя память на миллионах строкВыгружать только при поиске и произвольном доступе
Передают выборку в несколько процедурКаждая следующая получает курсор не с началаПередавать РезультатЗапроса
Ищут НайтиСледующий() в цикле по каждой строке другой выборкиКвадратичное время: каждый поиск — заново с началаСоединить данные в запросе или выгрузить и индексировать таблицу

Последний пункт стоит разобрать отдельно. Схема «для каждой строки А сбросить выборку Б и найти в ней значение» работает, но на 10 000 строк в каждой выборке даёт до 100 миллионов сравнений. Соединение в запросе или индекс таблицы значений решают ту же задачу на порядки быстрее.

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

Сбросить() выполняет запрос заново?
Нет. Метод только возвращает курсор перед первой записью уже полученного результата. Изменения в базе после выполнения запроса он не покажет.
Чем отличается Сбросить() от нового Выбрать()?
Сбросить() перематывает существующий курсор. Новый Выбрать() создаёт второй независимый курсор — можно держать два обхода одновременно, например сравнивать соседние строки.
Можно ли вернуться на одну запись назад?
Нет, выборка движется только вперёд. Если нужна предыдущая строка, запоминайте её значения в переменных или выгрузите результат в таблицу значений.
Сколько раз можно вызывать Результат.Выбрать()?
Сколько нужно. Результат запроса остаётся в памяти, пока на него есть ссылка, и каждый вызов даёт новую выборку с начала.
Почему Количество() у выборки больше нуля, а цикл пустой?
Количество() считает все записи результата и не зависит от позиции. Цикл пустой, потому что курсор уже в конце: выборку прошли раньше.