Обход результата запроса в 1С 8.3: выборка, таблица значений, итоги и порции

· 9 мин

Содержание

Перебрать запрос в 1С проще всего так: Выборка = Запрос.Выполнить().Выбрать(); и затем цикл Пока Выборка.Следующий() Цикл … КонецЦикла;. Внутри цикла поля доступны по псевдонимам из текста запроса: Выборка.Наименование, Выборка.Сумма.

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

Ниже код для 1С:Предприятия 8.3. Запрос выполняется на сервере: в управляемой форме код размещают в процедуре с директивой &НаСервере, в тонком клиенте объект Запрос недоступен.

Что возвращает запрос и чем перебирать результат

Метод Запрос.Выполнить() возвращает объект РезультатЗапроса. Сам по себе он ничего не перебирает. У него есть несколько методов, и от выбора метода зависит способ обхода.

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

Выборка — это курсор. Он стоит перед первой записью, каждый вызов Следующий() сдвигает его на одну строку и возвращает Истина, пока строки есть. Таблица значений — полноценная коллекция в памяти: к строкам можно обращаться по индексу, сортировать их, искать.

Если нужен только факт наличия данных, не перебирайте ничего. Если РезультатЗапроса.Пустой() Тогда Возврат; КонецЕсли; — самый дешёвый вариант.

Способ 1. Перебор выборки циклом «Пока Следующий()»

Базовый способ, которым закрывается большинство задач. Выборка не создаёт отдельную таблицу, поэтому для простого чтения строк по порядку это экономный вариант.

Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
|	Номенклатура.Ссылка КАК Ссылка,
|	Номенклатура.Наименование КАК Наименование,
|	Номенклатура.Артикул КАК Артикул
|ИЗ
|	Справочник.Номенклатура КАК Номенклатура
|ГДЕ
|	НЕ Номенклатура.ЭтоГруппа
|	И НЕ Номенклатура.ПометкаУдаления
|
|УПОРЯДОЧИТЬ ПО
|	Наименование";

РезультатЗапроса = Запрос.Выполнить();
Если РезультатЗапроса.Пустой() Тогда
	Возврат;
КонецЕсли;

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

Полезные методы выборки

  • 🔢 Выборка.Количество() — сколько записей в выборке. Вызывать можно до цикла.
  • 🔁 Выборка.Сбросить() — вернуть курсор в начало и пройти выборку ещё раз без повторного запроса.
  • 🔍 Выборка.НайтиСледующий(Значение, "ИмяКолонки") или Выборка.НайтиСледующий(Структура) — сдвинуть курсор к следующей записи с нужным значением. Поиск идёт от текущей позиции вперёд.
  • ⏹ Прервать внутри цикла — выйти, как только нашли нужное.

Если нужна только первая строка, не крутите цикл. Достаточно условия:

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

В тексте запроса при этом добавьте ВЫБРАТЬ ПЕРВЫЕ 1 и УПОРЯДОЧИТЬ ПО. Без сортировки «первая» строка будет случайной.

Способ 2. Перебор запроса через таблицу значений и «Для Каждого»

Выгрузить() превращает результат в таблицу значений. Её обходят циклом Для Каждого, и это удобно, когда данные нужны несколько раз, в произвольном порядке или для поиска.

ТаблицаТоваров = Запрос.Выполнить().Выгрузить();

Для Каждого СтрокаТЗ Из ТаблицаТоваров Цикл
	Сообщить(СтрокаТЗ.Наименование);
КонецЦикла;

Что даёт таблица значений, чего нет у выборки:

  • 📌 доступ по индексу: ТаблицаТоваров[0], ТаблицаТоваров.Количество();
  • 🔎 быстрый поиск после индексации: ТаблицаТоваров.Индексы.Добавить("Артикул"), затем ТаблицаТоваров.НайтиСтроки(Новый Структура("Артикул", "А-100"));
  • 📊 свёртка и сортировка: Свернуть("Номенклатура", "Количество, Сумма"), Сортировать("Сумма Убыв");
  • 📤 выгрузка колонки в массив: ВыгрузитьКолонку("Ссылка") — удобно для передачи параметром в следующий запрос.

Обход в обратном порядке и удаление строк

Удалять строки внутри Для Каждого нельзя: коллекция меняется под циклом, и часть строк пропускается. Идут с конца по индексу:

Всего = ТаблицаТоваров.Количество();
Для Сч = 1 По Всего Цикл
	СтрокаТЗ = ТаблицаТоваров[Всего - Сч];
	Если СтрокаТЗ.Количество = 0 Тогда
		ТаблицаТоваров.Удалить(СтрокаТЗ);
	КонецЕсли;
