Работа с большими объемами данных в 1С:Предприятие часто сталкивается с проблемой производительности. Один из ключевых инструментов оптимизации — правильное разделение запросов. Этот прием позволяет не только ускорить выполнение операций, но и сделать код более читаемым, а систему — стабильной. Однако не все разработчики знают, когда и как правильно дробить запросы, чтобы не потерять в функциональности.

В этой статье мы разберем 5 основных способов разделения запросов в 1С 8.3, их плюсы и минусы, а также типичные ошибки, которые допускают даже опытные программисты. Вы узнаете, как разделять сложные выборки на логические блоки, когда использовать временные таблицы, и почему иногда лучше оставить запрос цельным. Материал будет полезен как начинающим специалистам, так и тем, кто хочет систематизировать свои знания.

Почему нужно разделять запросы в 1С?

Основная причина — производительность. Чем сложнее запрос, тем дольше СУБД (обычно MS SQL Server или PostgreSQL) обрабатывает его. При этом:

  • 🔹 Блокировки таблиц — длинные транзакции замораживают данные, создавая очереди для других пользователей
  • 🔹 Потребление памяти — сложные JOIN'ы и подзапросы могут исчерпать ресурсы сервера
  • 🔹 Таймауты — при превышении лимита времени выполнения запрос обрывается с ошибкой
  • 🔹 Сложность отладки — монолитный запрос на 50+ строк труднее анализировать

К примеру, запрос с 10 соединениями таблиц и 5 подзапросами может выполняться 30 секунд, в то время как разбитый на 3 этапа — всего 8 секунд. Но важно понимать, что не всегда разделение дает выигрыш — иногда оптимизатор СУБД лучше справляется с цельным запросом.

⚠️ Внимание: Перед оптимизацией всегда замеряйте время выполнения исходного запроса и его частей с помощью НачатьИзмерениеВремени() и ЗакончитьИзмерениеВремени(). Некоторые "оптимизации" могут ухудшить производительность.

Способ 1: Разделение по логическим блокам

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

  1. Получить список активных клиентов
  2. Для каждого клиента выбрать заказы за последний месяц
  3. Проверить остатки товаров по этим заказам

Пример реализации:

// 1. Получаем клиентов

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

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

"ВЫБРАТЬ

| Клиенты.Ссылка КАК Клиент,

| Клиенты.Наименование

|ИЗ

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

|ГДЕ

| Клиенты.ПометкаУдаления = ЛОЖЬ

| И Клиенты.ДатаПоследнегоЗаказа > &ДатаНачала";

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

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

// 2. Для каждого клиента получаем заказы

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

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

"ВЫБРАТЬ

| ЗаказыКлиентов.Ссылка КАК Заказ,

| ЗаказыКлиентов.Дата,

| ЗаказыКлиентов.Сумма

|ИЗ

| Документ.ЗаказКлиента КАК ЗаказыКлиентов

|ГДЕ

| ЗаказыКлиентов.Клиент В(&СписокКлиентов)

| И ЗаказыКлиентов.Дата > &ДатаНачала";

СписокКлиентов = Результат1.Выгрузить().ВыгрузитьКолонку("Клиент");

Запрос2.УстановитьПараметр("СписокКлиентов", СписокКлиентов);

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

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

Такой подход особенно эффективен, когда:

  • 📌 Результаты промежуточных запросов можно кешировать
  • 📌 Логические блоки имеют разную частоту обновления (например, справочники меняются реже, чем документы)
  • 📌 Нужно гибко управлять ошибками на каждом этапе
⚠️ Внимание: При таком разделении следите за состоянием транзакций. Если первый запрос прошел успешно, а второй упал с ошибкой, данные могут оказаться в несогласованном состоянии. Используйте НачатьТранзакцию()/ЗафиксироватьТранзакцию() осмотрительно.
📊 Какой способ оптимизации запросов вы используете чаще?
Разделение по логическим блокам
Временные таблицы
Пакетные запросы
Подзапросы в FROM
Не оптимизирую

Способ 2: Использование временных таблиц

