Работа с большими объемами данных в 1С:Предприятие часто сталкивается с проблемой производительности. Один из ключевых инструментов оптимизации — правильное разделение запросов. Этот прием позволяет не только ускорить выполнение операций, но и сделать код более читаемым, а систему — стабильной. Однако не все разработчики знают, когда и как правильно дробить запросы, чтобы не потерять в функциональности.
В этой статье мы разберем 5 основных способов разделения запросов в 1С 8.3, их плюсы и минусы, а также типичные ошибки, которые допускают даже опытные программисты. Вы узнаете, как разделять сложные выборки на логические блоки, когда использовать временные таблицы, и почему иногда лучше оставить запрос цельным. Материал будет полезен как начинающим специалистам, так и тем, кто хочет систематизировать свои знания.
Почему нужно разделять запросы в 1С?
Основная причина — производительность. Чем сложнее запрос, тем дольше СУБД (обычно MS SQL Server или PostgreSQL) обрабатывает его. При этом:
- 🔹 Блокировки таблиц — длинные транзакции замораживают данные, создавая очереди для других пользователей
- 🔹 Потребление памяти — сложные JOIN'ы и подзапросы могут исчерпать ресурсы сервера
- 🔹 Таймауты — при превышении лимита времени выполнения запрос обрывается с ошибкой
- 🔹 Сложность отладки — монолитный запрос на 50+ строк труднее анализировать
К примеру, запрос с 10 соединениями таблиц и 5 подзапросами может выполняться 30 секунд, в то время как разбитый на 3 этапа — всего 8 секунд. Но важно понимать, что не всегда разделение дает выигрыш — иногда оптимизатор СУБД лучше справляется с цельным запросом.
⚠️ Внимание: Перед оптимизацией всегда замеряйте время выполнения исходного запроса и его частей с помощьюНачатьИзмерениеВремени()иЗакончитьИзмерениеВремени(). Некоторые "оптимизации" могут ухудшить производительность.
Способ 1: Разделение по логическим блокам
Самый простой метод — разбить запрос на независимые части, каждая из которых решает свою подзадачу. Например, вместо одного запроса на получение данных о клиентах, их заказах и остатках на складе, можно сделать три отдельных:
- Получить список активных клиентов
- Для каждого клиента выбрать заказы за последний месяц
- Проверить остатки товаров по этим заказам
Пример реализации:
// 1. Получаем клиентов
Запрос1 = Новый Запрос;
Запрос1.Текст =
"ВЫБРАТЬ
| Клиенты.Ссылка КАК Клиент,
| Клиенты.Наименование
|ИЗ
| Справочник.Клиенты КАК Клиенты
|ГДЕ
| Клиенты.ПометкаУдаления = ЛОЖЬ
| И Клиенты.ДатаПоследнегоЗаказа > &ДатаНачала";
Запрос1.УстановитьПараметр("ДатаНачала", НачалоМесяца(ТекущаяДата()));
Результат1 = Запрос1.Выполнить();
// 2. Для каждого клиента получаем заказы
Запрос2 = Новый Запрос;
Запрос2.Текст =
"ВЫБРАТЬ
| ЗаказыКлиентов.Ссылка КАК Заказ,
| ЗаказыКлиентов.Дата,
| ЗаказыКлиентов.Сумма
|ИЗ
| Документ.ЗаказКлиента КАК ЗаказыКлиентов
|ГДЕ
| ЗаказыКлиентов.Клиент В(&СписокКлиентов)
| И ЗаказыКлиентов.Дата > &ДатаНачала";
СписокКлиентов = Результат1.Выгрузить().ВыгрузитьКолонку("Клиент");
Запрос2.УстановитьПараметр("СписокКлиентов", СписокКлиентов);
Запрос2.УстановитьПараметр("ДатаНачала", НачалоМесяца(ТекущаяДата()));
Результат2 = Запрос2.Выполнить();
Такой подход особенно эффективен, когда:
- 📌 Результаты промежуточных запросов можно кешировать
- 📌 Логические блоки имеют разную частоту обновления (например, справочники меняются реже, чем документы)
- 📌 Нужно гибко управлять ошибками на каждом этапе
⚠️ Внимание: При таком разделении следите за состоянием транзакций. Если первый запрос прошел успешно, а второй упал с ошибкой, данные могут оказаться в несогласованном состоянии. ИспользуйтеНачатьТранзакцию()/ЗафиксироватьТранзакцию()осмотрительно.
Способ 2: Использование временных таблиц
Временные таблицы — мощный инструмент для оптимизации сложных запросов. Они позволяют:
- 🔧 Сохранять промежуточные результаты
- 🔧 Упрощать основной запрос за счет уменьшения вложенности
- 🔧 Повторно использовать данные без повторных выборок
Синтаксис создания временной таблицы в 1С 8.3:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1000
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.Артикул,
| Номенклатура.Наименование
|ПОМЕСТИТЬ ВТ_Номенклатура
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.ПометкаУдаления = ЛОЖЬ";
Запрос.Выполнить();
// Используем временную таблицу в основном запросе
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ВТ_Номенклатура.Артикул,
| СУММА(ОстаткиТоваров.КоличествоОстаток) КАК Остаток
|ИЗ
| ВТ_Номенклатура КАК ВТ_Номенклатура
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ОстаткиТоваров.Остатки КАК ОстаткиТоваров
| ПО ВТ_Номенклатура.Ссылка = ОстаткиТоваров.Номенклатура
|ГДЕ
| ОстаткиТоваров.Склад = &Склад
|СГРУППИРОВАТЬ ПО
| ВТ_Номенклатура.Артикул";
Запрос.УстановитьПараметр("Склад", ТекущийСклад);
Результат = Запрос.Выполнить();
Особенности работы с временными таблицами:
| Параметр | Описание | Ограничения |
|---|---|---|
| Область видимости | Доступны только в рамках текущего соединения | Не видны в других сеансах |
| Время жизни | Существуют до закрытия соединения или явного удаления | Автоматически удаляются при завершении сеанса |
| Производительность | Ускоряют повторные обращения к одним данным | Занимают память на сервере |
| Транзакции | Могут участвовать в транзакциях | При откате транзакции данные не восстанавливаются |
Для временных таблиц с большим объемом данных (100 000+ строк) создавайте индексы прямо в запросе с помощью конструкции ИНДЕКСИРОВАТЬ ПО. Это ускорит последующие соединения с ними.
Способ 3: Пакетные запросы
Пакетные запросы (или batch-запросы) позволяют отправить на сервер несколько SQL-команд в одном пакете. В 1С это реализуется через:
- 📦 Множественные операторы в одном тексте запроса (разделенные точкой с запятой)
- 📦 Использование объекта
ПакетЗапросов
Пример пакетного запроса:
Запрос = Новый Запрос;
Запрос.Текст =
"// Удаляем устаревшие записи
|УДАЛИТЬ ИЗ РегистрСведений.ЦеныНоменклатуры ГДЕ Дата < &ГраничнаяДата;
|
|// Обновляем актуальные цены
|ОБНОВИТЬ РегистрСведений.ЦеныНоменклатуры
|УСТАНОВИТЬ Цена = Цена * 1.1
|ГДЕ Номенклатура В (&СписокНоменклатуры)
| И Дата = &ТекущаяДата;
|
|// Добавляем новые записи
|ВСТАВИТЬ В РегистрСведений.ЦеныНоменклатуры (Номенклатура, Дата, Цена)
|ВЫБРАТЬ
| НоваяНоменклатура.Ссылка,
| &ТекущаяДата,
| НоваяНоменклатура.ЗакупочнаяЦена * 1.3
|ИЗ
| Справочник.Номенклатура КАК НоваяНоменклатура
|ГДЕ
| НоваяНоменклатура.Ссылка В (&НоваяНоменклатура)";
Запрос.УстановитьПараметр("ГраничнаяДата", НачалоГода(ТекущаяДата()));
Запрос.УстановитьПараметр("ТекущаяДата", ТекущаяДата());
Запрос.УстановитьПараметр("СписокНоменклатуры", СписокАктуальнойНоменклатуры);
Запрос.УстановитьПараметр("НоваяНоменклатура", СписокНовойНоменклатуры);
Результат = Запрос.Выполнить();
Преимущества пакетных запросов:
- ⚡ Снижение накладных расходов на установку соединения с СУБД
- 🔄 Возможность атомарного выполнения нескольких операций
- 📊 Упрощение кода за счет логического группирования операций
⚠️ Внимание: В пакетных запросах все операторы выполняются в рамках одной транзакции. Если любой из них завершится с ошибкой, откатятся изменения от всех предыдущих операторов пакета. Тестируйте такие запросы на резервной копии базы!
Убедиться, что все параметры установлены|Проверить права доступа на все таблицы|Создать резервную копию данных|Протестировать каждый оператор отдельно|Ограничить количество изменяемых записей-->
Способ 4: Подзапросы в секции FROM
Этот метод позволяет вынести сложную логику в отдельные подзапросы, которые затем используются как источники данных в основном запросе. Особенно полезен, когда нужно:
- 🔍 Применить разные условия фильтрации к одним данным
- 🔍 Выполнить агрегацию на промежуточном этапе
- 🔍 Соединить данные, полученные разными способами
Пример с агрегацией в подзапросе:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Клиенты.Наименование КАК Клиент,
| Заказы.КоличествоЗаказов,
| Заказы.СуммаЗаказов,
| Остатки.СуммаОстатка
|ИЗ
| Справочник.Клиенты КАК Клиенты
| ЛЕВОЕ СОЕДИНЕНИЕ (
| ВЫБРАТЬ
| ЗаказыКлиентов.Клиент КАК Клиент,
| КОЛИЧЕСТВО(*) КАК КоличествоЗаказов,
| СУММА(ЗаказыКлиентов.СуммаДокумента) КАК СуммаЗаказов
| ИЗ
| Документ.ЗаказКлиента КАК ЗаказыКлиентов
| ГДЕ
| ЗаказыКлиентов.Дата > &ДатаНачала
| СГРУППИРОВАТЬ ПО
| ЗаказыКлиентов.Клиент
| ) КАК Заказы ПО Клиенты.Ссылка = Заказы.Клиент
| ЛЕВОЕ СОЕДИНЕНИЕ (
| ВЫБРАТЬ
| РасчетыСКлиентами.Клиент КАК Клиент,
| СУММА(РасчетыСКлиентами.СуммаОстатка) КАК СуммаОстатка
| ИЗ
| РегистрБухгалтерии.РасчетыСКлиентами.Остатки(&ДатаКонца,) КАК РасчетыСКлиентами
| СГРУППИРОВАТЬ ПО
| РасчетыСКлиентами.Клиент
| ) КАК Остатки ПО Клиенты.Ссылка = Остатки.Клиент";
Запрос.УстановитьПараметр("ДатаНачала", НачалоКвартала(ТекущаяДата()));
Запрос.УстановитьПараметр("ДатаКонца", КонецДня(ТекущаяДата()));
Результат = Запрос.Выполнить();
Когда использовать этот метод:
| Сценарий | Пример | Альтернатива |
|---|---|---|
| Сложная агрегация | Расчет среднего чека по сегментам клиентов | Временные таблицы |
| Многоуровневая фильтрация | Выборка товаров с учетом остатков и цен | Последовательные запросы |
| Разные источники данных | Связь документов и регистров | JOIN с подзапросами |
Чем подзапросы в FROM отличаются от CTE (Common Table Expressions)?
В 1С 8.3 нет полноценной поддержки CTE (конструкции WITH), поэтому подзапросы в FROM — это основной способ создать "виртуальные таблицы" прямо в запросе. Однако они имеют ограничения:
1. Нельзя рекурсивно ссылаться на сам подзапрос
2. Сложнее читаются при глубокой вложенности
3. Могут дублировать вычисления при многократном использовании
Способ 5: Разделение по времени выполнения
Иногда целесообразно разделять запросы не по логике, а по временным характеристикам. Этот подход актуален для:
- ⏳ Длительных операций (более 5 секунд)
- 📅 Задач, выполняемых в фоне
- 🔄 Процессов с ручным контролем
Пример реализации с прогресс-баром:
// 1. Получаем общее количество записей для обработки
Запрос1 = Новый Запрос;
Запрос1.Текст = "ВЫБРАТЬ КОЛИЧЕСТВО(*) КАК Количество ИЗ Документ.ЗаказПоставщику";
ОбщееКоличество = Запрос1.Выполнить().Выгрузить()[0].Количество;
// 2. Обрабатываем данные пачками по 100 записей
Порция = 100;
Обработано = 0;
Пока Обработано < ОбщееКоличество Цикл
Запрос2 = Новый Запрос;
Запрос2.Текст =
"ВЫБРАТЬ ПЕРВЫЕ &Порция
| Заказы.Ссылка КАК Ссылка
|ИЗ
| Документ.ЗаказПоставщику КАК Заказы
|ГДЕ
| Заказы.Ссылка > &ПоследняяОбработанная
|УПОРЯДОЧИТЬ ПО
| Заказы.Ссылка";
Запрос2.УстановитьПараметр("Порция", Порция);
Запрос2.УстановитьПараметр("ПоследняяОбработанная", ПоследняяОбработаннаяСсылка);
Результат = Запрос2.Выполнить();
Если НЕ Результат.Пустой() Тогда
// Обработка порции данных
ОбработатьДокументы(Результат.Выгрузить());
Обработано = Обработано + Результат.ВыбранноеКоличество();
ПоследняяОбработаннаяСсылка = Результат.Выгрузить()[Результат.ВыбранноеКоличество()-1].Ссылка;
// Обновляем прогресс
Прогресс.УстановитьЗначение(Обработано / ОбщееКоличество * 100);
Иначе
Прервать;
КонецЕсли;
КонецЦикла;
Преимущества этого метода:
- 🎯 Возможность отображать прогресс выполнения
- 🛑 Легко прервать и возобновить процесс
- 📊 Контроль над потреблением ресурсов
Разделение по времени особенно важно для фоновых задач и обработок, которые запускаются по расписанию. Это позволяет избегать блокировок базы в рабочее время и равномерно распределять нагрузку.
Типичные ошибки при разделении запросов
Даже опытные разработчики допускают ошибки, которые сводят на нет все преимущества разделения. Вот самые распространенные:
- Избыточное разделение — когда запрос разбивается на слишком мелкие части, что приводит к:
- 🔄 Многократным обращениям к базе
- 🐢 Увеличению сетевого трафика
- 🧩 Потере целостности данных
- 🔄 Несогласованности данных
- 🚫 Потере изменений при сбоях
ПОЛНОЕ СОЕДИНЕНИЕ вместо ЛЕВОЕ СОЕДИНЕНИЕ)Пример неоптимального соединения:
// Плохо: полное соединение без необходимости
Запрос.Текст =
"ВЫБРАТЬ
| Товары.Наименование,
| Остатки.Количество
|ИЗ
| ВТ_Товары КАК Товары
| ПОЛНОЕ СОЕДИНЕНИЕ РегистрНакопления.ОстаткиТоваров.Остатки КАК Остатки
| ПО Товары.Ссылка = Остатки.Номенклатура";
// Хорошо: левое соединение с явной обработкой NULL
Запрос.Текст =
"ВЫБРАТЬ
| Товары.Наименование,
| ЕСТЬNULL(Остатки.Количество, 0) КАК Количество
|ИЗ
| ВТ_Товары КАК Товары
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ОстаткиТоваров.Остатки КАК Остатки
| ПО Товары.Ссылка = Остатки.Номенклатура";
FAQ: Ответы на частые вопросы
Как определить, что запрос нужно разделять?
Есть несколько признаков, что запрос стоит разбить:
- 🕒 Время выполнения превышает 3-5 секунд
- 📊 В плане выполнения видно полное сканирование таблиц (Table Scan)
- 🔄 Запрос содержит более 5 соединений (JOIN)
- 📝 Код запроса занимает более 30 строк
- 🔄 Есть повторяющиеся подзапросы
Используйте ОбъяснитьЗапрос() для анализа плана выполнения:
Объяснение = Запрос.ОбъяснитьЗапрос();
Объяснение.Вывести();
Можно ли разделять запросы в управляемых формах?
Да, но с осторожностью. В управляемых формах:
- 🔹 Используйте
ПриОткрытиидля предварительной загрузки данных во временные таблицы - 🔹 Разделяйте логику получения данных и отображения
- 🔹 Для динамических списков настройте отбор на сервере
Пример:
&НаСервере
Процедура ПриОткрытии(Отказ)
// Загружаем справочники при открытии формы
ЗагрузитьСправочники();
// Настраиваем динамический список
ЭлементыФормы.СписокТоваров.Запрос.Текст =
"ВЫБРАТЬ
| Товары.Ссылка КАК Ссылка,
| Товары.Наименование,
| Товары.Артикул
|ИЗ
| ВТ_Товары КАК Товары
|ГДЕ
| Товары.Группа = &ТекущаяГруппа";
КонецПроцедуры
Как разделять запросы при работе с большими регистрами (10 млн+ записей)?
Для крупных регистров:
- Используйте виртуальные таблицы с отборами по периодам
- Разделяйте обработку по периодам (день/неделя/месяц)
- Применяйте побитовую обработку для регистров сведений
- Настраивайте кластерные индексы на часто используемые поля
Пример работы с виртуальной таблицей:
Запрос.Текст =
"ВЫБРАТЬ
| ОстаткиТоваров.Номенклатура КАК Номенклатура,
| ОстаткиТоваров.КоличествоОстаток КАК Остаток
|ИЗ
| РегистрНакопления.ОстаткиТоваров.Остатки(
| &ДатаНачала,
| &ДатаКонца,
| Склад В (&СписокСкладов)
| ) КАК ОстаткиТоваров";
Влияет ли разделение запросов на лицензирование 1С?
Нет, разделение запросов не влияет на:
- 🔑 Количество лицензий 1С:Предприятие
- 📊 Лимиты клиентских соединений
- 💾 Объем используемой памяти
Однако косвенно может повлиять на:
- 🔧 Нагрузку на сервер 1С, что важно для облачных решений с ограничением по CPU
- 📡 Сетевой трафик между клиентом и сервером
⚠️ Внимание: В некоторых тарифах 1С:Fresh и 1С:ГISPRU могут действовать ограничения на длительные операции. Уточняйте условия в личном кабинете.
Как тестировать производительность после разделения?
Используйте комплексный подход:
- Замеры времени:
Начало = ТекущаяДата();// ... выполнение кода ...
Конец = ТекущаяДата();
Сообщить("Время выполнения: " + (Конец - Начало));
- Анализ плана выполнения через
ОбъяснитьЗапрос() - Тестирование под нагрузкой с помощью 1С:Тест-центр или JMeter
- Мониторинг ресурсов сервера (CPU, RAM, диски) через Перфоманс Монитор
Обращайте внимание на:
- 📈 Время ответа при пиковых нагрузках
- 🔄 Количество блокировок (
sys.dm_tran_locksв SQL Server) - 💾 Объем временных таблиц в
tempdb