Большой запрос 1С по частям: временные таблицы, пакет, порции

· 10 мин

Содержание

Разделить запрос в 1С проще всего так: вынести каждый логический шаг в отдельную временную таблицу через ПОМЕСТИТЬ, собрать шаги в пакет через ; и выполнить их одним вызовом Запрос.Выполнить() или Запрос.ВыполнитьПакет(). Если данных слишком много даже для такого пакета, запрос дробят по времени: по месяцам или порциями по N строк.

Ниже пять рабочих способов для 1С 8.3, от самого частого к узкоспециальному. Все они относятся к языку запросов платформы и не зависят от конфигурации: одинаково работают в БП 3.0, ЗУП 3.1, УТ 11 и самописных базах.

Зачем дробить запрос и когда это даёт эффект

Один «монолитный» запрос на 200 строк с пятью соединениями и тремя вложенными подзапросами — частая картина в старых доработках. Он работает, пока данных мало. Потом отчёт начинает формироваться минутами.

Причины, по которым разделение помогает:

  • 🐢 Оптимизатор СУБД ошибается на сложных соединениях. Когда в запросе соединяются подзапросы, СУБД не знает, сколько строк они вернут, и выбирает плохой план. Временная таблица — это уже готовый набор строк, по нему статистика есть.
  • 🔁 Один и тот же фрагмент считается несколько раз. Если подзапрос с остатками используется в двух местах, без временной таблицы он выполнится дважды.
  • 🔍 Монолит невозможно отлаживать. Когда запрос разбит на шаги, промежуточный результат каждого шага видно в консоли запросов.
  • 🔒 Долгий запрос дольше держит блокировки. В транзакции (при проведении документа) это напрямую бьёт по другим пользователям.

Когда делить не нужно:

  • запрос простой, с одной таблицей и фильтром — разбиение только добавит накладные расходы на создание временных таблиц;
  • тормозит не запрос, а код после него: обход выборки с обращением к реквизитам через точку, запись объектов в цикле;
  • проблема в отсутствии индекса или в фильтре по реквизиту без индекса — это лечится настройкой индексирования реквизита, а не переписыванием запроса.

Перед переделкой замерьте исходное время. Включите замер производительности (Отладка → Замер производительности в конфигураторе) или просто засеките время формирования отчёта на одном и том же периоде. Без цифры «до» нельзя сказать, помогла ли переделка.

Способ 1. Разбить монолит на логические шаги

Первый шаг — не код, а схема. Возьмите большой запрос и выпишите, из каких смысловых кусков он состоит. Обычно получается 3–5 блоков:

  1. Отбор «ключей» — какие товары, контрагенты или документы нас вообще интересуют.
  2. Сбор показателей — остатки, обороты, цены по этим ключам.
  3. Сведение — соединение показателей между собой.
  4. Итоговая выборка — группировка, сортировка, вычисляемые поля для вывода.

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

Пример плохой структуры: соединение всех остатков со всеми ценами и фильтр по группе в самом конце, в ГДЕ итоговой выборки. СУБД честно перемножает десятки тысяч строк, чтобы потом выбросить 99% из них.

Способ 2. Временные таблицы и менеджер временных таблиц

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

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

Запрос.УстановитьПараметр("Группа", Группа);
Запрос.УстановитьПараметр("Склад", Склад);
Запрос.УстановитьПараметр("Дата", Новый Граница(ДатаОтчета, ВидГраницы.Включая));

Выборка = Запрос.Выполнить().Выбрать();

Имя регистра в примере условное: в вашей конфигурации подставьте свой регистр остатков. Структура приёма от этого не меняется.

Что здесь важно:

  • 📌 Фильтр передан в параметры виртуальной таблицы Остатки(...), а не в ГДЕ. Так платформа отбирает данные до расчёта остатков, а не после.
  • 📌 ИНДЕКСИРОВАТЬ ПО имеет смысл, когда временная таблица потом участвует в соединении или условии В и строк в ней много (тысячи и больше). Для таблицы на 20 строк индекс не нужен.
  • 📌 Менеджер временных таблиц нужен, если временные таблицы должны пережить один вызов Выполнить() и использоваться в другом объекте Запрос. Внутри одного пакета они видны и без него.

