Чтение результата запроса во встроенном языке 1С: от выборки до пакетов и временных таблиц

· 10 мин

Содержание

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

Это базовый путь. Но на практике код спотыкается в других местах: поле приходит NULL, итоги читаются не на том уровне, пакет из пяти запросов возвращает не тот результат, а выгрузка миллиона строк съедает память сервера. Для каждого из этих случаев есть свой приём, и он укладывается в несколько строк кода на платформе 8.3.

Как выполнить запрос и получить выборку

Цепочка всегда одна: объект Запрос → текст и параметры → РезультатЗапроса → способ чтения. Код работает в серверном контексте, поэтому в форме его размещают в процедуре с директивой &НаСервере или в общем модуле.

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

Запрос.УстановитьПараметр("НачалоПериода", НачалоМесяца(ТекущаяДатаСеанса()));
Запрос.УстановитьПараметр("КонецПериода", КонецМесяца(ТекущаяДатаСеанса()));

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

Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
	Сообщить(СтрШаблон("%1 — %2 руб.", Выборка.Ссылка, Формат(Выборка.Сумма, "ЧДЦ=2")));
КонецЦикла;

Что здесь происходит по шагам:

  1. Выполнить() отправляет запрос в СУБД и возвращает объект РезультатЗапроса. Данные уже получены, дальше вы только читаете их.
  2. Пустой() проверяет, есть ли строки. Это дешевле, чем выгружать результат и считать его размер.
  3. Выбрать() создаёт ВыборкаИзРезультатаЗапроса — курсор, стоящий перед первой строкой.
  4. Следующий() сдвигает курсор и возвращает Истина, пока строки не кончились.

Если нужна одна строка (например, последняя цена), напишите в запросе ВЫБРАТЬ ПЕРВЫЕ 1 и вызовите Если Выборка.Следующий() Тогда … КонецЕсли вместо цикла. Код сразу показывает, что строка ожидается одна.

Сам текст запроса удобнее собирать конструктором: в модуле щёлкните правой кнопкой мыши внутри строки с текстом запроса и выберите «Конструктор запроса». Он расставит переносы и символы | сам.

Чтение полей: имена, типы, NULL и ссылки

Поле выборки доступно по псевдониму после КАК. Если псевдоним не задан, платформа назначит имя сама, и угадывать его неудобно. Всегда задавайте псевдонимы явно — это избавляет от половины ошибок «поле не найдено».

Обратиться к полю можно двумя способами:

  • 🔹 через точку: Выборка.Контрагент — обычный вариант;
  • 🔹 через квадратные скобки: Выборка["Контрагент"] — когда имя поля хранится в переменной.

Какие типы приходят в выборку

Что выбрано в запросеЧто получите в кодеНа что смотреть
Реквизит-ссылка (Контрагент)СправочникСсылка.КонтрагентыОбращение Выборка.Контрагент.ИНН идёт в базу ещё раз
Число, строка, датаПримитивный типСтрока неограниченной длины не сравнивается в запросе
Поле из левого соединенияЗначение или NULLБез ЕСТЬNULL арифметика в коде падает
Составной типЛюбой из типов составногоПроверяйте ТипЗнч()
ПРЕДСТАВЛЕНИЕ(Поле)СтрокаНе годится для отбора и сравнения

Ссылки и «точка через точку»

Код Выборка.Контрагент.ИНН выглядит невинно, но на каждой строке платформа читает объект контрагента отдельным обращением к базе. На 10 строках не заметно, на 50 000 — минуты.

Нужные реквизиты выбирайте прямо в запросе:

|	Реализация.Контрагент КАК Контрагент,
|	Реализация.Контрагент.ИНН КАК ИНН,
|	ПРЕДСТАВЛЕНИЕ(Реализация.Контрагент) КАК КонтрагентПредставление

NULL против пустого значения

NULL появляется, когда в левом соединении нет пары, и в агрегатах по пустому набору. В коде NULL — отдельный тип, не равный нулю и пустой строке. Выражение Выборка.Цена * Выборка.Количество с NULL в цене завершится ошибкой.

Лечится на стороне запроса:

|	ЕСТЬNULL(Цены.Цена, 0) КАК Цена

Если заменить в запросе нельзя, проверяйте в коде: Если Выборка.Цена = NULL Тогда. Функция ЗначениеЗаполнено() для NULL возвращает Ложь, поэтому тоже подходит для проверки «есть ли значение».

Состав колонок результата

Когда текст запроса собирается динамически, полезно узнать колонки до чтения:

Для Каждого Колонка Из РезультатЗапроса.Колонки Цикл
	Сообщить(Колонка.Имя + ": " + Строка(Колонка.ТипЗначения));
КонецЦикла;

ЕстьИНН = РезультатЗапроса.Колонки.Найти("ИНН") <> Неопределено;

Выгрузка результата в таблицу, массив или одно значение

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

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

Пример получения массива документов:

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

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

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

Итоги и группировки: как прочитать каждый уровень

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

Запрос для примеров:

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

Способ 1. Вложенные выборки по группировкам

РезультатЗапроса = Запрос.Выполнить();

ВыборкаОбщий = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаОбщий.Следующий() Цикл
	Сообщить("Всего: " + Формат(ВыборкаОбщий.Сумма, "ЧДЦ=2"));

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

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

