Чтение результата запроса во встроенном языке 1С: от выборки до пакетов и временных таблиц
Содержание
Чтобы получить данные из запроса в 1С, выполните его методом Выполнить(), вызовите у результата Выбрать() и пройдите выборку циклом Пока Выборка.Следующий() Цикл. Значение каждого поля берётся по его псевдониму: Выборка.Контрагент, Выборка.Сумма.
Это базовый путь. Но на практике код спотыкается в других местах: поле приходит NULL, итоги читаются не на том уровне, пакет из пяти запросов возвращает не тот результат, а выгрузка миллиона строк съедает память сервера. Для каждого из этих случаев есть свой приём, и он укладывается в несколько строк кода на платформе 8.3.
Как выполнить запрос и получить выборку
Цепочка всегда одна: объект Запрос → текст и параметры → РезультатЗапроса → способ чтения. Код работает в серверном контексте, поэтому в форме его размещают в процедуре с директивой &НаСервере или в общем модуле.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Реализация.Ссылка КАК Ссылка,
| Реализация.Дата КАК Дата,
| Реализация.Контрагент КАК Контрагент,
| Реализация.СуммаДокумента КАК Сумма
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Реализация
|ГДЕ
| Реализация.Дата МЕЖДУ &НачалоПериода И &КонецПериода
| И Реализация.Проведен";
Запрос.УстановитьПараметр("НачалоПериода", НачалоМесяца(ТекущаяДатаСеанса()));
Запрос.УстановитьПараметр("КонецПериода", КонецМесяца(ТекущаяДатаСеанса()));
РезультатЗапроса = Запрос.Выполнить();
Если РезультатЗапроса.Пустой() Тогда
Возврат;
КонецЕсли;
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
Сообщить(СтрШаблон("%1 — %2 руб.", Выборка.Ссылка, Формат(Выборка.Сумма, "ЧДЦ=2")));
КонецЦикла;
Что здесь происходит по шагам:
Выполнить()отправляет запрос в СУБД и возвращает объектРезультатЗапроса. Данные уже получены, дальше вы только читаете их.Пустой()проверяет, есть ли строки. Это дешевле, чем выгружать результат и считать его размер.Выбрать()создаётВыборкаИзРезультатаЗапроса— курсор, стоящий перед первой строкой.Следующий()сдвигает курсор и возвращаетИстина, пока строки не кончились.
Если нужна одна строка (например, последняя цена), напишите в запросе ВЫБРАТЬ ПЕРВЫЕ 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-й тысяче не откатит всё сделанное.
Массовую запись в рабочей базе запускайте в нерабочее время и только после резервной копии: длинные транзакции блокируют данные, и у пользователей начнутся ожидания на блокировках.
Ошибки при получении данных и как их перехватить
Ошибки бывают двух видов: синтаксические (текст запроса не разобрался) и ошибки выполнения (параметр, тип, соединение с СУБД). И те и другие возникают в момент вызова Выполнить(), поэтому оборачивать в Попытка нужно именно его:
Попытка
РезультатЗапроса = Запрос.Выполнить();
Исключение
Инфо = ИнформацияОбОшибке();
ЗаписьЖурналаРегистрации("Загрузка данных",
УровеньЖурналаРегистрации.Ошибка, , ,
ПодробноеПредставлениеОшибки(Инфо));
ВызватьИсключение КраткоеПредставлениеОшибки(Инфо);
КонецПопытки;
Подробный текст уходит в журнал регистрации для разработчика, пользователю — короткий. Не глушите исключение пустым блоком Исключение: обработка «отработает успешно» и ничего не сделает.
Для разбора сложных случаев используйте консоль запросов: она показывает каждую временную таблицу пакета отдельно. Отладчик в режиме конфигуратора тоже умеет раскрывать РезультатЗапроса — выделите переменную и откройте её в окне вычисления выражения.
Частые вопросы
Можно ли вернуться к началу выборки и пройти её второй раз?
Сбросить() возвращает курсор перед первой строкой. Повторно выполнять запрос не нужно.Как узнать число строк, не выгружая результат?
Выборка.Количество(). Если нужно только «есть или нет», используйте РезультатЗапроса.Пустой() — так проще.Как найти в выборке строку с нужным значением?
НайтиСледующий() ищет от текущей позиции строку по значению поля или по структуре полей. Для частого поиска удобнее выгрузить в таблицу значений и вызвать НайтиСтроки().Можно ли передать результат запроса на клиент?
РезультатЗапроса и выборка существуют только на сервере. На клиент передают таблицу значений, массив или структуру, а для формы — реквизит типа «Таблица значений» с методом ЗначениеВРеквизитФормы().Почему порядок строк в выборке каждый раз разный?
УПОРЯДОЧИТЬ ПО СУБД не обязана возвращать строки в одном порядке. Если порядок важен, задавайте сортировку явно.