Как оптимизировать запрос динамического списка при соединении со срезом последних?

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

Разберем типичную и крайне неприятную проблему производительности в управляемом интерфейсе 1С 8.3: медленное открытие формы списка (подбора или остатков товаров) и сильную нагрузку на сервер при одновременной работе нескольких пользователей. Выясним, почему соединение с виртуальной таблицей СрезПоследних замедляет выборку в десятки раз и как правильно решить эту задачу.

Анализ проблемы: почему падает производительность

Рассмотрим исходную структуру запроса динамического списка, которая приводит к зависаниям:


ВЫБРАТЬ
    СправочникКлючиАналитикиУчетаНоменклатуры.Ссылка КАК АналитикаУчетаНоменклатуры,
    ПРЕДСТАВЛЕНИЕ(СправочникКлючиАналитикиУчетаНоменклатуры.Номенклатура) КАК Номенклатура,
    ПРЕДСТАВЛЕНИЕ(СправочникКлючиАналитикиУчетаНоменклатуры.СерияНоменклатуры) КАК СерияНоменклатуры,
    ТоварыНаСкладахОстатки.КоличествоОстаток КАК Количество,
    РозничныеЦеныНоменклатурыСрезПоследних.Цена КАК Цена
ИЗ
    РегистрНакопления.ТоварыНаСкладах.Остатки(, Склад = &Склад) КАК ТоварыНаСкладахОстатки
        ЛЕВОЕ СОЕДИНЕНИЕ Справочник.КлючиАналитикиУчетаНоменклатуры КАК СправочникКлючиАналитикиУчетаНоменклатуры
        ПО ТоварыНаСкладахОстатки.АналитикаУчетаНоменклатуры = СправочникКлючиАналитикиУчетаНоменклатуры.Ссылка
        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.РозничныеЦеныНоменклатуры.СрезПоследних(, Склад = &Склад) КАК РозничныеЦеныНоменклатурыСрезПоследних
        ПО ТоварыНаСкладахОстатки.АналитикаУчетаНоменклатуры = РозничныеЦеныНоменклатурыСрезПоследних.АналитикаУчетаНоменклатуры

Проанализируем механику работы СУБД в данном случае:

  1. Постраничная выборка динамического списка: Динамический список запрашивает данные порциями (обычно от 20 до 45 записей для заполнения видимой части таблицы). Однако при наличии соединения с виртуальной таблицей СрезПоследних СУБД часто не может «пробросить» ограничение порции внутрь тяжелого вложенного подзапроса среза.
  2. Вычисление среза по всей таблице: Для выполнения соединения СУБД сначала рассчитывает срез последних цен по всему регистру (или по всему складу), выполняя группировки по максимуму периода, и только затем соединяет результат с остатками и отсекает видимые строки. Это вызывает колоссальный объем дискового чтения и загрузку процессора.
  3. Отсутствие основной таблицы: Поскольку в запросе используются виртуальные таблицы, динамический список не может определить основную таблицу для оптимизированной навигации по первичному ключу.

Решение 1. Дополнительная обработка данных списка на сервере (рекомендация 1С)

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

Разберем по шагам реализацию этого механизма:

Шаг 1. Упростим основной запрос динамического списка, оставив в нем только получение остатков и аналитики:


ВЫБРАТЬ
    ТоварыНаСкладахОстатки.АналитикаУчетаНоменклатуры КАК АналитикаУчетаНоменклатуры,
    ПРЕДСТАВЛЕНИЕ(ТоварыНаСкладахОстатки.АналитикаУчетаНоменклатуры.Номенклатура) КАК Номенклатура,
    ПРЕДСТАВЛЕНИЕ(ТоварыНаСкладахОстатки.АналитикаУчетаНоменклатуры.СерияНоменклатуры) КАК СерияНоменклатуры,
    ТоварыНаСкладахОстатки.КоличествоОстаток КАК Количество,
    0 КАК Цена
ИЗ
    РегистрНакопления.ТоварыНаСкладах.Остатки(, Склад = &Склад) КАК ТоварыНаСкладахОстатки

Шаг 2. В свойствах динамического списка включим обработчик события ПриПолученииДанныхНаСервере. Напишем код для получения цен только по текущей порции строк:


