К статье
Практический материал

Разработка и Конфигурация · 12 мин

Как разделить запрос в 1С: рабочие способы и примеры

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

Фраза «разделить запрос» в контексте 1С обычно означает одну из четырёх задач: выполнить несколько запросов одним пакетом, разбить большую выборку на порции, разделить длинный список значений, который не помещается в условие В(), или вынести часть логики во временные таблицы. Все эти задачи решаются штатными средствами языка запросов и объекта Запрос, но у каждого способа есть свои условия применения и ограничения.

Главная причина делить запросы — производительность и устойчивость кода. Один гигантский запрос с десятком соединений тяжело читать, отлаживать и оптимизировать, а выборка миллионов записей за один вызов может занять всю доступную память сервера. Разделение позволяет контролировать каждый этап, переиспользовать промежуточные результаты и не упираться в технические лимиты СУБД.

Ниже разобраны основные сценарии с примерами кода на встроенном языке 1С. Примеры применимы к управляемым приложениям на платформе 8.3; для обычных форм и более ранних версий синтаксис запроса тот же, отличается только контекст выполнения.

Как выполнить несколько запросов одним пакетом

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

Каждый запрос в пакете возвращает свой результат. Метод ВыполнитьПакет() возвращает массив результатов выборки в том порядке, в котором запросы записаны в тексте. Если запрос создаёт временную таблицу через ПОМЕСТИТЬ, на месте его результата будет Неопределено — это нормальное поведение, а не ошибка.

Запрос = Новый Запрос;

Запрос.Текст =

"ВЫБРАТЬ

| Товары.Ссылка КАК Номенклатура

|ИЗ

| Справочник.Номенклатура КАК Товары

|ГДЕ

| НЕ Товары.ПометкаУдаления

|;

|ВЫБРАТЬ

| Остатки.Номенклатура КАК Номенклатура,

| Остатки.КоличествоОстаток КАК Остаток

|ИЗ

| РегистрНакопления.ТоварыНаСкладах.Остатки КАК Остатки";

МассивРезультатов = Запрос.ВыполнитьПакет();

ВыборкаТоваров = МассивРезультатов[0].Выбрать();

ВыборкаОстатков = МассивРезультатов[1].Выбрать();

Типичная ошибка при работе с пакетом — перепутать индексы результатов после добавления или удаления одного из запросов. Чтобы снизить риск, держите запросы в пакете короткими и комментируйте порядок, либо используйте временные таблицы, где обращение идёт по имени таблицы, а не по индексу.

Как разбить сложный запрос через временные таблицы

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

Для хранения временных таблиц между несколькими объектами Запрос используется МенеджерВременныхТаблиц. Пока менеджер жив, созданные в нём таблицы доступны любому запросу, которому этот менеджер передан. Это позволяет разнести этапы по разным процедурам модуля.

Порядок работы с временными таблицами
  1. 1
    Создайте МенеджерВременныхТаблиц и передайте его объекту Запрос
  2. 2
    Первым запросом отберите данные во временную таблицу через ПОМЕСТИТЬ
  3. 3
    Следующими запросами используйте эту таблицу в соединениях и группировках
  4. 4
    Промежуточные этапы проверяйте отдельным запросом ВЫБРАТЬ * ИЗ ВременнаяТаблица
  5. 5
    После работы уничтожьте таблицы через УНИЧТОЖИТЬ или закройте менеджер
МенеджерВТ = Новый МенеджерВременныхТаблиц;

Запрос = Новый Запрос;

Запрос.МенеджерВременныхТаблиц = МенеджерВТ;

Запрос.Текст =

"ВЫБРАТЬ

| Реализация.Ссылка КАК Документ

|ПОМЕСТИТЬ ВТ_Документы

|ИЗ

| Документ.РеализацияТоваровУслуг КАК Реализация

|ГДЕ

| Реализация.Дата МЕЖДУ &НачалоПериода И &КонецПериода

| И Реализация.Проведен";

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

Запрос.Текст =

