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