Пакетный запрос 1С: временные таблицы, ВыполнитьПакет и отладка
Содержание
Пакет запросов в 1С — это несколько запросов в одном тексте, разделённых точкой с запятой ;. Выполняются они за одно обращение к базе, а промежуточные результаты складываются во временные таблицы оператором ПОМЕСТИТЬ. В коде такой текст запускают методом Запрос.ВыполнитьПакет() — он вернёт массив результатов, по одному на каждый запрос пакета.
Собрать и проверить пакет проще всего в консоли запросов (внешняя обработка «Консоль запросов» с диска ИТС или из типовой поставки). А чтобы встроить его в модуль, достаточно объекта Запрос, одного свойства Текст и выбора метода выполнения. Ниже — синтаксис, рабочие примеры на языке запросов 1С 8.3, приёмы ускорения и разбор типичных ошибок.
Что такое пакетный запрос и зачем он нужен
Обычный запрос возвращает одну таблицу. Но реальная задача часто требует нескольких шагов: сначала отобрать номенклатуру, потом по этому списку получить остатки, затем соединить с ценами. Если делать это вложенными подзапросами и соединениями в одном запросе, текст разрастается, а оптимизатор СУБД перестаёт строить хороший план.
Пакет решает это иначе. Каждый логический шаг — отдельный запрос. Результат шага кладётся во временную таблицу, следующий шаг читает уже из неё. Получается конвейер.
Когда пакет оправдан:
- 📊 Многошаговая выборка — данные готовятся в несколько приёмов, и каждый шаг опирается на предыдущий.
- 🔁 Повторное использование промежутка — один и тот же набор (например, список складов) нужен в двух-трёх местах, и его дешевле посчитать один раз.
- ⚡ Тяжёлые соединения — большое соединение разбивается на части, каждая из которых быстрее.
- 🧮 Отчёты и обработки — когда на основе одной выборки строится несколько разных результатов.
Важное ограничение: язык запросов 1С умеет только читать данные. Пакет не вставляет, не меняет и не удаляет записи. Для изменения данных существуют объектная модель и наборы записей — это отдельный механизм, не запрос.
Базовый синтаксис: как собрать и выполнить пакет
Запросы в тексте разделяются точкой с запятой. Чтобы передать результат дальше, первый запрос завершают конструкцией ПОМЕСТИТЬ ИмяТаблицы — это и есть временная таблица.
ВЫБРАТЬ
Номенклатура.Ссылка КАК Номенклатура,
Номенклатура.Артикул КАК Артикул
ПОМЕСТИТЬ ВТ_Товары
ИЗ
Справочник.Номенклатура КАК Номенклатура
ГДЕ
НЕ Номенклатура.ПометкаУдаления
;
ВЫБРАТЬ
ВТ_Товары.Номенклатура КАК Номенклатура,
ВТ_Товары.Артикул КАК Артикул,
ОстаткиТоваров.КоличествоОстаток КАК Остаток
ИЗ
ВТ_Товары КАК ВТ_Товары
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыНаСкладах.Остатки КАК ОстаткиТоваров
ПО ОстаткиТоваров.Номенклатура = ВТ_Товары.Номенклатура
Первый запрос сложил отобранную номенклатуру в таблицу ВТ_Товары. Второй читает уже из неё и соединяет с остатками. Имена временных таблиц принято начинать с ВТ_, чтобы отличать от реальных.
В коде всё это выполняет объект Запрос:
Запрос = Новый Запрос;
Запрос.Текст = ТекстПакета; // текст из примера выше
РезультатыПакета = Запрос.ВыполнитьПакет();
// Нужен результат последнего запроса пакета
ПоследнийРезультат = РезультатыПакета[РезультатыПакета.Количество() - 1];
Выборка = ПоследнийРезультат.Выбрать();
Пока Выборка.Следующий() Цикл
Сообщить(Выборка.Номенклатура);
КонецЦикла;
ВыполнитьПакет() возвращает массив РезультатЗапроса. Элементы идут по порядку запросов, нумерация с нуля. Для запросов с ПОМЕСТИТЬ результат будет пустым — данные ушли во временную таблицу, а не в выборку.
Есть и более короткий путь. Метод Запрос.Выполнить(), вызванный на пакете, выполняет весь текст и возвращает результат последнего запроса. Если промежуточные таблицы не нужны по отдельности, а важен только итог, хватает его:
Запрос = Новый Запрос(ТекстПакета);
Результат = Запрос.Выполнить(); // результат последнего запроса пакета
Параметры устанавливаются один раз на весь пакет: Запрос.УстановитьПараметр("Склад", Склад). Один параметр виден всем запросам текста.
Чтобы освободить временную таблицу досрочно, внутри пакета используют УНИЧТОЖИТЬ ВТ_Товары. После завершения запроса платформа очищает временные таблицы сама, поэтому вручную это нужно редко — например, когда таблица большая и память дорога.
Менеджер временных таблиц: когда шаги в разных запросах
Временные таблицы живут в пределах одного объекта Запрос. Но иногда нужно посчитать таблицу в одном месте кода, а использовать в другом — через несколько вызовов. Для этого есть МенеджерВременныхТаблиц.
МВТ = Новый МенеджерВременныхТаблиц;
Запрос1 = Новый Запрос;
Запрос1.МенеджерВременныхТаблиц = МВТ;
Запрос1.Текст =
"ВЫБРАТЬ Склад.Ссылка КАК Склад
|ПОМЕСТИТЬ ВТ_Склады
|ИЗ Справочник.Склады КАК Склад
|ГДЕ НЕ Склад.ПометкаУдаления";
Запрос1.Выполнить();
Запрос2 = Новый Запрос;
Запрос2.МенеджерВременныхТаблиц = МВТ;
Запрос2.Текст =
"ВЫБРАТЬ ВТ_Склады.Склад КАК Склад
|ИЗ ВТ_Склады КАК ВТ_Склады";
Результат = Запрос2.Выполнить();
Оба запроса подключены к одному менеджеру, поэтому таблица ВТ_Склады, созданная первым, видна второму. Такой менеджер часто передают в функции компоновки данных и в типовые методы — он позволяет собирать данные по кусочкам, не держа всё в одном тексте.
Менеджер очищается методом МВТ.Закрыть() либо автоматически, когда на него перестают ссылаться. В длительных обработках закрывайте его явно, чтобы не копить таблицы.
Чтение в транзакции: согласованность данных пакета
Запрос данные не меняет, поэтому собственной транзакции «на запись» у пакета нет. Но у чтения есть другой вопрос — согласованность. Если между двумя запросами пакета кто-то проведёт документ, второй запрос может увидеть уже изменённые остатки.
В рамках одного вызова Выполнить() или ВыполнитьПакет() платформа читает данные согласованно — это одно обращение к базе. Проблема возникает, когда вы делаете несколько отдельных вызовов и между ними логика кода.
Чтобы несколько чтений видели одну и ту же картину данных, их оборачивают в управляемую транзакцию:
НачатьТранзакцию();
Попытка
Результат1 = Запрос1.Выполнить();
// ... обработка
Результат2 = Запрос2.Выполнить();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
Транзакция удерживает ресурсы и может ставить блокировки. Не держите её открытой во время долгих вычислений или диалога с пользователем — другие сеансы будут ждать. Открыли транзакцию — быстро прочитали, зафиксировали.
Если нужна защита от изменения данных конкурентами на время расчёта, применяют управляемые блокировки (БлокировкаДанных) перед чтением. Это уже уровень учётной логики проведения, а не самого пакета запросов, и включать его стоит осознанно.
Оптимизация пакетных запросов: как ускорить выполнение
Пакет сам по себе быстрее не делает — ускоряет правильная работа с временными таблицами. Несколько приёмов, которые дают основной выигрыш.
Индексируйте временные таблицы. Если по временной таблице потом идёт соединение или отбор, добавьте индекс по полю соединения:
ВЫБРАТЬ
Заказы.Номенклатура КАК Номенклатура,
Заказы.Количество КАК Количество
ПОМЕСТИТЬ ВТ_Заказы
ИЗ
Документ.ЗаказПокупателя.Товары КАК Заказы
ИНДЕКСИРОВАТЬ ПО
Номенклатура
Без индекса соединение большой временной таблицы приводит к полному перебору.
Отбирайте раньше. Условия ГДЕ ставьте на первом шаге, чтобы во временную таблицу попало как можно меньше строк. Чем меньше промежуток — тем дешевле все последующие шаги.
Не соединяйте с виртуальными таблицами напрямую. Остатки и обороты регистров сначала положите в свою временную таблицу, а уже её соединяйте. Это классический приём: виртуальная таблица в соединении часто ломает план запроса.
Разрешённые данные. Если у пользователя настроено ограничение доступа (RLS), пишите ВЫБРАТЬ РАЗРЕШЕННЫЕ — иначе запрос упадёт на записи, к которой нет прав, вместо того чтобы её пропустить.
Сравнение подходов к многошаговой выборке:
| Подход | Когда применять | Риски |
|---|---|---|
| Один запрос с подзапросами | Данных мало, 1–2 соединения | План запроса деградирует на объёме |
| Пакет с временными таблицами | Много шагов, тяжёлые соединения | Нужно не забыть индексы |
| Запрос в цикле | Почти никогда | Обращение к базе на каждой итерации — самая частая причина тормозов |
Главный антипаттерн — запрос внутри цикла по строкам другой выборки. Его почти всегда можно свернуть в пакет с одним соединением по временной таблице.
Отладка пакета: как увидеть промежуточные таблицы
Главная сложность пакета — промежуточные таблицы не видно, если пользоваться обычным ВыполнитьПакет(): там, где был ПОМЕСТИТЬ, вернётся пустой результат. Для отладки есть специальный метод.
Запрос.ВыполнитьПакетСПромежуточнымиДанными() возвращает массив, где для каждого ПОМЕСТИТЬ лежит таблица значений с её реальным содержимым. Удобно смотреть, что именно попало в ВТ_Товары на шаге 2:
РезультатыОтладки = Запрос.ВыполнитьПакетСПромежуточнымиДанными();
Для Каждого РезультатШага Из РезультатыОтладки Цикл
Если ТипЗнч(РезультатШага) = Тип("ТаблицаЗначений") Тогда
// содержимое временной таблицы
КонецЕсли;
КонецЦикла;
Этот метод тяжелее обычного — он выгружает все промежутки в память. В рабочем коде его не используют, только при разборе.
Второй инструмент — консоль запросов. Она показывает каждый запрос пакета отдельной вкладкой и его результат, умеет замерять время выполнения и строить план. Собирать и проверять пакет удобнее сначала в ней, а в модуль переносить уже готовый текст.
При ошибке платформа сообщает, в каком запросе пакета она произошла, и приводит фрагмент текста. Частые причины падения пакета:
- 🔸 обращение к временной таблице, которую забыли создать раньше по тексту;
- 🔸 попытка прочитать таблицу после
УНИЧТОЖИТЬ; - 🔸 разные имена полей при
ПОМЕСТИТЬи при чтении; - 🔸 потерянная точка с запятой между запросами — два запроса слиплись в один.
Практические примеры пакетных запросов
Пример 1. Остатки с ценами в два шага. Сначала остатки на складе кладём во временную таблицу, затем соединяем с актуальными ценами.
ВЫБРАТЬ
Остатки.Номенклатура КАК Номенклатура,
Остатки.КоличествоОстаток КАК Количество
ПОМЕСТИТЬ ВТ_Остатки
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(&Период, Склад = &Склад) КАК Остатки
ИНДЕКСИРОВАТЬ ПО
Номенклатура
;
ВЫБРАТЬ
ВТ_Остатки.Номенклатура КАК Номенклатура,
ВТ_Остатки.Количество КАК Количество,
Цены.Цена КАК Цена
ИЗ
ВТ_Остатки КАК ВТ_Остатки
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период) КАК Цены
ПО Цены.Номенклатура = ВТ_Остатки.Номенклатура
Виртуальные таблицы Остатки и СрезПоследних разведены по шагам, между ними — индексированная временная таблица. Это и есть правильная форма.
Пример 2. Один список — два результата. Отобрали контрагентов-должников один раз, затем из этого промежутка построили и список, и свод по менеджерам, вызвав ВыполнитьПакет() и забрав два последних результата массива.
Пример 3. Проверка перед обработкой. Первый запрос пакета собирает строки, которые не проходят контроль (например, товары без цены). Если его результат не пуст — обработку останавливают и показывают список проблем. Пустой результат первого запроса проверяют методом Пустой().
Типичные ошибки и как их избежать
Ошибки пакета обычно не в синтаксисе, а в логике и производительности.
- ❌ Временная таблица без индекса. Соединение по неиндексированному полю временной таблицы сводит на нет весь выигрыш. Правило простое: есть соединение или отбор по полю — есть индекс по нему.
- ❌ Соединение виртуальной таблицы напрямую. Остатки и обороты прогоняйте через свою временную таблицу, не соединяйте с виртуальной в лоб.
- ❌ Запрос в цикле вместо пакета. Самая дорогая ошибка. Вместо обращения к базе на каждой итерации соберите все данные одним пакетом и обходите готовую выборку.
- ❌ Чтение не того результата. После
ВыполнитьПакет()берут результат по индексу. Легко ошибиться и взять промежуточный пустой вместо итогового — считайте индекс отКоличество() - 1. - ❌ Открытая транзакция на время расчёта. Длинные вычисления внутри
НачатьТранзакцию()блокируют других. Транзакция — только вокруг быстрых чтений и записей.
Когда пакет не нужен: альтернативы
Пакет — не универсальный инструмент. В части задач он лишний или не подходит вовсе.
- Если выборка одношаговая и соединений немного, хватит обычного запроса. Дробить его на пакет ради «красоты» не нужно — лишние временные таблицы тоже стоят ресурсов.
- Если задача — изменить данные (провести документ , записать регистр, пометить на удаление), запрос не подходит в принципе: он только читает. Используйте объектную модель и наборы записей, при необходимости — в транзакции.
- Для готовых отчётов со сложной группировкой и итогами логичнее система компоновки данных (СКД): она сама строит эффективные запросы и управляет временными таблицами.
- Для разовой выгрузки «посмотреть» не нужен код вообще — хватит консоли запросов.
Ориентир такой: пакет оправдан там, где есть несколько шагов чтения, зависящих друг от друга. Для одного шага — обычный запрос, для записи — объекты, для отчёта — СКД.
Частые вопросы
Чем `Выполнить()` отличается от `ВыполнитьПакет()` на пакете?
Выполнить() возвращает результат только последнего запроса пакета. ВыполнитьПакет() возвращает массив результатов — по одному на каждый запрос, в порядке их следования. Если нужны промежутки — берите второй метод.Почему результат запроса с `ПОМЕСТИТЬ` пустой?
ВыполнитьПакетСПромежуточнымиДанными().Сколько запросов можно поместить в один пакет?
Можно ли пакетом изменить данные в базе?
Нужно ли вручную уничтожать временные таблицы?
УНИЧТОЖИТЬ применяют, когда таблица большая и память нужно освободить до конца пакета.