В практике разработчика 1С нередко встречается парадоксальная ситуация: простейший запрос, который месяцами работал мгновенно, внезапно начинает «вешать» систему. Особенно странно это выглядит, когда запрос без ограничения количества строк работает штатно, а добавление конструкции ПЕРВЫЕ 1000 приводит к бесконечному ожиданию. В этой статье мы подробно разберем, почему так происходит, изучим механизмы работы MS SQL Server и научимся исправлять подобные ошибки оптимизации.
Рассмотрим типичный запрос к регистру сведений, который вызвал затруднения. В нем присутствуют фильтры по измерениям, обращение к реквизитам связанных документов через «точку» и сортировка по дате этого самого документа. Проблема проявляется именно при наличии ключевого слова ПЕРВЫЕ. Разберем по шагам, что происходит «под капотом» системы.
Когда SQL Server получает запрос без TOP (в 1С это ПЕРВЫЕ), его задача — максимально быстро вернуть весь набор данных. Для этого он часто использует эффективные алгоритмы, такие как Hash Join (соединение хэшированием). Но как только в запросе появляется TOP, логика оптимизатора меняется. Включается механизм, называемый Row Goal (цель по количеству строк).
Оптимизатор предполагает: «Если пользователю нужны только первые 1000 строк, я не буду строить сложные хэш-таблицы для миллионов записей. Я лучше найду индекс, который соответствует порядку сортировки (УПОРЯДОЧИТЬ ПО), и буду просто читать его подряд, пока не наберу нужные 1000 строк, подходящих под условия ГДЕ».
В чем ловушка? Если данных, подходящих под фильтры, в начале таблицы очень мало (например, старые заказы за прошлые годы), SQL Server может начать последовательно сканировать (Index Scan) миллионы записей в поисках тех самых «первых 1000». Это вызывает колоссальную нагрузку на дисковую подсистему и процессор, что пользователь и видит как «зависание».
Первое, что необходимо сделать, если «ничего не менялось, но все сломалось» — это проверить актуальность статистики базы данных. SQL Server строит план запроса на основе данных о распределении значений в таблицах. Если статистика устарела, он может ошибочно выбрать стратегию сканирования там, где нужен поиск.
Проанализируем действия по обслуживанию базы. Выполним очистку кэша планов (чтобы SQL Server забыл неудачный план запроса) и принудительно обновим статистику:
-- Очистка кэша планов для всей базы (использовать с осторожностью на нагруженных системах)
DBCC FREEPROCCACHE;
-- Обновление статистики для всех таблиц с полным сканированием
EXEC sp_msforeachtable N'UPDATE STATISTICS ? WITH FULLSCAN';
Часто этого действия достаточно, чтобы вернуть производительность в норму. Однако это лишь временная мера, если сама структура запроса провоцирует ошибку оптимизатора.
Выясним причину, связанную с обращением к полям через точку. В 1С поля типа «ЛюбаяСсылка» или составные типы данных при обращении через точку заставляют SQL Server соединяться со всеми возможными таблицами объектов, входящих в этот тип. Если ЗаказНаПроизводство — это поле с составным типом, запрос становится тяжелым.
Принудительно ограничим тип данных с помощью ВЫРАЗИТЬ. Это подскажет платформе 1С, что соединение нужно строить только с одной конкретной таблицей документа:
ВЫБРАТЬ ПЕРВЫЕ 1000
ФТ_ШтрихкодыЭтикеток.Штрихкод КАК Штрихкод
ИЗ
РегистрСведений.ФТ_ШтрихкодыЭтикеток КАК ФТ_ШтрихкодыЭтикеток
ГДЕ
ВЫРАЗИТЬ(ФТ_ШтрихкодыЭтикеток.ЗаказНаПроизводство КАК Документ.ЗаказНаПроизводство).Подразделение = &Подразделение
И РАЗНОСТЬДАТ(ВЫРАЗИТЬ(ФТ_ШтрихкодыЭтикеток.ЗаказНаПроизводство КАК Документ.ЗаказНаПроизводство).Дата, &ТекущаяДата, ДЕНЬ) > 30
УПОРЯДОЧИТЬ ПО
ВЫРАЗИТЬ(ФТ_ШтрихкодыЭтикеток.ЗаказНаПроизводство КАК Документ.ЗаказНаПроизводство).Дата
Это упрощает структуру соединений (JOIN) в итоговом SQL-запросе и помогает оптимизатору лучше оценить стоимость выполнения.
Важным моментом является использование функции РАЗНОСТЬДАТ непосредственно в условии фильтрации. Такие запросы называются non-SARGable. Это означает, что SQL Server не может использовать индекс по полю Дата для быстрого поиска записей, так как ему нужно вычислить результат функции для каждой строки таблицы.
Разберем, как сделать запрос эффективнее. Вычислим граничную дату заранее в коде 1С и передадим её как параметр:
// В коде 1С
ГраничнаяДата = ТекущаяДата() - (30 * 24 * 60 * 60); // минус 30 дней
// В запросе
ГДЕ
ФТ_ШтрихкодыЭтикеток.ЗаказНаПроизводство.Дата < &ГраничнаяДата
Теперь SQL Server может использовать Index Seek (поиск по индексу), если таковой имеется, что на порядки быстрее сканирования всей таблицы.
Если вышеперечисленные методы не помогают, применим самый надежный способ — разделение одного сложного запроса на несколько простых. Это «разрывает» цепочку оптимизации Row Goal и заставляет SQL Server работать с предсказуемыми наборами данных.
ЗаказыНаПроизводство, которые подходят под фильтры подразделения и даты.
// Шаг 1: Выбираем подходящие заказы
ВЫБРАТЬ
Заказ.Ссылка КАК Заказ
ПОМЕСТИТЬ ВТ_ОтфильтрованныеЗаказы
ИЗ
Документ.ЗаказНаПроизводство КАК Заказ
ГДЕ
Заказ.Подразделение = &Подразделение
И Заказ.Дата < &ГраничнаяДата;
// Шаг 2: Основная выборка
ВЫБРАТЬ ПЕРВЫЕ 1000
Рс.Штрихкод КАК Штрихкод
ИЗ
РегистрСведений.ФТ_ШтрихкодыЭтикеток КАК Рс
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ОтфильтрованныеЗаказы КАК ВТ
ПО Рс.ЗаказНаПроизводство = ВТ.Заказ
ГДЕ
Рс.ДатаСканирования = ДАТАВРЕМЯ(1, 1, 1)
И Рс.Смена = ""
УПОРЯДОЧИТЬ ПО
Рс.ЗаказНаПроизводство.Дата
Такой подход гарантирует, что SQL Server не будет пытаться сканировать весь регистр в поисках подходящих записей, так как круг поиска уже ограничен временной таблицей ВТ_ОтфильтрованныеЗаказы.
Посмотрим на ситуацию шире. Если запрос к регистру сведений постоянно требует фильтрации или сортировки по реквизитам связанного документа, это явный признак того, что архитектура регистра не оптимальна. Обращение через точку в условиях запроса — это всегда риск.
Рекомендуется рассмотреть следующие изменения:
ФТ_ШтрихкодыЭтикеток дополнительные реквизиты (например, ДатаЗаказа и ПодразделениеЗаказа).В этом случае запрос будет работать с одной-единственной таблицей без соединений, что обеспечит максимальную производительность даже при огромных объемах данных.
Зависание запроса с ПЕРВЫЕ — это не магия, а результат специфического поведения оптимизатора SQL. Чтобы победить эту проблему, мы должны:
ГДЕ (делать их SARGable).Соблюдение этих правил позволит вашим запросам работать стабильно и предсказуемо вне зависимости от роста объема базы данных.