Как передать временную таблицу в другой запрос

Частая задача: в одной процедуре подготовили набор документов, в другой по нему считаем. Для этого оба запроса используют один и тот же менеджер:

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

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

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

Временная таблица живёт, пока жив менеджер. Когда переменная МВТ выходит из области видимости, таблицы удаляются. Если менеджер живёт долго (например, в длинной обработке), освобождайте ненужные таблицы явно запросом УНИЧТОЖИТЬ ВТ_Документы.

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

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

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

У колонок таблицы значений должны быть заданы типы через ОписаниеТипов при создании колонок. Без типов запрос не поймёт, что лежит в колонке, и выдаст ошибку.

Способ 3. Пакет запросов: несколько результатов за один вызов

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

Есть два метода выполнения:

МетодЧто возвращаетКогда использовать
Выполнить()Результат только последнего запроса пакетаПромежуточные шаги нужны лишь для расчёта итога
ВыполнитьПакет()Массив результатов всех запросов пакетаНужно несколько независимых выборок (шапка + табличная часть, итоги + детали)
ВыполнитьПакетСПромежуточнымиДанными()Массив результатов, включая содержимое временных таблицОтладка: посмотреть, что легло в каждую ВТ

Пример с ВыполнитьПакет() — печатная форма, где нужны и шапка, и строки:

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

Результаты = Запрос.ВыполнитьПакет();
ВыборкаШапки = Результаты[0].Выбрать();
ВыборкаСтрок = Результаты[1].Выбрать();

Индексы в массиве идут с нуля и строго по порядку запросов в тексте. Если в пакете перед нужной выборкой стоят запросы с ПОМЕСТИТЬ, они тоже занимают свои места в массиве. Добавили в начало ещё одну временную таблицу — все индексы сдвинулись. Для надёжности держите выборки для вывода в конце пакета и берите их от конца: Результаты[Результаты.ВГраница()].

Один пакет — это один обмен с сервером СУБД вместо нескольких. На файловой базе разница почти не видна, в клиент-серверной при сотнях вызовов в цикле она ощутима.

Способ 4. Вложенный запрос в источнике данных

Вложенный запрос в секции ИЗ — это «временная таблица без имени»: подзапрос пишется прямо в скобках вместо имени таблицы.

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

Здесь вложенный запрос уместен: он один, ни с чем не соединяется, а внешний запрос лишь фильтрует результат группировки.

Проблемы начинаются, когда вложенный запрос соединяется с другой таблицей или другим подзапросом. Индекса у подзапроса нет, оценки числа строк тоже. На больших объёмах СУБД нередко выбирает соединение вложенными циклами — и запрос, который на тестовой базе шёл секунду, на рабочей идёт десять минут.

Практическое правило:

  • ✅ подзапрос в ИЗ без соединений или в условии В (ВЫБРАТЬ ...) — допустимо;
  • ⚠️ СОЕДИНЕНИЕ с вложенным запросом — переписывайте на временную таблицу с индексом по полю соединения;
  • ❌ соединение двух вложенных запросов между собой — переписывайте обязательно.

Способ 5. Делить по времени: периоды и порции

Когда объём данных огромный (движения за несколько лет, миллионы строк), даже хороший пакет может упереться в память сервера или в таймаут. Тогда делят не текст запроса, а сами данные.

По периодам

Запрос выполняется в цикле по месяцам, результат каждого месяца сразу обрабатывается или выгружается:

ТекНачало = НачалоМесяца(ДатаНачала);
Пока ТекНачало <= ДатаОкончания Цикл

	ТекКонец = Мин(КонецМесяца(ТекНачало), ДатаОкончания);

	Запрос.УстановитьПараметр("Начало", ТекНачало);
	Запрос.УстановитьПараметр("Конец", ТекКонец);
	Выборка = Запрос.Выполнить().Выбрать();

	Пока Выборка.Следующий() Цикл
		// обработка строк одного месяца
	КонецЦикла;

	ТекНачало = ДобавитьМесяц(ТекНачало, 1);

КонецЦикла;