КонецЦикла;

Граница Всего вычисляется один раз до цикла. При Всего = 5 индексы идут 4, 3, 2, 1, 0, и удаление строки не сдвигает ещё не пройденные.

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

Перебор итогов и группировок запроса

Когда в запросе есть секция ИТОГИ, результат содержит не только детальные строки, но и строки итогов. Обходить его можно по-разному.

Запрос.Текст =
"ВЫБРАТЬ
|	Товары.Ссылка.Контрагент КАК Контрагент,
|	Товары.Номенклатура КАК Номенклатура,
|	Товары.Сумма КАК Сумма
|ИЗ
|	Документ.РеализацияТоваровУслуг.Товары КАК Товары
|ГДЕ
|	Товары.Ссылка.Проведен
|	И Товары.Ссылка.Дата МЕЖДУ &Начало И &Окончание
|ИТОГИ
|	СУММА(Сумма)
|ПО
|	Контрагент";

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

Сначала перебираем контрагентов, внутри каждого — его строки:

Результат = Запрос.Выполнить();
ВыборкаКонтрагенты = Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);

Пока ВыборкаКонтрагенты.Следующий() Цикл
	Сообщить(Строка(ВыборкаКонтрагенты.Контрагент) + ": "
		+ Формат(ВыборкаКонтрагенты.Сумма, "ЧДЦ=2"));

	ВыборкаСтроки = ВыборкаКонтрагенты.Выбрать();
	Пока ВыборкаСтроки.Следующий() Цикл
		Сообщить("   " + Строка(ВыборкаСтроки.Номенклатура) + " — "
			+ Формат(ВыборкаСтроки.Сумма, "ЧДЦ=2"));
	КонецЦикла;
КонецЦикла;

Вызов Выбрать() у выборки верхнего уровня возвращает подчинённую выборку только по текущей группе. Так строится отчёт «контрагент → его товары» без ручной группировки в коде.

Прямой обход с отсевом итоговых строк

При обычном Выбрать() (прямой обход) итоговые и детальные строки идут вперемешку. Различают их методом ТипЗаписи():

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

Для иерархических справочников есть третий вариант — ОбходРезультатаЗапроса.ПоГруппировкамСИерархией. Его используют, когда итоги строятся ПО Номенклатура ИЕРАРХИЯ и нужно пройти группы и подгруппы.

Временные таблицы ПОМЕСТИТЬ вместо запроса в цикле

Самая дорогая ошибка при переборе — выполнять запрос внутри цикла по другой выборке. На 1000 строк это 1000 обращений к СУБД. Правильно — собрать всё в один пакет через временные таблицы.

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

Пример: есть таблица значений с артикулами из загрузки, нужно найти по ним номенклатуру.

ТаблицаЗагрузки = Новый ТаблицаЗначений;
ТаблицаЗагрузки.Колонки.Добавить("Артикул",
	Новый ОписаниеТипов("Строка", , Новый КвалификаторыСтроки(50)));
ТаблицаЗагрузки.Колонки.Добавить("Количество",
	Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 3)));
// ... заполнение таблицы из файла

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

Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
	Если Выборка.Номенклатура = NULL Тогда
		Сообщить("Не найден артикул " + Выборка.Артикул);
	КонецЕсли;
КонецЦикла;

Что здесь важно:

  1. Колонки таблицы-параметра типизированы через ОписаниеТипов. Без типов запрос не поймёт, что лежит в колонке.
  2. Строка ограничена по длине. Строки неограниченной длины нельзя сравнивать в условиях соединения.
  3. Таблицу значений сначала помещают во временную таблицу простым запросом, и уже потом соединяют с другими таблицами.
  4. Выполнить() вернёт результат последнего запроса пакета. Если нужны результаты всех, вызывайте ВыполнитьПакет(): получится массив, где [0] — результат первого запроса, [1] — второго.

Менеджер временных таблиц живёт, пока жива переменная. Его можно передать в другой запрос (Запрос2.МенеджерВременныхТаблиц = Запрос.МенеджерВременныхТаблиц) и продолжить работу с ВТ_Загрузка, не выполняя первый запрос повторно. Когда таблицы больше не нужны, вызовите МенеджерВременныхТаблиц.Закрыть().

Обработка большого результата запроса порциями

Метода вроде «ВыбратьПачками» у результата запроса в платформе нет. Порции собирают сами. Это нужно, когда по каждой строке что-то записывается: долгая транзакция на 200 000 документов держит блокировки и мешает пользователям.