Временные таблицы — мощный инструмент для оптимизации сложных запросов. Они позволяют:

  • 🔧 Сохранять промежуточные результаты
  • 🔧 Упрощать основной запрос за счет уменьшения вложенности
  • 🔧 Повторно использовать данные без повторных выборок

Синтаксис создания временной таблицы в 1С 8.3:

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

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

"ВЫБРАТЬ ПЕРВЫЕ 1000

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

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

| Номенклатура.Наименование

|ПОМЕСТИТЬ ВТ_Номенклатура

|ИЗ

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

|ГДЕ

| Номенклатура.ПометкаУдаления = ЛОЖЬ";

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

// Используем временную таблицу в основном запросе

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

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

"ВЫБРАТЬ

| ВТ_Номенклатура.Артикул,

| СУММА(ОстаткиТоваров.КоличествоОстаток) КАК Остаток

|ИЗ

| ВТ_Номенклатура КАК ВТ_Номенклатура

| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ОстаткиТоваров.Остатки КАК ОстаткиТоваров

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

|ГДЕ

| ОстаткиТоваров.Склад = &Склад

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

| ВТ_Номенклатура.Артикул";

Запрос.УстановитьПараметр("Склад", ТекущийСклад);

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

Особенности работы с временными таблицами:

ПараметрОписаниеОграничения
Область видимостиДоступны только в рамках текущего соединенияНе видны в других сеансах
Время жизниСуществуют до закрытия соединения или явного удаленияАвтоматически удаляются при завершении сеанса
ПроизводительностьУскоряют повторные обращения к одним даннымЗанимают память на сервере
ТранзакцииМогут участвовать в транзакцияхПри откате транзакции данные не восстанавливаются
💡

Для временных таблиц с большим объемом данных (100 000+ строк) создавайте индексы прямо в запросе с помощью конструкции ИНДЕКСИРОВАТЬ ПО. Это ускорит последующие соединения с ними.

Способ 3: Пакетные запросы

Пакетные запросы (или batch-запросы) позволяют отправить на сервер несколько SQL-команд в одном пакете. В это реализуется через:

  • 📦 Множественные операторы в одном тексте запроса (разделенные точкой с запятой)
  • 📦 Использование объекта ПакетЗапросов

Пример пакетного запроса:

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

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

"// Удаляем устаревшие записи

|УДАЛИТЬ ИЗ РегистрСведений.ЦеныНоменклатуры ГДЕ Дата < &ГраничнаяДата;

|

|// Обновляем актуальные цены

|ОБНОВИТЬ РегистрСведений.ЦеныНоменклатуры

|УСТАНОВИТЬ Цена = Цена * 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);

Иначе

Прервать;

КонецЕсли;

КонецЦикла;

Преимущества этого метода:

  • 🎯 Возможность отображать прогресс выполнения
  • 🛑 Легко прервать и возобновить процесс
  • 📊 Контроль над потреблением ресурсов
💡

Разделение по времени особенно важно для фоновых задач и обработок, которые запускаются по расписанию. Это позволяет избегать блокировок базы в рабочее время и равномерно распределять нагрузку.

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