"ВЫБРАТЬ

| Товары.Ссылка КАК Документ,

| СУММА(Товары.Сумма) КАК Сумма

|ИЗ

| ВТ_Документы КАК ВТ

| ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг.Товары КАК Товары

| ПО Товары.Ссылка = ВТ.Документ

|СГРУППИРОВАТЬ ПО

| Товары.Ссылка";

Результат = Запрос.Выполнить();

Как разделить выборку большого результата на порции

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

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

ТекущаяДата = НачалоПериода;

Пока ТекущаяДата <= КонецПериода Цикл

Запрос.УстановитьПараметр("Начало", ТекущаяДата);

Запрос.УстановитьПараметр("Конец", КонецДня(ТекущаяДата));

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

Пока Выборка.Следующий() Цикл

// обработка строки порции

КонецЦикла;

ТекущаяДата = КонецДня(ТекущаяДата) + 1;

КонецЦикла;

Если данные в периоде распределены неравномерно (в один день тысяча документов, в другой — миллион), делите по ссылке: сортировка по ссылке стабильна, и порцию можно ограничить конструкцией ПЕРВЫЕ N с условием по последнему значению предыдущей итерации.

Как разделить длинный список значений в условии запроса

Передача огромного массива в условие В(&Список) — частая причина проблем. На MS SQL Server есть ограничение на число элементов в IN-списке при определённых способах формирования текста, а на любой СУБД очень длинный список ухудшает план выполнения. Практическое правило: если элементов больше нескольких тысяч, список нужно разделить.

Есть два рабочих подхода. Первый — разбить массив на части фиксированного размера (например, по 1000 элементов) и выполнить запрос в цикле, объединяя результаты. Второй, обычно предпочтительный, — поместить весь список во временную таблицу через таблицу значений-параметр и соединиться с ней: СУБД обрабатывает соединение эффективнее, чем гигантский IN.

ТаблицаКлючей = Новый ТаблицаЗначений;

ТаблицаКлючей.Колонки.Добавить("Номенклатура",

Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));

Для Каждого Элемент Из БольшойМассив Цикл

ТаблицаКлючей.Добавить().Номенклатура = Элемент;

КонецЦикла;

Запрос.УстановитьПараметр("ТаблицаКлючей", ТаблицаКлючей);

Запрос.Текст =

"ВЫБРАТЬ Т.Номенклатура

|ПОМЕСТИТЬ ВТ_Отбор

|ИЗ &ТаблицаКлючей КАК Т

|ИНДЕКСИРОВАТЬ ПО Т.Номенклатура

|;

|ВЫБРАТЬ Остатки.Номенклатура, Остатки.КоличествоОстаток

|ИЗ РегистрНакопления.ТоварыНаСкладах.Остатки КАК Остатки

|ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Отбор КАК Отбор

| ПО Отбор.Номенклатура = Остатки.Номенклатура";

Сравнение вариантов разделения списка

Цикл по частям массива

Простая реализация, но много обращений к базе. Подходит для редких операций и списков до десятков тысяч элементов.

Временная таблица из таблицы значений

Одно обращение к базе, эффективное соединение. Предпочтительный вариант для больших списков, требует точного описания типов колонок.

Условие В(&Список) целиком

Приемлемо только для сотен элементов. На больших объёмах риск ошибки СУБД и плохого плана запроса.

Как проверить, что разделение дало правильный результат

После разделения запроса на части главный риск — потерять или задвоить данные на стыках порций. Контрольный приём: сравнить итоговые количества. Выполните исходный запрос с КОЛИЧЕСТВО(*) и сумму количеств по всем порциям — значения должны совпадать.

Для пакетных запросов проверяйте, что число элементов в массиве результатов равно числу запросов в пакете, и что на месте запросов с ПОМЕСТИТЬ стоит Неопределено. Для временных таблиц перед финальным запросом выполните отладочный ВЫБРАТЬ из каждой промежуточной таблицы и убедитесь, что фильтры отработали как задумано.

