Обход результата запроса в 1С 8.3: выборка, таблица значений, итоги и порции
Содержание
Перебрать запрос в 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 Тогда
Сообщить("Не найден артикул " + Выборка.Артикул);
КонецЕсли;
КонецЦикла;
Что здесь важно:
- Колонки таблицы-параметра типизированы через
ОписаниеТипов. Без типов запрос не поймёт, что лежит в колонке. - Строка ограничена по длине. Строки неограниченной длины нельзя сравнивать в условиях соединения.
- Таблицу значений сначала помещают во временную таблицу простым запросом, и уже потом соединяют с другими таблицами.
Выполнить()вернёт результат последнего запроса пакета. Если нужны результаты всех, вызывайтеВыполнитьПакет(): получится массив, где[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). - 🔁
НайтиСледующий()безСбросить(). Поиск идёт только вперёд от текущей позиции. Второй поиск значения, которое уже пройдено, вернётЛожь. - 📦
Выгрузить()только ради циклаДля Каждого. Если таблица потом не используется, это лишняя копия данных в памяти. Хватит выборки.
Частые вопросы
Можно ли перебрать запрос на клиенте?
Запрос недоступен. Выполните запрос в серверной процедуре и верните на клиент то, что нужно: массив, структуру или данные формы.Как узнать количество строк до начала перебора?
Количество(), у таблицы значений — тоже Количество(). Если нужен только счёт без данных, быстрее запрос с КОЛИЧЕСТВО(*).Зачем нужен ВыполнитьПакет, если есть Выполнить?
Выполнить() отдаёт результат только последнего запроса пакета. ВыполнитьПакет() возвращает массив результатов всех запросов, и каждый можно перебрать отдельно.Почему порядок строк в выборке каждый раз разный?
УПОРЯДОЧИТЬ ПО СУБД отдаёт строки в произвольном порядке. Если порядок важен, задайте сортировку в тексте запроса.Как перебрать результат запроса, превратив его в дерево?
Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам) для запроса с итогами: получится дерево значений. Его строки обходят рекурсивно через свойство Строки каждой строки дерева.