Даже опытные разработчики допускают ошибки, которые сводят на нет все преимущества разделения. Вот самые распространенные:

  1. Избыточное разделение — когда запрос разбивается на слишком мелкие части, что приводит к:
    • 🔄 Многократным обращениям к базе
    • 🐢 Увеличению сетевого трафика
    • 🧩 Потере целостности данных
  • Игнорирование транзакций — когда части одного логического действия выполняются в разных транзакциях, что может привести к:
    • 🔄 Несогласованности данных
    • 🚫 Потере изменений при сбоях
  • Неоптимальные соединения — когда временные таблицы или подзапросы соединяются неэффективными способами (например, через ПОЛНОЕ СОЕДИНЕНИЕ вместо ЛЕВОЕ СОЕДИНЕНИЕ)
  • Отсутствие индексов — когда временные таблицы или промежуточные результаты не индексируются, что сводит на нет выигрыш от разделения
  • Пример неоптимального соединения:

    // Плохо: полное соединение без необходимости
    

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

    "ВЫБРАТЬ

    | Товары.Наименование,

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

    |ИЗ

    | ВТ_Товары КАК Товары

    | ПОЛНОЕ СОЕДИНЕНИЕ РегистрНакопления.ОстаткиТоваров.Остатки КАК Остатки

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

    // Хорошо: левое соединение с явной обработкой NULL

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

    "ВЫБРАТЬ

    | Товары.Наименование,

    | ЕСТЬNULL(Остатки.Количество, 0) КАК Количество

    |ИЗ

    | ВТ_Товары КАК Товары

    | ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ОстаткиТоваров.Остатки КАК Остатки

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

    FAQ: Ответы на частые вопросы

    Как определить, что запрос нужно разделять?

    Есть несколько признаков, что запрос стоит разбить:

    • 🕒 Время выполнения превышает 3-5 секунд
    • 📊 В плане выполнения видно полное сканирование таблиц (Table Scan)
    • 🔄 Запрос содержит более 5 соединений (JOIN)
    • 📝 Код запроса занимает более 30 строк
    • 🔄 Есть повторяющиеся подзапросы

    Используйте ОбъяснитьЗапрос() для анализа плана выполнения:

    Объяснение = Запрос.ОбъяснитьЗапрос();
    

    Объяснение.Вывести();

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

    Да, но с осторожностью. В управляемых формах:

    • 🔹 Используйте ПриОткрытии для предварительной загрузки данных во временные таблицы
    • 🔹 Разделяйте логику получения данных и отображения
    • 🔹 Для динамических списков настройте отбор на сервере

    Пример:

    &НаСервере
    

    Процедура ПриОткрытии(Отказ)

    // Загружаем справочники при открытии формы

    ЗагрузитьСправочники();

    // Настраиваем динамический список

    ЭлементыФормы.СписокТоваров.Запрос.Текст =

    "ВЫБРАТЬ

    | Товары.Ссылка КАК Ссылка,

    | Товары.Наименование,

    | Товары.Артикул

    |ИЗ

    | ВТ_Товары КАК Товары

    |ГДЕ

    | Товары.Группа = &ТекущаяГруппа";

    КонецПроцедуры

    Как разделять запросы при работе с большими регистрами (10 млн+ записей)?

    Для крупных регистров:

    1. Используйте виртуальные таблицы с отборами по периодам
    2. Разделяйте обработку по периодам (день/неделя/месяц)
    3. Применяйте побитовую обработку для регистров сведений
    4. Настраивайте кластерные индексы на часто используемые поля

    Пример работы с виртуальной таблицей:

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

    "ВЫБРАТЬ

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

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

    |ИЗ

    | РегистрНакопления.ОстаткиТоваров.Остатки(

    | &ДатаНачала,

    | &ДатаКонца,

    | Склад В (&СписокСкладов)

    | ) КАК ОстаткиТоваров";

    Влияет ли разделение запросов на лицензирование 1С?

    Нет, разделение запросов не влияет на:

    • 🔑 Количество лицензий 1С:Предприятие
    • 📊 Лимиты клиентских соединений
    • 💾 Объем используемой памяти

    Однако косвенно может повлиять на:

    • 🔧 Нагрузку на сервер , что важно для облачных решений с ограничением по CPU
    • 📡 Сетевой трафик между клиентом и сервером
    ⚠️ Внимание: В некоторых тарифах 1С:Fresh и 1С:ГISPRU могут действовать ограничения на длительные операции. Уточняйте условия в личном кабинете.
    Как тестировать производительность после разделения?

    Используйте комплексный подход:

    1. Замеры времени:
      Начало = ТекущаяДата();
      

      // ... выполнение кода ...

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

      Сообщить("Время выполнения: " + (Конец - Начало));

    2. Анализ плана выполнения через ОбъяснитьЗапрос()
    3. Тестирование под нагрузкой с помощью 1С:Тест-центр или JMeter
    4. Мониторинг ресурсов сервера (CPU, RAM, диски) через Перфоманс Монитор

    Обращайте внимание на:

    • 📈 Время ответа при пиковых нагрузках
    • 🔄 Количество блокировок (sys.dm_tran_locks в SQL Server)
    • 💾 Объем временных таблиц в tempdb