Контрольные точки

Как проверить результат

Сумма строк по порциям
Норма

Равна КОЛИЧЕСТВО(*) исходного запроса без разделения

Тревожный признак

Меньше — потеряны граничные записи, больше — порции пересекаются

Границы порций по дате
Норма

Следующая порция начинается после конца предыдущей без пропуска

Тревожный признак

Записи на границе дня попадают в обе порции или ни в одну

Массив результатов пакета
Норма

Количество элементов равно числу запросов, типы соответствуют ожидаемым

Тревожный признак

Обращение по неверному индексу даёт ошибку или чужие данные

Временная таблица
Норма

Число строк соответствует условию отбора, есть индекс по полям соединения

Тревожный признак

Пустая таблица при заведомо существующих данных — ошибка в параметрах

Типичные ошибки при разделении запросов

Большинство проблем возникает не из-за синтаксиса, а из-за нарушения границ порций и неправильного управления временными таблицами. Ниже собраны ошибки, которые чаще всего встречаются на практике.

Типичные ошибки
01
Граница порции задана через >= и <= одновременно — записи на стыке обрабатываются дважды. Используйте строгое неравенство с одной стороны
02
Временная таблица не уничтожается, а менеджер живёт долго — растёт tempdb на СУБД и замедляются другие сеансы
03
Добавили запрос в середину пакета и не поправили индексы результатов — отчёт молча показывает чужие данные
04
Список из сотен тысяч элементов передан в В(&Список) целиком — ошибка СУБД или многоминутное выполнение
05
Порционная обработка без упорядочивания — при изменении данных между итерациями часть записей теряется
06
Соединение с временной таблицей без ИНДЕКСИРОВАТЬ ПО на большом объёме — полное сканирование и таймаут

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

Маршрут решения

Как выбрать способ разделения

Запрос трудно читать из-за вложенных подзапросовРазбейте на цепочку временных таблиц через МенеджерВременныхТаблиц
Нужно несколько независимых результатов за одно обращениеИспользуйте пакетный запрос с ВыполнитьПакет()
Результат содержит сотни тысяч строк и большеОбрабатывайте порциями по диапазону дат или по упорядоченному ключу
В условие В() передаётся массив больше нескольких тысяч элементовПоместите список во временную таблицу через таблицу значений и соединитесь с ней
Данные меняются во время обработкиФиксируйте срез в начале (временная таблица ключей) и работайте только с ним

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

Можно ли разделить текст запроса на части и собирать его строками?

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

Что возвращает пакетный запрос на месте запроса с ПОМЕСТИТЬ?

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

Какой размер порции выбрать при обработке больших данных?

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

Нужно ли уничтожать временные таблицы вручную?

Желательно, особенно в длительных операциях с несколькими этапами: конструкция УНИЧТОЖИТЬ ИмяТаблицы освобождает ресурсы СУБД сразу, а не в конце работы менеджера. В коротких процедурах, где менеджер создаётся и умирает локально, явное уничтожение необязательно.

Можно ли выполнять порции запроса параллельно через фоновые задания?

Можно, если порции независимы и не меняют одни и те же данные. Каждому фоновому заданию передаются свои границы диапазона. Учитывайте, что параллельные задания конкурируют за ресурсы сервера и могут вызвать блокировки при записи, поэтому для модифицирующих операций параллелизм нужно тестировать на копии базы.

Почему запрос с временной таблицей работает медленнее вложенного подзапроса?

Чаще всего из-за отсутствия индекса: добавьте ИНДЕКСИРОВАТЬ ПО по полям соединения. Также проверьте, не помещаете ли вы во временную таблицу лишние строки и колонки — чем меньше промежуточный набор, тем быстрее соединение.

Итог

Что важно запомнить

Разделение запроса в 1С — это выбор между пакетом, временными таблицами, порционной выборкой и разбиением списков значений. Всегда проверяйте итоговое количество строк на стыках порций, индексируйте временные таблицы и не передавайте огромные массивы в условие В() целиком.