Объект Запрос с текстом создаётся один раз до цикла, меняются только параметры. Это не тот «запрос в цикле», от которого предостерегают: за год получится 12 вызовов, а не по одному на каждую строку документа.

Порциями по N строк

Для обработок, которые что-то меняют в базе (перепроведение, заполнение нового реквизита), удобнее порции. Берут ВЫБРАТЬ ПЕРВЫЕ 1000, обрабатывают каждый объект в своей транзакции и повторяют, пока запрос что-то возвращает. Чтобы не выбирать одно и то же повторно, в условие добавляют признак «ещё не обработан»: пустое значение нового реквизита или отсутствие записи в служебном регистре.

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

Порции решают две задачи: обработка не держит одну огромную транзакцию с блокировками, а при сбое её можно перезапустить с места остановки.

Какой способ разделения подходит вашему запросу?

Сравнение способов в одной таблице

СпособПлюсыМинусы и риски
Логические шагиПонятный запрос, сильный фильтр в началеТребует переписать запрос целиком
Временные таблицыИндекс, повторное использование, отладкаНагрузка на временную область СУБД при огромных ВТ
Пакет запросовОдин вызов, несколько результатовИндексы результатов сдвигаются при правке
Вложенный запрос в ИЗКомпактно, без лишних ВТПлохие планы при соединениях
Периоды и порцииРаботает на любых объёмах, перезапускБольше вызовов, сложнее код

На практике способы комбинируют: запрос делят на шаги, шаги оформляют временными таблицами в одном пакете, а при совсем больших объёмах этот пакет крутят в цикле по месяцам.

Ошибки, которые съедают выигрыш от разделения

  • 🚫 Таблица не найдена "ВТ_...". Временная таблица создана одним объектом Запрос, а читается другим без общего менеджера временных таблиц. Либо имя написано с опечаткой.
  • 🚫 Фильтр по виртуальной таблице в ГДЕ. Временную таблицу сделали, а остатки по-прежнему считаются по всем складам и всей номенклатуре. Условие должно стоять в параметрах Остатки(...) или Обороты(...).
  • 🚫 Индекс на всё подряд. ИНДЕКСИРОВАТЬ ПО по пяти полям для таблицы на 50 строк только тратит время на построение индекса.
  • 🚫 Разыменование через точку в итоговой выборке. ВТ.Номенклатура.Родитель.Наименование порождает дополнительные соединения. Нужные реквизиты лучше забрать ещё на шаге формирования ВТ.
  • 🚫 РАЗЛИЧНЫЕ вместо правильного соединения. Если после разделения появились дубли строк, ошибка в условии соединения. ВЫБРАТЬ РАЗЛИЧНЫЕ её маскирует и добавляет сортировку.
  • 🚫 Порции без порядка. ВЫБРАТЬ ПЕРВЫЕ 1000 без условия «ещё не обработан» на каждой итерации может возвращать одни и те же строки. Обработка зацикливается.

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

Сколько временных таблиц можно создать в одном пакете?
Жёсткого практического предела нет, пакеты из 10–20 временных таблиц встречаются в типовых конфигурациях. Ограничение одно — объём: каждая ВТ занимает место на сервере СУБД, пока жив менеджер.
Ускоряет ли разделение запрос в файловой базе?
Да, если исходный запрос соединял вложенные запросы или считал одно и то же дважды. Выигрыш от «одного обращения к серверу» в файловом режиме почти незаметен, а выигрыш от правильного плана остаётся.
Чем временная таблица отличается от таблицы значений в коде?
Временная таблица живёт на стороне СУБД и участвует в запросах как обычная таблица: её можно соединять, индексировать, группировать. Таблица значений живёт в памяти сервера 1С, и для использования в запросе её всё равно передают параметром и кладут во временную таблицу.
Можно ли разделить запрос в схеме компоновки данных?
Можно. В наборе данных типа «Запрос» в СКД допустим пакетный текст с временными таблицами, итоговым набором считается последний запрос пакета.
Как посмотреть, что легло в каждую временную таблицу?
Выполните пакет методом ВыполнитьПакетСПромежуточнымиДанными() или воспользуйтесь консолью запросов, которая показывает содержимое временных таблиц. В рабочем коде этот метод не оставляйте: он выгружает лишние данные.