Как заново пройти выборку запроса в 1С 8.3
Содержание
Выборка результата запроса в 1С — это курсор, который двигается только вперёд. Дошли циклом Пока Выборка.Следующий() Цикл до конца — и встроенного способа «отмотать назад» у самой выборки нет. Чтобы пройти её ещё раз с первой записи, курсор нужно вернуть в начало методом Сбросить() либо получить из результата запроса новую выборку.
Частая ошибка — думать, что второй цикл Пока Выборка.Следующий() подряд снова прочитает все записи. Он не прочитает ничего: курсор уже стоит на конце, и Следующий() сразу вернёт Ложь. Ниже — четыре рабочих способа начать обход сначала и когда какой уместен.
Метод Сбросить: возврат курсора к первой записи
Сбросить() — метод объекта ВыборкаИзРезультатаЗапроса. Он ставит курсор перед первой записью, не трогая сам результат запроса. После него ближайший Следующий() снова встанет на первую строку.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.Артикул КАК Артикул
|ИЗ
| Справочник.Номенклатура КАК Номенклатура";
Результат = Запрос.Выполнить();
Выборка = Результат.Выбрать();
// первый проход
Пока Выборка.Следующий() Цикл
Сообщить(Выборка.Артикул);
КонецЦикла;
Выборка.Сбросить(); // курсор снова в начале
// второй проход — опять с первой записи
Пока Выборка.Следующий() Цикл
Сообщить(Выборка.Ссылка);
КонецЦикла;
Ключевой момент: Сбросить() не перечитывает базу данных. Результат запроса уже лежит в памяти, метод лишь переставляет позицию курсора. Поэтому повторный проход после Сбросить() ничего не стоит по времени обращения к СУБД — все данные уже получены при Выполнить().
Если нужно просто узнать число записей перед обходом, вызовите Выборка.Количество(). Этот метод не сдвигает курсор, и Сбросить() после него не нужен.
Повторный Выбрать вместо «перемещения» курсора
Иногда задачу формулируют как «переместить выборку в начало» или «сдвинуть курсор на нужную запись». У линейной выборки произвольного перемещения нет: двигаться можно только вперёд методом Следующий(), а назад — только через полный сброс. Отдельного оператора, который переставит курсор на произвольную строку, в выборке результата запроса не существует.
Зато из одного результата запроса можно получить сколько угодно независимых выборок. Повторный вызов Результат.Выбрать() создаёт новую выборку, курсор которой стоит в начале:
Результат = Запрос.Выполнить();
Выборка1 = Результат.Выбрать();
Пока Выборка1.Следующий() Цикл
// первый обход
КонецЦикла;
Выборка2 = Результат.Выбрать(); // независимая выборка, снова с начала
Пока Выборка2.Следующий() Цикл
// второй обход
КонецЦикла;
Разница с Сбросить() простая. Сбросить() возвращает в начало ту же самую выборку — удобно, когда вы работаете с одной переменной. Повторный Выбрать() даёт отдельный объект — удобно, когда нужно вести два обхода параллельно или когда ссылку на старую выборку уже потеряли, а результат запроса сохранён.
У метода Выбрать() есть параметр типа обхода: ОбходРезультатаЗапроса.Прямой (линейный, по умолчанию) и ОбходРезультатаЗапроса.ПоГруппировкам (по итогам). Для простого повторного прохода берите прямой обход.
Обход через Для Каждого и почему в нём нет сброса
Выборку результата запроса нельзя обойти конструкцией Для Каждого … Из … Цикл — только Пока Выборка.Следующий() Цикл. Это первое ограничение, на которое натыкаются новички: попытка написать Для Каждого Строка Из Выборка Цикл приведёт к ошибке.
Для Каждого работает по коллекции, которую отдаёт метод Выгрузить() — по таблице значений:
Результат = Запрос.Выполнить();
Таблица = Результат.Выгрузить();
Для Каждого Строка Из Таблица Цикл
Сообщить(Строка.Артикул);
КонецЦикла;
Второе ограничение уже у самого Для Каждого: внутри него нет курсора, который можно «сбросить» в середине цикла. Чтобы пройти таблицу заново, вы просто запускаете новый цикл Для Каждого — счётчик начнётся с первой строки автоматически. Отдельного метода возврата здесь не требуется.
Нельзя удалять и добавлять строки коллекции прямо внутри Для Каждого по ней же — обход собьётся. Если нужно удалять по условию, соберите строки на удаление в отдельный массив, а потом удалите их обратным циклом Для по индексу.
Сравним три способа обхода:
| Способ | Можно пройти заново | Произвольный доступ | Когда брать |
|---|---|---|---|
Пока Следующий() + Сбросить() | да, через Сбросить() | нет | один-два последовательных прохода |
Повторный Выбрать() | да, новой выборкой | нет | параллельные или независимые обходы |
Выгрузить() + Для Каждого | да, новым циклом | да, по индексу Таблица[0] | много проходов, доступ по номеру строки |
Доступ к записи по индексу через таблицу значений
Если кроме повторного обхода нужно обращаться к конкретной записи по номеру или прыгать по строкам туда-сюда, курсор не подойдёт — выгружайте результат в таблицу значений. У неё есть индексация:
Таблица = Результат.Выгрузить();
ПерваяСтрока = Таблица[0];
ПоследняяСтрока = Таблица[Таблица.Количество() - 1];
// обход в обратном порядке
Для Сч = Таблица.Количество() - 1 По 0 Цикл
Строка = Таблица[Сч];
Сообщить(Строка.Артикул);
КонецЦикла;
Обратите внимание на обратный цикл: индекс идёт от Количество() - 1 до 0, граница вычисляется один раз до входа в цикл. Это аккуратный способ обойти коллекцию с конца, не завися от направления курсора.
Таблицу значений удобно ещё и искать: Таблица.Найти(Значение, "Артикул") вернёт строку по значению колонки, а Таблица.НайтиСтроки(Отбор) — массив подходящих строк. Курсор выборки такого не умеет.
Колонки выгруженной таблицы повторяют поля запроса: если в запросе поле названо через КАК Артикул, то и колонка будет Артикул, и обращаться к ней можно как Строка.Артикул. Список колонок доступен через Таблица.Колонки — это пригодится, когда структура результата заранее не известна. Для линейного результата без итогов выгрузку обычно делают прямым обходом: Результат.Выгрузить(ОбходРезультатаЗапроса.Прямой).
Какой способ выбрать под вашу задачу:
Пока Следующий(), между проходами вызывайте Сбросить().Результат получите две выборки повторным Выбрать() — у каждой свой курсор.Таблица = Результат.Выгрузить() и работайте с таблицей значений: индекс, Найти, НайтиСтроки.Обработка NULL при повторном чтении полей
При повторном обходе значения полей те же, что и в первый раз, — выборка их не меняет. Но если в запросе есть левые соединения или остатки, часть полей приходит как NULL, и это одинаково мешает при любом проходе. Лучше закрыть NULL ещё в тексте запроса, тогда в коде проверять ничего не придётся.
В языке запросов для этого есть функция ЕСТЬNULL:
ВЫБРАТЬ
Товары.Номенклатура КАК Номенклатура,
ЕСТЬNULL(Остатки.КоличествоОстаток, 0) КАК Остаток
ИЗ
Справочник.Номенклатура КАК Товары
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки КАК Остатки
ПО Остатки.Номенклатура = Товары.Ссылка
Теперь поле Остаток всегда число, а не NULL, — и при первом, и при повторном проходе арифметика с ним не упадёт.
Если по какой-то причине NULL всё же доходит до кода, сравнивайте с ним явно:
Пока Выборка.Следующий() Цикл
Если Выборка.Остаток = NULL Тогда
Остаток = 0;
Иначе
Остаток = Выборка.Остаток;
КонецЕсли;
КонецЦикла;
Не путайте NULL, Неопределено и пустую ссылку — это три разных значения. NULL приходит из СУБД, когда соединение не нашло пары. ЗначениеЗаполнено() считает NULL незаполненным, но безопаснее убрать NULL в запросе функцией ЕСТЬNULL.
Производительность: когда перечитывать, а когда держать в памяти
Главное правило — не вызывайте Запрос.Выполнить() повторно ради второго прохода. Каждый Выполнить() — это обращение к СУБД: разбор текста, построение плана, чтение данных. Если данные те же, одного выполнения достаточно, а дальше работайте с результатом в памяти.
Ориентиры по стоимости операций:
- 🟢
Сбросить()и повторныйВыбрать()— почти бесплатны, обращения к базе нет. - 🟢
Выгрузить()в таблицу значений — один раз занимает память под все строки, зато дальше любое число проходов и поиск по индексу без СУБД. - 🔴 Повторный
Выполнить()— полноценный новый запрос к базе, самый дорогой вариант.
Выбор между выборкой и выгрузкой в таблицу зависит от объёма. Если строк немного и нужен один-два прохода — хватит Сбросить(). Если строк десятки и сотни тысяч, а проходов много, взвесьте: таблица значений держит всё в памяти клиента или сервера, и на больших объёмах это заметная нагрузка. Тогда разумнее сузить сам запрос — отобрать только нужные поля и записи — чем тащить лишнее и потом многократно по нему бегать.
Ещё один приём для тяжёлых обходов — считать агрегаты сразу в запросе. Если второй проход нужен лишь чтобы просуммировать поле или найти максимум, добавьте в запрос СУММА, МАКСИМУМ или КОЛИЧЕСТВО с группировкой — СУБД посчитает это быстрее, чем ручной цикл по выборке, и второй проход вообще не понадобится.
Временные таблицы как альтернатива повторным проходам
Когда результат нужен не для одного обхода в коде, а сразу в нескольких запросах, его помещают во временную таблицу. Для этого запросу назначают менеджер временных таблиц, а в тексте используют конструкцию ПОМЕСТИТЬ:
МенеджерВТ = Новый МенеджерВременныхТаблиц;
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
Запрос.Текст =
"ВЫБРАТЬ
| Товары.Ссылка КАК Номенклатура
|ПОМЕСТИТЬ ВТ_Товары
|ИЗ
| Справочник.Номенклатура КАК Товары";
Запрос.Выполнить();
После этого таблица ВТ_Товары доступна в последующих запросах того же менеджера — к ней можно обращаться и соединять с другими источниками сколько угодно раз, не пересчитывая исходную выборку. Это и есть «хранимый» вариант повторного использования данных на стороне СУБД.
Разница с выгрузкой в таблицу значений: временная таблица живёт в базе и участвует в запросах, таблица значений живёт в памяти приложения и обходится циклом. Для чисто кодового обхода берите выгрузку, для соединений в последующих запросах — временную таблицу.
Повторный обход вложенной выборки по группировкам
Если в запросе есть ИТОГИ ПО, результат читают выборкой по группировкам, а внутри каждой группы получают детальную выборку. И тут важная деталь: вложенная выборка каждый раз получается заново и уже стоит в начале своей группы — специально сбрасывать её не нужно.
ВыборкаСклад = Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаСклад.Следующий() Цикл
// на каждой итерации — своя детальная выборка, с первой записи
ВыборкаСтрок = ВыборкаСклад.Выбрать(ОбходРезультатаЗапроса.Прямой);
Пока ВыборкаСтрок.Следующий() Цикл
Сообщить(ВыборкаСклад.Склад + ": " + ВыборкаСтрок.Артикул);
КонецЦикла;
КонецЦикла;
Чтобы пройти детали одной группы повторно, не выходя из неё, примените тот же приём, что и к обычной выборке: ВыборкаСтрок.Сбросить() вернёт курсор к первой строке группы, а повторный ВыборкаСклад.Выбрать() даст новую детальную выборку. Внешнюю выборку по группировкам тоже можно обойти заново — через ВыборкаСклад.Сбросить().
Поля верхнего уровня (ВыборкаСклад.Склад) доступны и внутри вложенного цикла — обращаться к внешней выборке, пока вы в её группе, можно свободно.
Частые вопросы
Почему второй цикл `Пока Выборка.Следующий()` ничего не выводит?
Выборка.Сбросить() или получите новую выборку через Результат.Выбрать().Можно ли обойти выборку результата запроса через `Для Каждого`?
Пока Выборка.Следующий() Цикл. Конструкция Для Каждого работает по таблице значений, которую даёт Результат.Выгрузить().Чем `Сбросить()` отличается от повторного `Выбрать()`?
Сбросить() возвращает в начало ту же самую выборку. Повторный Выбрать() создаёт новую независимую выборку из того же результата — так можно вести два обхода одновременно.Как получить запись выборки по номеру?
Таблица[0], Таблица[5] и так далее.Нужно ли вызывать `Выполнить()` ещё раз для второго прохода?
Сбросить(), повторный Выбрать() или Выгрузить().