Каждый уровень — своя выборка. Первый уровень здесь — ОБЩИЕ, в нём ровно одна строка с общей суммой. Если общий итог не нужен, уберите ОБЩИЕ из запроса или передайте имя группировки вторым параметром: Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "Контрагент").

Способ 2. Прямой обход и тип записи

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

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

Для справочников с иерархией есть ещё ТипЗаписиЗапроса.ИтогПоИерархии и обход ОбходРезультатаЗапроса.ПоГруппировкамСИерархией. Уровень вложенности текущей строки вернёт Выборка.Уровень().

Способ 3. Сразу в дерево

Для вывода в форму с деревом проще всего Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам). Получится ДеревоЗначений, где у строки-контрагента в Строки лежат документы.

Пакетный запрос и временные таблицы

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

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

Какой результат вернётся

МетодЧто вернёт
Выполнить()Результат последнего запроса пакета
ВыполнитьПакет()Массив результатов всех запросов, индексы с 0
Результаты = Запрос.ВыполнитьПакет();
ВыборкаИтог = Результаты[1].Выбрать();

Результат запроса с ПОМЕСТИТЬ содержит одну строку с полем Количество — сколько записей попало во временную таблицу. По нему удобно проверить, есть ли что обрабатывать дальше.

Индексы считайте от нуля и после каждой правки пакета перепроверяйте. Добавили в начало ещё одну временную таблицу — Результаты[1] теперь указывает не туда.

Временные таблицы между запросами

Без менеджера временные таблицы живут только внутри одного выполнения пакета. Чтобы использовать их в нескольких запросах подряд, нужен МенеджерВременныхТаблиц:

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

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

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

Содержимое уже созданной таблицы можно посмотреть без отдельного запроса: МенеджерВТ.Таблицы["ВТ_Документы"].ПолучитьДанные() вернёт результат запроса. Это удобно при отладке. Ненужные таблицы удаляйте командой УНИЧТОЖИТЬ ВТ_Документы в тексте пакета или закрывайте менеджер методом Закрыть().

Таблица значений как источник

Таблицу значений из кода можно передать параметром и соединить с данными базы. Но сначала её нужно поместить во временную таблицу — прямо в соединении параметр-таблица не используется:

|ВЫБРАТЬ
|	Т.Номенклатура КАК Номенклатура,
|	Т.Количество КАК Количество
|ПОМЕСТИТЬ ВТ_Загрузка
|ИЗ
|	&ТаблицаЗагрузки КАК Т

У колонок передаваемой таблицы должны быть заданы типы через ОписаниеТипов. Таблица, созданная через Колонки.Добавить("Номенклатура") без типа, вызовет ошибку при выполнении.

Большие объёмы: как читать миллион строк и не уронить сервер

Типичный сценарий проблем — обработка, которая «работала на тестовой базе», а на рабочей висит час или падает с нехваткой памяти.

Правила, которые реально меняют время работы:

  • 🚫 Никаких запросов в цикле. 10 000 строк выборки × запрос внутри = 10 000 обращений к СУБД. Соберите ключи в массив, передайте параметром В (&Массив) или во временную таблицу и сделайте один запрос.
  • 📉 Выбирайте только нужные поля. ВЫБРАТЬ * тянет все реквизиты, включая длинные строки и хранилища значений.
  • 🧭 Не используйте Выгрузить() для потоковой обработки. Выборка читает строки по очереди и не строит вторую копию данных.
  • 🔗 Реквизиты ссылок — в запросе, не через точку в коде (см. выше).
  • 🗂 Индексируйте временные таблицы, по которым идёт соединение: ИНДЕКСИРОВАТЬ ПО Номенклатура после ПОМЕСТИТЬ.

Чтение порциями

Когда объём заранее неизвестен, обрабатывайте данные порциями по ссылке:

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

ПоследняяСсылка = Справочники.Номенклатура.ПустаяСсылка();
Пока Истина Цикл
	Запрос.УстановитьПараметр("ПоследняяСсылка", ПоследняяСсылка);
	Выборка = Запрос.Выполнить().Выбрать();
	Если Выборка.Количество() = 0 Тогда
		Прервать;
	КонецЕсли;
	Пока Выборка.Следующий() Цикл
		// обработка элемента
		ПоследняяСсылка = Выборка.Ссылка;
	КонецЦикла;
КонецЦикла;

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

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

Ошибки при получении данных и как их перехватить

Ошибки бывают двух видов: синтаксические (текст запроса не разобрался) и ошибки выполнения (параметр, тип, соединение с СУБД). И те и другие возникают в момент вызова Выполнить(), поэтому оборачивать в Попытка нужно именно его:

Попытка
	РезультатЗапроса = Запрос.Выполнить();
Исключение
	Инфо = ИнформацияОбОшибке();
	ЗаписьЖурналаРегистрации("Загрузка данных",
		УровеньЖурналаРегистрации.Ошибка, , ,
		ПодробноеПредставлениеОшибки(Инфо));
	ВызватьИсключение КраткоеПредставлениеОшибки(Инфо);
КонецПопытки;

Подробный текст уходит в журнал регистрации для разработчика, пользователю — короткий. Не глушите исключение пустым блоком Исключение: обработка «отработает успешно» и ничего не сделает.

Диагностика: выберите симптом

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

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

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