Пакетный запрос 1С: временные таблицы, ВыполнитьПакет и отладка

· 10 мин

Содержание

Пакет запросов в 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.
  • ❌ Открытая транзакция на время расчёта. Длинные вычисления внутри НачатьТранзакцию() блокируют других. Транзакция — только вокруг быстрых чтений и записей.
Перед вставкой пакета в рабочий модуль
Выполнено: 0 / 6

Когда пакет не нужен: альтернативы

Пакет — не универсальный инструмент. В части задач он лишний или не подходит вовсе.

  • Если выборка одношаговая и соединений немного, хватит обычного запроса. Дробить его на пакет ради «красоты» не нужно — лишние временные таблицы тоже стоят ресурсов.
  • Если задача — изменить данные (провести документ , записать регистр, пометить на удаление), запрос не подходит в принципе: он только читает. Используйте объектную модель и наборы записей, при необходимости — в транзакции.
  • Для готовых отчётов со сложной группировкой и итогами логичнее система компоновки данных (СКД): она сама строит эффективные запросы и управляет временными таблицами.
  • Для разовой выгрузки «посмотреть» не нужен код вообще — хватит консоли запросов.

Ориентир такой: пакет оправдан там, где есть несколько шагов чтения, зависящих друг от друга. Для одного шага — обычный запрос, для записи — объекты, для отчёта — СКД.

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

Чем `Выполнить()` отличается от `ВыполнитьПакет()` на пакете?
Выполнить() возвращает результат только последнего запроса пакета. ВыполнитьПакет() возвращает массив результатов — по одному на каждый запрос, в порядке их следования. Если нужны промежутки — берите второй метод.
Почему результат запроса с `ПОМЕСТИТЬ` пустой?
Так и задумано: данные ушли во временную таблицу, а не в выборку. Чтобы увидеть содержимое временной таблицы при отладке, используйте ВыполнитьПакетСПромежуточнымиДанными().
Сколько запросов можно поместить в один пакет?
Жёсткого предела нет, ограничение практическое — читаемость и память под временные таблицы. Если пакет разросся до десятков шагов, это обычно сигнал вынести часть логики в отдельные запросы с менеджером временных таблиц.
Можно ли пакетом изменить данные в базе?
Нет. Язык запросов 1С только читает. Для изменения данных применяют объектную модель (запись объектов, наборов записей), при необходимости обёрнутую в транзакцию.
Нужно ли вручную уничтожать временные таблицы?
Обычно нет — платформа очищает их по завершении запроса. Оператор УНИЧТОЖИТЬ применяют, когда таблица большая и память нужно освободить до конца пакета.