&НаСервере
Процедура СписокПриПолученииДанныхНаСервере(Элемент, ОформленияСтрок)
    
    МассивАналитик = Новый Массив;
    Для Каждого Строка Из ОформленияСтрок Цикл
        МассивАналитик.Добавить(Строка.Данные.АналитикаУчетаНоменклатуры);
    КонецЦикла;
    
    Если МассивАналитик.Количество() = 0 Тогда
        Возврат;
    КонецЕсли;
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	Цены.АналитикаУчетаНоменклатуры КАК АналитикаУчетаНоменклатуры,
        |	Цены.Цена КАК Цена
        |ИЗ
        |	РегистрСведений.РозничныеЦеныНоменклатуры.СрезПоследних(
        |		,
        |		Склад = &Склад
        |			И АналитикаУчетаНоменклатуры В (&МассивАналитик)) КАК Цены";
    
    Запрос.УстановитьПараметр("Склад", Склад);
    Запрос.УстановитьПараметр("МассивАналитик", МассивАналитик);
    
    ТаблицаЦен = Запрос.Выполнить().Выгрузить();
    ТаблицаЦен.Индексы.Добавить("АналитикаУчетаНоменклатуры");
    
    Для Каждого Строка Из ОформленияСтрок Цикл
        СтрокаЦены = ТаблицаЦен.Найти(Строка.Данные.АналитикаУчетаНоменклатуры, "АналитикаУчетаНоменклатуры");
        Если СтрокаЦены <> Неопределено Тогда
            Строка.Ячейки.Цена.Значение = СтрокаЦены.Цена;
            Строка.Ячейки.Цена.Текст = Формат(СтрокаЦены.Цена, "ЧЦ=15; ЧДЦ=2");
        КонецЕсли;
    КонецЦикла;
    
КонецПроцедуры

Важное ограничение: При таком способе пользователи не смогут сортировать список по колонке Цена или накладывать на нее отбор через стандартную панель динамического списка, так как эти данные заполняются после выполнения основного SQL-запроса.

Решение 2. Использование временных таблиц в запросе динамического списка

Если используется платформа 1С:Предприятие 8.3.8 и выше (с соответствующим режимом совместимости), в динамических списках поддерживается работа с пакетными запросами и временными таблицами. Это позволяет отфильтровать цены на уровне СУБД через промежуточную выборку.

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


ВЫБРАТЬ
    ТоварыНаСкладахОстатки.АналитикаУчетаНоменклатуры КАК АналитикаУчетаНоменклатуры,
    ТоварыНаСкладахОстатки.КоличествоОстаток КАК Количество
ПОМЕСТИТЬ ВТОстатки
ИЗ
    РегистрНакопления.ТоварыНаСкладах.Остатки(, Склад = &Склад) КАК ТоварыНаСкладахОстатки
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ВТОстатки.АналитикаУчетаНоменклатуры КАК АналитикаУчетаНоменклатуры,
    ПРЕДСТАВЛЕНИЕ(СправочникКлючи.Номенклатура) КАК Номенклатура,
    ПРЕДСТАВЛЕНИЕ(СправочникКлючи.СерияНоменклатуры) КАК СерияНоменклатуры,
    ВТОстатки.Количество КАК Количество,
    ЕСТЬNULL(РозничныеЦеныНоменклатурыСрезПоследних.Цена, 0) КАК Цена
ИЗ
    ВТОстатки КАК ВТОстатки
        ЛЕВОЕ СОЕДИНЕНИЕ Справочник.КлючиАналитикиУчетаНоменклатуры КАК СправочникКлючи
        ПО ВТОстатки.АналитикаУчетаНоменклатуры = СправочникКлючи.Ссылка
        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.РозничныеЦеныНоменклатуры.СрезПоследних(
            ,
            Склад = &Склад
                И АналитикаУчетаНоменклатуры В
                    (ВЫБРАТЬ
                        ВТОстатки.АналитикаУчетаНоменклатуры
                    ИЗ
                        ВТОстатки КАК ВТОстатки)) КАК РозничныеЦеныНоменклатурыСрезПоследних
        ПО ВТОстатки.АналитикаУчетаНоменклатуры = РозничныеЦеныНоменклатурыСрезПоследних.АналитикаУчетаНоменклатуры

Обратите внимание: мы передали условие АналитикаУчетаНоменклатуры В (ВЫБРАТЬ ...) непосредственно в параметры виртуальной таблицы СрезПоследних. Это принудительно ограничивает выборку среза только теми товарами, по которым действительно есть ненулевые остатки на складе.

Решение 3. Оптимизация структуры метаданных и индексов

Если структура конфигурации доступна для изменения, проанализируем параметры регистра сведений:

  1. Порядок измерений: Убедимся, что измерение АналитикаУчетаНоменклатуры стоит на первом месте в составе измерений регистра сведений РозничныеЦеныНоменклатуры. Это сформирует кластерный индекс вида АналитикаУчетаНоменклатуры + Период, что позволит СУБД выполнять быстрый поиск (Index Seek) вместо полного сканирования таблицы.
  2. Итоги среза последних: Если регистр периодический, проверим, включена ли функциональность хранения итогов среза последних в свойствах регистра сведений. Это позволяет платформе обращаться к готовой агрегатной таблице последних значений без вычисления MAX(Период) на лету.
  3. Непериодический регистр цен: Если для конкретного бизнес-процесса не требуется вести историю изменения розничных цен по датам, переведем регистр сведений в статус Непериодический. В этом случае чтение будет идти напрямую из физической таблицы без дополнительных подзапросов среза.
← На главную