Надёжная схема: лёгким запросом получить список ссылок, разбить его на массивы по 1000 элементов и каждую порцию обработать отдельным запросом с условием В (&Порция).

Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
|	Док.Ссылка КАК Ссылка
|ИЗ
|	Документ.РеализацияТоваровУслуг КАК Док
|ГДЕ
|	Док.Дата МЕЖДУ &Начало И &Окончание
|
|УПОРЯДОЧИТЬ ПО
|	Док.Дата";
Запрос.УстановитьПараметр("Начало", НачалоПериода);
Запрос.УстановитьПараметр("Окончание", КонецПериода);

МассивСсылок = Запрос.Выполнить().Выгрузить().ВыгрузитьКолонку("Ссылка");

РазмерПорции = 1000;
Всего = МассивСсылок.Количество();
Начало = 0;
Пока Начало < Всего Цикл
	Порция = Новый Массив;
	Конец = Мин(Начало + РазмерПорции, Всего) - 1;
	Для Индекс = Начало По Конец Цикл
		Порция.Добавить(МассивСсылок[Индекс]);
	КонецЦикла;

	ОбработатьПорцию(Порция); // внутри — запрос с условием Ссылка В (&Порция)

	Начало = Начало + РазмерПорции;
КонецЦикла;

Проверка на 2500 документах: первая порция — индексы 0…999, вторая — 1000…1999, третья — 2000…2499 (Мин(3000, 2500) - 1 = 2499). После неё Начало = 3000, цикл завершается.

Если внутри порции записываются объекты, оборачивайте в транзакцию каждую порцию отдельно, а не весь объём. Тогда ошибка на 1700-м документе откатит одну порцию, а не всю работу.

Массовую запись документов запускайте сначала на копии базы. Ошибка в обработке на рабочих данных исправляется дольше, чем пишется сама обработка.

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

СпособКогда подходитСлабое место
Выборка Пока Следующий()Один проход по строкам, отчёт, проверкаНет доступа по индексу, повторный проход — через Сбросить()
Выгрузить() + Для КаждогоДанные нужны несколько раз, поиск, сортировка, свёрткаВся таблица в памяти
Обход ПоГруппировкамВложенные отчёты «группа → строки» с итогамиТребует секции ИТОГИ
Пакет с ПОМЕСТИТЬДанных из нескольких источников, связь с внешней таблицейНужно следить за типами колонок параметра
Порции по массиву ссылокМассовая запись или изменение объектовБольше кода, несколько запросов

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

Ошибки при переборе результата запроса

  • 🐢 ПолучитьОбъект() в цикле ради чтения реквизита. Каждый вызов читает объект целиком вместе с табличными частями. Нужный реквизит добавьте в текст запроса — через точку, например Док.Контрагент.ИНН.
  • ❌ Обращение к полю до первого Следующий(). Курсор стоит перед первой записью, данных в нём ещё нет. Сначала Следующий(), потом чтение полей.
  • 🧩 Сообщение Поле объекта не обнаружено (…). Чаще всего в коде указано имя поля, а в запросе стоит другой псевдоним после КАК. Обращайтесь по псевдониму.
  • 🕳 NULL после левого соединения. Если справа ничего не нашлось, поле равно NULL, и сложение с ним даёт ошибку. В запросе оборачивайте поле в ЕСТЬNULL(Поле, 0).
  • 🔁 НайтиСледующий() без Сбросить(). Поиск идёт только вперёд от текущей позиции. Второй поиск значения, которое уже пройдено, вернёт Ложь.
  • 📦 Выгрузить() только ради цикла Для Каждого. Если таблица потом не используется, это лишняя копия данных в памяти. Хватит выборки.

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

Можно ли перебрать запрос на клиенте?
В тонком и веб-клиенте объект Запрос недоступен. Выполните запрос в серверной процедуре и верните на клиент то, что нужно: массив, структуру или данные формы.
Как узнать количество строк до начала перебора?
У выборки есть Количество(), у таблицы значений — тоже Количество(). Если нужен только счёт без данных, быстрее запрос с КОЛИЧЕСТВО(*).
Зачем нужен ВыполнитьПакет, если есть Выполнить?
Выполнить() отдаёт результат только последнего запроса пакета. ВыполнитьПакет() возвращает массив результатов всех запросов, и каждый можно перебрать отдельно.
Почему порядок строк в выборке каждый раз разный?
Без УПОРЯДОЧИТЬ ПО СУБД отдаёт строки в произвольном порядке. Если порядок важен, задайте сортировку в тексте запроса.
Как перебрать результат запроса, превратив его в дерево?
Вызовите Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам) для запроса с итогами: получится дерево значений. Его строки обходят рекурсивно через свойство Строки каждой строки дерева.