Как заново пройти выборку запроса в 1С 8.3

· 9 мин

Содержание

Выборка результата запроса в 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

Временные таблицы как альтернатива повторным проходам

Когда результат нужен не для одного обхода в коде, а сразу в нескольких запросах, его помещают во временную таблицу. Для этого запросу назначают менеджер временных таблиц, а в тексте используют конструкцию ПОМЕСТИТЬ:

МенеджерВТ = Новый МенеджерВременныхТаблиц;

Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
Запрос.Текст =
	"ВЫБРАТЬ
	|	Товары.Ссылка КАК Номенклатура
	|ПОМЕСТИТЬ ВТ_Товары
	|ИЗ
	|	Справочник.Номенклатура КАК Товары";
Запрос.Выполнить();

После этого таблица ВТ_Товары доступна в последующих запросах того же менеджера — к ней можно обращаться и соединять с другими источниками сколько угодно раз, не пересчитывая исходную выборку. Это и есть «хранимый» вариант повторного использования данных на стороне СУБД.

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

Повторный обход вложенной выборки по группировкам

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

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

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

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

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

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