Почему не работает или зависает левое соединение с подзапросом в запросе 1С и как это исправить?

Программист 1С v8.3 (Управляемые формы) 1С:Управление торговлей Управленческий учет Торговля и дистрибуция
← На главную

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

Причины неоптимальной работы соединения с подзапросом

Рассмотрим ключевые технические факторы, которые приводят к высокой нагрузке на СУБД при использовании вложенных запросов в соединениях:

  1. Неверная оценка кардинальности СУБД: При выполнении прямого соединения физической таблицы с вложенным подзапросом оптимизатор СУБД (например, MS SQL Server) не может заранее рассчитать количество строк, которое вернет подзапрос. Из-за отсутствия статистики СУБД выбирает неоптимальный план выполнения (чаще всего алгоритм Nested Loops — вложенные циклы), что вызывает миллионы лишних итераций.
  2. Отсутствие индексов у подзапроса: В отличие от реальных таблиц базы данных, результат подзапроса формируется «на лету» и не содержит индексов. При каждом сопоставлении строк СУБД вынуждена выполнять полное сканирование выборки (Table Scan).
  3. Работа с составными типами данных: Поле типа Объект часто является составным (может хранить ссылки на различные документы и справочники). На уровне СУБД составное поле хранится в виде трех колонок (тип, вид метаданных и GUID). Соединение по такому полю без явного приведения типов заставляет 1С генерировать громоздкий SQL-код с многочисленными проверками всех возможных таблиц, исключая использование существующих индексов.

Решение 1. Перенос подзапроса во временную таблицу (Рекомендуемый подход)

Главный стандарт оптимизации запросов в 1С — отказ от вложенных подзапросов в секциях ПО и ГДЕ в пользу пакета запросов с временными таблицами. Вынесем выборку первичных данных во временную таблицу и проиндексируем поле связи.

Разберем по шагам создание пакета запросов:

  1. Во первом запросе пакета выбираем нужные документы с фильтром по статусу и помещаем результат во временную таблицу ВТ_ПринятыеДокументы.
  2. Явно указываем конструкцию ИНДЕКСИРОВАТЬ ПО по полю соединения. Это позволит СУБД построить быстрый индекс для временной таблицы в TempDB.
  3. Во втором запросе соединяем подготовленную временную таблицу с регистром сведений для проверки второго статуса.

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


ВЫБРАТЬ
	ПеремещениеТоваров.Ссылка КАК Док,
	ПеремещениеТоваров.Дата КАК ДатаДок,
	ПеремещениеТоваров.СкладПолучатель КАК СкладПолучатель,
	ИсторияСтатусов.Дата КАК Принято
ПОМЕСТИТЬ ВТ_ПринятыеДокументы
ИЗ
	РегистрСведений.ИсторияСтатусовСправочниковИДокументов КАК ИсторияСтатусов
		ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.ПеремещениеТоваров КАК ПеремещениеТоваров
		ПО ИсторияСтатусов.Объект = ПеремещениеТоваров.Ссылка
ГДЕ
	ПеремещениеТоваров.Проведен
	И ПеремещениеТоваров.Дата МЕЖДУ &нДата И &кДата
	И ПеремещениеТоваров.Организация = &Организация
	И ИсторияСтатусов.Статус = ЗНАЧЕНИЕ(Справочник.СтатусыСправочниковИДокументов.ПТПринято)

ИНДЕКСИРОВАТЬ ПО
	Док
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
	ТЗПринято.ДатаДок КАК ДатаДок,
	ТЗПринято.Док КАК Док,
	ТЗПринято.Принято КАК Принято,
	ТЗПринято.СкладПолучатель КАК СкладПолучатель,
	ИсторияСтатусовОтгружено.Дата КАК ДатаОтгружено
ИЗ
	ВТ_ПринятыеДокументы КАК ТЗПринято
		ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ИсторияСтатусовСправочниковИДокументов КАК ИсторияСтатусовОтгружено
		ПО ТЗПринято.Док = ИсторияСтатусовОтгружено.Объект
			И (ИсторияСтатусовОтгружено.Статус = ЗНАЧЕНИЕ(Справочник.СтатусыСправочниковИДокументов.ПТОтгружено))

Решение 2. Использование функции ВЫРАЗИТЬ() для составных полей

Если реквизит Объект имеет составной тип, обязательно используем функцию ВЫРАЗИТЬ(). Это сообщает платформе и СУБД конкретную таблицу метаданных, что позволяет существенно сократить генерируемый SQL-код и ускорить выполнение соединений.

Пример приведения составного типа в секции выборки или соединения:

ВЫРАЗИТЬ(ИсторияСтатусов.Объект КАК Документ.ПеремещениеТоваров)

Решение 3. Однократный проход по регистру с группировкой и фильтрацией ИМЕЮЩИЕ

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

Рассмотрим структуру такого запроса:


ВЫБРАТЬ
	ВЫРАЗИТЬ(ИсторияСтатусов.Объект КАК Документ.ПеремещениеТоваров) КАК Док,
	МАКСИМУМ(ВЫБОР 
		КОГДА ИсторияСтатусов.Статус = ЗНАЧЕНИЕ(Справочник.СтатусыСправочниковИДокументов.ПТПринято) 
		ТОГДА ИсторияСтатусов.Дата 
	КОНЕЦ) КАК ДатаПринято,
	МАКСИМУМ(ВЫБОР 
		КОГДА ИсторияСтатусов.Статус = ЗНАЧЕНИЕ(Справочник.СтатусыСправочниковИДокументов.ПТОтгружено) 
		ТОГДА ИсторияСтатусов.Дата 
	КОНЕЦ) КАК ДатаОтгружено
ПОМЕСТИТЬ ВТ_СтатусыДокументов
ИЗ
	РегистрСведений.ИсторияСтатусовСправочниковИДокументов КАК ИсторияСтатусов
ГДЕ
	ИсторияСтатусов.Статус В (
		ЗНАЧЕНИЕ(Справочник.СтатусыСправочниковИДокументов.ПТПринято),
		ЗНАЧЕНИЕ(Справочник.СтатусыСправочниковИДокументов.ПТОтгружено)
	)
СГРУППИРОВАТЬ ПО
	ВЫРАЗИТЬ(ИсторияСтатусов.Объект КАК Документ.ПеремещениеТоваров)
ИМЕЮЩИЕ
	МАКСИМУМ(ВЫБОР 
		КОГДА ИсторияСтатусов.Статус = ЗНАЧЕНИЕ(Справочник.СтатусыСправочниковИДокументов.ПТПринято) 
		ТОГДА ИсторияСтатусов.Дата 
	КОНЕЦ) ЕСТЬ НЕ NULL

Этот способ позволяет избежать повторных соединений к крупным таблицам и максимально эффективно использует индексы СУБД.

Выводы

Сформулируем основные правила оптимизации запросов при работе с соединениями и составными типами в 1С:

← На главную