Большой запрос 1С по частям: временные таблицы, пакет, порции
Содержание
Разделить запрос в 1С проще всего так: вынести каждый логический шаг в отдельную временную таблицу через ПОМЕСТИТЬ, собрать шаги в пакет через ; и выполнить их одним вызовом Запрос.Выполнить() или Запрос.ВыполнитьПакет(). Если данных слишком много даже для такого пакета, запрос дробят по времени: по месяцам или порциями по N строк.
Ниже пять рабочих способов для 1С 8.3, от самого частого к узкоспециальному. Все они относятся к языку запросов платформы и не зависят от конфигурации: одинаково работают в БП 3.0, ЗУП 3.1, УТ 11 и самописных базах.
Зачем дробить запрос и когда это даёт эффект
Один «монолитный» запрос на 200 строк с пятью соединениями и тремя вложенными подзапросами — частая картина в старых доработках. Он работает, пока данных мало. Потом отчёт начинает формироваться минутами.
Причины, по которым разделение помогает:
- 🐢 Оптимизатор СУБД ошибается на сложных соединениях. Когда в запросе соединяются подзапросы, СУБД не знает, сколько строк они вернут, и выбирает плохой план. Временная таблица — это уже готовый набор строк, по нему статистика есть.
- 🔁 Один и тот же фрагмент считается несколько раз. Если подзапрос с остатками используется в двух местах, без временной таблицы он выполнится дважды.
- 🔍 Монолит невозможно отлаживать. Когда запрос разбит на шаги, промежуточный результат каждого шага видно в консоли запросов.
- 🔒 Долгий запрос дольше держит блокировки. В транзакции (при проведении документа) это напрямую бьёт по другим пользователям.
Когда делить не нужно:
- запрос простой, с одной таблицей и фильтром — разбиение только добавит накладные расходы на создание временных таблиц;
- тормозит не запрос, а код после него: обход выборки с обращением к реквизитам через точку, запись объектов в цикле;
- проблема в отсутствии индекса или в фильтре по реквизиту без индекса — это лечится настройкой индексирования реквизита, а не переписыванием запроса.
Перед переделкой замерьте исходное время. Включите замер производительности (Отладка → Замер производительности в конфигураторе) или просто засеките время формирования отчёта на одном и том же периоде. Без цифры «до» нельзя сказать, помогла ли переделка.
Способ 1. Разбить монолит на логические шаги
Первый шаг — не код, а схема. Возьмите большой запрос и выпишите, из каких смысловых кусков он состоит. Обычно получается 3–5 блоков:
- Отбор «ключей» — какие товары, контрагенты или документы нас вообще интересуют.
- Сбор показателей — остатки, обороты, цены по этим ключам.
- Сведение — соединение показателей между собой.
- Итоговая выборка — группировка, сортировка, вычисляемые поля для вывода.
Каждый блок становится отдельным запросом. Ключевое правило: первым идёт самый сильный фильтр. Если отчёт строится по одному складу и одной группе товаров, сначала отбираем 300 нужных позиций, а уже потом считаем по ним остатки, а не наоборот.
Пример плохой структуры: соединение всех остатков со всеми ценами и фильтр по группе в самом конце, в ГДЕ итоговой выборки. СУБД честно перемножает десятки тысяч строк, чтобы потом выбросить 99% из них.
Способ 2. Временные таблицы и менеджер временных таблиц
Это основной инструмент. Результат промежуточного запроса кладётся во временную таблицу конструкцией ПОМЕСТИТЬ ИмяТаблицы, а следующий запрос берёт из неё данные как из обычной таблицы.
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Номенклатура
|ПОМЕСТИТЬ ВТ_Товары
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Ссылка В ИЕРАРХИИ(&Группа)
| И НЕ Номенклатура.ЭтоГруппа
|
|ИНДЕКСИРОВАТЬ ПО
| Номенклатура
|;
|
|////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Остатки.Номенклатура КАК Номенклатура,
| Остатки.КоличествоОстаток КАК Количество
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(
| &Дата,
| Склад = &Склад
| И Номенклатура В
| (ВЫБРАТЬ
| ВТ.Номенклатура
| ИЗ
| ВТ_Товары КАК ВТ)) КАК Остатки";
Запрос.УстановитьПараметр("Группа", Группа);
Запрос.УстановитьПараметр("Склад", Склад);
Запрос.УстановитьПараметр("Дата", Новый Граница(ДатаОтчета, ВидГраницы.Включая));
Выборка = Запрос.Выполнить().Выбрать();
Имя регистра в примере условное: в вашей конфигурации подставьте свой регистр остатков. Структура приёма от этого не меняется.
Что здесь важно:
- 📌 Фильтр передан в параметры виртуальной таблицы
Остатки(...), а не вГДЕ. Так платформа отбирает данные до расчёта остатков, а не после. - 📌
ИНДЕКСИРОВАТЬ ПОимеет смысл, когда временная таблица потом участвует в соединении или условииВи строк в ней много (тысячи и больше). Для таблицы на 20 строк индекс не нужен. - 📌 Менеджер временных таблиц нужен, если временные таблицы должны пережить один вызов
Выполнить()и использоваться в другом объектеЗапрос. Внутри одного пакета они видны и без него.
Как передать временную таблицу в другой запрос
Частая задача: в одной процедуре подготовили набор документов, в другой по нему считаем. Для этого оба запроса используют один и тот же менеджер:
МВТ = Новый МенеджерВременныхТаблиц;
Запрос1 = Новый Запрос;
Запрос1.МенеджерВременныхТаблиц = МВТ;
Запрос1.Текст = "ВЫБРАТЬ ... ПОМЕСТИТЬ ВТ_Документы ИЗ ...";
Запрос1.Выполнить();
Запрос2 = Новый Запрос;
Запрос2.МенеджерВременныхТаблиц = МВТ;
Запрос2.Текст = "ВЫБРАТЬ ... ИЗ ВТ_Документы КАК ВТ ...";
Результат = Запрос2.Выполнить();
Временная таблица живёт, пока жив менеджер. Когда переменная МВТ выходит из области видимости, таблицы удаляются. Если менеджер живёт долго (например, в длинной обработке), освобождайте ненужные таблицы явно запросом УНИЧТОЖИТЬ ВТ_Документы.
Таблица значений как источник
Временную таблицу можно заполнить и из таблицы значений, собранной в коде. Таблицу передают параметром и выбирают из него с ПОМЕСТИТЬ:
Запрос.Текст =
"ВЫБРАТЬ
| Т.Номенклатура КАК Номенклатура,
| Т.Количество КАК Количество
|ПОМЕСТИТЬ ВТ_Загрузка
|ИЗ
| &ТаблицаЗагрузки КАК Т";
Запрос.УстановитьПараметр("ТаблицаЗагрузки", ТаблицаЗначений);
У колонок таблицы значений должны быть заданы типы через ОписаниеТипов при создании колонок. Без типов запрос не поймёт, что лежит в колонке, и выдаст ошибку.
Способ 3. Пакет запросов: несколько результатов за один вызов
Пакет — это несколько запросов в одном тексте, разделённых точкой с запятой. В конструкторе запросов для этого есть вкладка Пакет запросов: каждый запрос пакета редактируется отдельно, а временные таблицы предыдущих шагов доступны в следующих как источники.
Есть два метода выполнения:
| Метод | Что возвращает | Когда использовать |
|---|---|---|
Выполнить() | Результат только последнего запроса пакета | Промежуточные шаги нужны лишь для расчёта итога |
ВыполнитьПакет() | Массив результатов всех запросов пакета | Нужно несколько независимых выборок (шапка + табличная часть, итоги + детали) |
ВыполнитьПакетСПромежуточнымиДанными() | Массив результатов, включая содержимое временных таблиц | Отладка: посмотреть, что легло в каждую ВТ |
Пример с ВыполнитьПакет() — печатная форма, где нужны и шапка, и строки:
Запрос.Текст =
"ВЫБРАТЬ
| Док.Номер КАК Номер,
| Док.Дата КАК Дата,
| Док.Контрагент КАК Контрагент
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Док
|ГДЕ
| Док.Ссылка = &Ссылка
|;
|
|////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Строки.НомерСтроки КАК НомерСтроки,
| Строки.Номенклатура КАК Номенклатура,
| Строки.Количество КАК Количество,
| Строки.Сумма КАК Сумма
|ИЗ
| Документ.РеализацияТоваровУслуг.Товары КАК Строки
|ГДЕ
| Строки.Ссылка = &Ссылка
|
|УПОРЯДОЧИТЬ ПО
| НомерСтроки";
Запрос.УстановитьПараметр("Ссылка", Ссылка);
Результаты = Запрос.ВыполнитьПакет();
ВыборкаШапки = Результаты[0].Выбрать();
ВыборкаСтрок = Результаты[1].Выбрать();
Индексы в массиве идут с нуля и строго по порядку запросов в тексте. Если в пакете перед нужной выборкой стоят запросы с ПОМЕСТИТЬ, они тоже занимают свои места в массиве. Добавили в начало ещё одну временную таблицу — все индексы сдвинулись. Для надёжности держите выборки для вывода в конце пакета и берите их от конца: Результаты[Результаты.ВГраница()].
Один пакет — это один обмен с сервером СУБД вместо нескольких. На файловой базе разница почти не видна, в клиент-серверной при сотнях вызовов в цикле она ощутима.
Способ 4. Вложенный запрос в источнике данных
Вложенный запрос в секции ИЗ — это «временная таблица без имени»: подзапрос пишется прямо в скобках вместо имени таблицы.
"ВЫБРАТЬ
| Продажи.Контрагент КАК Контрагент,
| Продажи.Сумма КАК Сумма
|ИЗ
| (ВЫБРАТЬ
| Док.Контрагент КАК Контрагент,
| СУММА(Док.СуммаДокумента) КАК Сумма
| ИЗ
| Документ.РеализацияТоваровУслуг КАК Док
| ГДЕ
| Док.Дата МЕЖДУ &Начало И &Конец
| И Док.Проведен
| СГРУППИРОВАТЬ ПО
| Док.Контрагент) КАК Продажи
|ГДЕ
| Продажи.Сумма > &Порог"
Здесь вложенный запрос уместен: он один, ни с чем не соединяется, а внешний запрос лишь фильтрует результат группировки.
Проблемы начинаются, когда вложенный запрос соединяется с другой таблицей или другим подзапросом. Индекса у подзапроса нет, оценки числа строк тоже. На больших объёмах СУБД нередко выбирает соединение вложенными циклами — и запрос, который на тестовой базе шёл секунду, на рабочей идёт десять минут.
Практическое правило:
- ✅ подзапрос в
ИЗбез соединений или в условииВ (ВЫБРАТЬ ...)— допустимо; - ⚠️
СОЕДИНЕНИЕс вложенным запросом — переписывайте на временную таблицу с индексом по полю соединения; - ❌ соединение двух вложенных запросов между собой — переписывайте обязательно.
Способ 5. Делить по времени: периоды и порции
Когда объём данных огромный (движения за несколько лет, миллионы строк), даже хороший пакет может упереться в память сервера или в таймаут. Тогда делят не текст запроса, а сами данные.
По периодам
Запрос выполняется в цикле по месяцам, результат каждого месяца сразу обрабатывается или выгружается:
ТекНачало = НачалоМесяца(ДатаНачала);
Пока ТекНачало <= ДатаОкончания Цикл
ТекКонец = Мин(КонецМесяца(ТекНачало), ДатаОкончания);
Запрос.УстановитьПараметр("Начало", ТекНачало);
Запрос.УстановитьПараметр("Конец", ТекКонец);
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
// обработка строк одного месяца
КонецЦикла;
ТекНачало = ДобавитьМесяц(ТекНачало, 1);
КонецЦикла;
Объект Запрос с текстом создаётся один раз до цикла, меняются только параметры. Это не тот «запрос в цикле», от которого предостерегают: за год получится 12 вызовов, а не по одному на каждую строку документа.
Порциями по N строк
Для обработок, которые что-то меняют в базе (перепроведение, заполнение нового реквизита), удобнее порции. Берут ВЫБРАТЬ ПЕРВЫЕ 1000, обрабатывают каждый объект в своей транзакции и повторяют, пока запрос что-то возвращает. Чтобы не выбирать одно и то же повторно, в условие добавляют признак «ещё не обработан»: пустое значение нового реквизита или отсутствие записи в служебном регистре.
Массовое перепроведение и изменение объектов делайте на копии базы или вне рабочего времени, предварительно сделав резервную копию. Порционная обработка в рабочей базе заметно нагружает сервер, а ошибка в условии отбора может изменить не те документы.
Порции решают две задачи: обработка не держит одну огромную транзакцию с блокировками, а при сбое её можно перезапустить с места остановки.
Сравнение способов в одной таблице
| Способ | Плюсы | Минусы и риски |
|---|---|---|
| Логические шаги | Понятный запрос, сильный фильтр в начале | Требует переписать запрос целиком |
| Временные таблицы | Индекс, повторное использование, отладка | Нагрузка на временную область СУБД при огромных ВТ |
| Пакет запросов | Один вызов, несколько результатов | Индексы результатов сдвигаются при правке |
Вложенный запрос в ИЗ | Компактно, без лишних ВТ | Плохие планы при соединениях |
| Периоды и порции | Работает на любых объёмах, перезапуск | Больше вызовов, сложнее код |
На практике способы комбинируют: запрос делят на шаги, шаги оформляют временными таблицами в одном пакете, а при совсем больших объёмах этот пакет крутят в цикле по месяцам.
Ошибки, которые съедают выигрыш от разделения
- 🚫
Таблица не найдена "ВТ_...". Временная таблица создана одним объектомЗапрос, а читается другим без общего менеджера временных таблиц. Либо имя написано с опечаткой. - 🚫 Фильтр по виртуальной таблице в
ГДЕ. Временную таблицу сделали, а остатки по-прежнему считаются по всем складам и всей номенклатуре. Условие должно стоять в параметрахОстатки(...)илиОбороты(...). - 🚫 Индекс на всё подряд.
ИНДЕКСИРОВАТЬ ПОпо пяти полям для таблицы на 50 строк только тратит время на построение индекса. - 🚫 Разыменование через точку в итоговой выборке.
ВТ.Номенклатура.Родитель.Наименованиепорождает дополнительные соединения. Нужные реквизиты лучше забрать ещё на шаге формирования ВТ. - 🚫
РАЗЛИЧНЫЕвместо правильного соединения. Если после разделения появились дубли строк, ошибка в условии соединения.ВЫБРАТЬ РАЗЛИЧНЫЕеё маскирует и добавляет сортировку. - 🚫 Порции без порядка.
ВЫБРАТЬ ПЕРВЫЕ 1000без условия «ещё не обработан» на каждой итерации может возвращать одни и те же строки. Обработка зацикливается.
Частые вопросы
Сколько временных таблиц можно создать в одном пакете?
Ускоряет ли разделение запрос в файловой базе?
Чем временная таблица отличается от таблицы значений в коде?
Можно ли разделить запрос в схеме компоновки данных?
Как посмотреть, что легло в каждую временную таблицу?
ВыполнитьПакетСПромежуточнымиДанными() или воспользуйтесь консолью запросов, которая показывает содержимое временных таблиц. В рабочем коде этот метод не оставляйте: он выгружает лишние данные.