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

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

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

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

Анализируем причину замедления: проблемный запрос

Давайте рассмотрим исходный запрос, который используется в динамическом списке. Мы увидим в нем несколько конструкций, которые критически влияют на производительность.


ВЫБРАТЬ
    РеализацияТоваровУслуг.Ссылка КАК Ссылка,
    РеализацияТоваровУслуг.Дата КАК Дата,
    РеализацияТоваровУслуг.Номер КАК Номер,
    РеализацияТоваровУслуг.Контрагент КАК Получатель,
    РеализацияТоваровУслуг.ГородДоставки КАК ГородДоставки,
    РеализацияТоваровУслуг.Комментарий КАК Комментарий,
    тчРТиУ.Количество КАК Количество,
    тчРТиУ.Вес КАК Вес,
    тчРТиУ.Объем КАК Объем,
    РеализацияТоваровУслуг.Ответственный КАК Ответственный,
    РеализацияТоваровУслуг.СпособДоставки КАК СпособДоставки,
    "РТИУ" КАК ТипДокумента
ИЗ
    Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг
        ПОЛНОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ
            РеализацияТоваровУслугТовары.Ссылка КАК Ссылка,
            СУММА(РеализацияТоваровУслугТовары.Количество) КАК Количество,
            СУММА(РеализацияТоваровУслугТовары.Номенклатура.ЕдиницаХраненияОстатков.Вес * РеализацияТоваровУслугТовары.Количество) КАК Вес,
            СУММА(РеализацияТоваровУслугТовары.Номенклатура.ЕдиницаХраненияОстатков.Объем * РеализацияТоваровУслугТовары.Количество) КАК Объем
        ИЗ
            Документ.РеализацияТоваровУслуг.Товары КАК РеализацияТоваровУслугТовары
        ГДЕ
            РеализацияТоваровУслугТовары.Ссылка.Проведен = ИСТИНА
            И РеализацияТоваровУслугТовары.Ссылка.Склад В(&СписокСкладов)
            И НЕ РеализацияТоваровУслугТовары.Ссылка В (&СписокОбработаныхНакладных)
            И РеализацияТоваровУслугТовары.Ссылка.ТипДоставки <> ЗНАЧЕНИЕ(Перечисление.ТипДоставки.Самовывоз)
            И РеализацияТоваровУслугТовары.Ссылка.Дата > ДАТАВРЕМЯ(2020, 5, 1, 0, 0, 0)
        
        СГРУППИРОВАТЬ ПО
            РеализацияТоваровУслугТовары.Ссылка) КАК тчРТиУ
        ПО РеализацияТоваровУслуг.Ссылка = тчРТиУ.Ссылка
ГДЕ
    РеализацияТоваровУслуг.Проведен = ИСТИНА
    И РеализацияТоваровУслуг.Склад В(&СписокСкладов)
    И НЕ РеализацияТоваровУслуг.Ссылка В (&СписокОбработаныхНакладных)
    И РеализацияТоваровУслуг.ТипДоставки <> ЗНАЧЕНИЕ(Перечисление.ТипДоставки.Самовывоз)
    И РеализацияТоваровУслуг.Дата > ДАТАВРЕМЯ(2020, 5, 1, 0, 0, 0)
 ОБЪЕДИНИТЬ ВСЕ
 ВЫБРАТЬ
    ПеремещениеТоваров.Ссылка,
    ПеремещениеТоваров.Дата,
    ПеремещениеТоваров.Номер,
    ПеремещениеТоваров.СкладПолучатель,
    ПеремещениеТоваров.СкладПолучатель.ГородДоставки,
    ПеремещениеТоваров.Комментарий,
    тчПеремещение.КоличествоЕдиницТоваров,
    тчПеремещение.Вес,
    тчПеремещение.Объем,
    ПеремещениеТоваров.Ответственный,
    "Перемещение",
    "Перемещение"
ИЗ
    Документ.ПеремещениеТоваров КАК ПеремещениеТоваров
        ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ
            ПеремещениеТоваровТовары.Ссылка КАК Ссылка,
            СУММА(ПеремещениеТоваровТовары.Количество) КАК КоличествоЕдиницТоваров,
            СУММА(ПеремещениеТоваровТовары.Номенклатура.ЕдиницаХраненияОстатков.Вес * ПеремещениеТоваровТовары.Количество) КАК Вес,
            СУММА(ПеремещениеТоваровТовары.Номенклатура.ЕдиницаХраненияОстатков.Объем * ПеремещениеТоваровТовары.Количество) КАК Объем
        ИЗ
            Документ.ПеремещениеТоваров.Товары КАК ПеремещениеТоваровТовары
        ГДЕ
            ПеремещениеТоваровТовары.Ссылка.Проведен = ИСТИНА
            И ПеремещениеТоваровТовары.Ссылка.СкладОтправитель В(&СписокСкладов)
            И НЕ ПеремещениеТоваровТовары.Ссылка В (&СписокОбработаныхНакладных)
            И ПеремещениеТоваровТовары.Ссылка.Дата > ДАТАВРЕМЯ(2020, 5, 1, 0, 0, 0)
        
        СГРУППИРОВАТЬ ПО
            ПеремещениеТоваровТовары.Ссылка) КАК тчПеремещение
        ПО ПеремещениеТоваров.Ссылка = тчПеремещение.Ссылка
ГДЕ
    ПеремещениеТоваров.Проведен = ИСТИНА
    И ПеремещениеТоваров.СкладОтправитель В(&СписокСкладов)
    И НЕ ПеремещениеТоваров.Ссылка В (&СписокОбработаныхНакладных)
    И ПеремещениеТоваров.Дата > ДАТАВРЕМЯ(2020, 5, 1, 0, 0, 0)

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

  1. ПОЛНОЕ СОЕДИНЕНИЕ (FULL JOIN): Это одна из наиболее ресурсоемких операций в SQL. В отличие от ЛЕВОЕ или ПРАВОЕ СОЕДИНЕНИЕ, которое может быть оптимизировано для поиска данных только в одной из таблиц при отсутствии совпадений, ПОЛНОЕ СОЕДИНЕНИЕ требует от СУБД сопоставления каждой строки из одной таблицы с каждой строкой из другой и включения всех строк, независимо от наличия совпадений. В контексте динамического списка это особенно критично, поскольку платформа стремится считывать данные порциями, а ПОЛНОЕ СОЕДИНЕНИЕ часто делает это невозможным.
  2. ОБЪЕДИНИТЬ ВСЕ (UNION ALL): Объединение нескольких запросов с помощью ОБЪЕДИНИТЬ ВСЕ означает, что каждый подзапрос выполняется полностью и независимо. Если каждый из этих подзапросов уже сложен, как в нашем случае, общая производительность резко падает. Динамический список при такой структуре запроса не может эффективно определить "основную таблицу" для порционного чтения, и ему приходится считывать весь результат целиком.
  3. Вложенные запросы с агрегацией и условиями по ссылкам: Подзапросы тчРТиУ и тчПеремещение выполняют агрегацию СУММА(), что само по себе может быть долгим на больших объемах. Более того, в условии ГДЕ этих подзапросов используются разыменования ссылок через точку, например, РеализацияТоваровУслугТовары.Ссылка.Проведен = ИСТИНА. Каждое такое разыменование приводит к дополнительному обращению к базе данных, замедляя выполнение.
  4. Отсутствие "Основной таблицы": Из-за сложности запроса с ПОЛНОЕ СОЕДИНЕНИЕ и ОБЪЕДИНИТЬ ВСЕ платформа 1С не может однозначно определить "основную таблицу" для динамического списка. Это означает, что механизм порционного чтения данных, который позволяет быстро показывать первые строки списка и подгружать остальные по мере прокрутки, не работает. Вместо этого, платформа вынуждена считывать весь набор данных запроса целиком перед отображением, что приводит к значительным задержкам при первом открытии списка и при каждом его обновлении.
  5. Параметры типа В (&СписокСкладов) и НЕ В (&СписокОбработаныхНакладных): Если списки, передаваемые в эти параметры, очень большие, это может негативно сказаться на производительности запроса, особенно в сочетании с другими проблемными конструкциями.
  6. Наличие группировок: Вложенные запросы используют группировки, что также является ресурсоемкой операцией. Динамический список не очень хорошо работает с данными, которые требуют сложной предварительной агрегации.

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

Решения по оптимизации динамического списка

Мы предлагаем несколько подходов к решению проблемы, от архитектурных изменений до оптимизации запроса.

1. Архитектурное решение: Использование регистра сведений для предрасчета данных

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

  1. Создаем вспомогательный регистр сведений:

    Создадим новый периодический регистр сведений (или непериодический, если данные не зависят от даты) под названием, например, РасчетныеДанныеДокументов. В нем будем хранить предрассчитанные агрегированные значения для каждого документа.

    Пример структуры регистра:

    • Измерение: ДокументСсылка (Тип: ДокументСсылка.РеализацияТоваровУслуг, ДокументСсылка.ПеремещениеТоваров)
    • Ресурсы: Количество (Число), Вес (Число), Объем (Число)
    • Измерение: ТипДокумента (Тип: Строка или Перечисление, для удобства идентификации)
  2. Заполняем регистр при проведении документов:

    В обработчиках проведения документов РеализацияТоваровУслуг и ПеремещениеТоваров добавляем код, который вычисляет необходимые Количество, Вес и Объем по товарам документа и записывает эти данные в созданный регистр сведений. Это позволяет перенести "тяжелые" вычисления из запроса динамического списка в момент записи документа, когда пользователь уже ждет.

    Пример кода в модуле объекта документа РеализацияТоваровУслуг:

    
    // ... в процедуре ОбработкаПроведения()
    Движения.РасчетныеДанныеДокументов.Записывать = Истина;
    Движения.РасчетныеДанныеДокументов.ОчиститьПоСвязке(Ссылка);
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |    СУММА(РеализацияТоваровУслугТовары.Количество) КАК Количество,
        |    СУММА(РеализацияТоваровУслугТовары.Номенклатура.ЕдиницаХраненияОстатков.Вес * РеализацияТоваровУслугТовары.Количество) КАК Вес,
        |    СУММА(РеализацияТоваровУслугТовары.Номенклатура.ЕдиницаХраненияОстатков.Объем * РеализацияТоваровУслугТовары.Количество) КАК Объем
        |ИЗ
        |    Документ.РеализацияТоваровУслуг.Товары КАК РеализацияТоваровУслугТовары
        |ГДЕ
        |    РеализацияТоваровУслугТовары.Ссылка = &Ссылка";
    Запрос.УстановитьПараметр("Ссылка", Ссылка);
            
    Результат = Запрос.Выполнить().Выгрузить();
    Если НЕ Результат.Пуст() Тогда
        СтрокаРезультата = Результат[0];
        Движение = Движения.РасчетныеДанныеДокументов.Добавить();
        Движение.ДокументСсылка = Ссылка;
        Движение.Количество = СтрокаРезультата.Количество;
        Движение.Вес = СтрокаРезультата.Вес;
        Движение.Объем = СтрокаРезультата.Объем;
        Движение.ТипДокумента = "РТИУ"; // Или использовать перечисление
    КонецЕсли;
    // ...
    

    Аналогичный код добавляем и для документа ПеремещениеТоваров.

  3. Обновляем запрос динамического списка:

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

    Пример упрощенного запроса:

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

    Даже с ОБЪЕДИНИТЬ ВСЕ этот запрос будет работать значительно быстрее, так как подзапросы убраны, агрегация вынесена, и данные из табличных частей получаются не напрямую. В идеале, если можно, стоит объединить оба типа документов в одном регистре сведений с общими полями, чтобы избежать ОБЪЕДИНИТЬ ВСЕ в основном запросе динамического списка или использовать временные таблицы для его оптимизации.

  4. Индексация регистра сведений:

    Убедитесь, что измерение ДокументСсылка в новом регистре сведений проиндексировано. Это обеспечит быстрое соединение с таблицами документов.

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

2. Оптимизация существующего запроса (если архитектурное изменение невозможно)

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

  1. Заменяем ПОЛНОЕ СОЕДИНЕНИЕ на ЛЕВОЕ СОЕДИНЕНИЕ:

    Как правило, ПОЛНОЕ СОЕДИНЕНИЕ почти всегда можно заменить на ЛЕВОЕ СОЕДИНЕНИЕ с проверкой на NULL, если вам нужны данные из одной таблицы, даже если нет совпадений в другой. В вашем случае, если вы всегда ожидаете получить документ РеализацияТоваровУслуг, и к нему опционально прицепляются агрегированные данные, ЛЕВОЕ СОЕДИНЕНИЕ будет предпочтительнее.

    
    ИЗ
        Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг
            ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ
                РеализацияТоваровУслугТовары.Ссылка КАК Ссылка,
                СУММА(РеализацияТоваровУслугТовары.Количество) КАК Количество,
                // ... остальные поля
            ИЗ
                Документ.РеализацияТоваровУслуг.Товары КАК РеализацияТоваровУслугТовары
            ГДЕ
                РеализацияТоваровУслугТовары.Ссылка.Проведен = ИСТИНА
                // ... остальные условия
            СГРУППИРОВАТЬ ПО
                РеализацияТоваровУслугТовары.Ссылка) КАК тчРТиУ
            ПО РеализацияТоваровУслуг.Ссылка = тчРТиУ.Ссылка
    
  2. Используем временные таблицы для ОБЪЕДИНИТЬ ВСЕ:

    Для сложных запросов с ОБЪЕДИНИТЬ ВСЕ мы можем использовать временные таблицы (конструкция ПОМЕСТИТЬ в запросах 1С) для поэтапной обработки данных. Это позволяет разбить сложный запрос на более простые шаги и проиндексировать промежуточные результаты, что может значительно ускорить выполнение.

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

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

  3. Оптимизируем условия ГДЕ:

    Старайтесь избегать разыменования ссылок через точку (Ссылка.Поле) в условии ГДЕ, если это возможно, особенно в подзапросах. Если поля типа Проведен или Склад нужны для отбора, они должны быть в основной таблице запроса или в таблице, к которой присоединяется основной запрос, и быть проиндексированы.

    В вашем запросе условия РеализацияТоваровУслугТовары.Ссылка.Проведен = ИСТИНА и РеализацияТоваровУслугТовары.Ссылка.Склад В(&СписокСкладов) в подзапросах являются источником медленной работы. Попробуйте перенести эти условия в основной запрос или убедитесь, что они применяются до агрегации.

    Также проверьте, что поля, участвующие в условиях ГДЕ и ПО, имеют индексы в базе данных (например, Проведен, Дата, Склад, Ссылка).

  4. Минимизируем количество полей в запросе:

    Если какие-то поля не нужны для отображения в динамическом списке, не включайте их в запрос. Меньше полей – быстрее выборка.

  5. Назначаем "Основную таблицу":

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

3. Общие рекомендации по работе с динамическими списками

Для обеспечения максимальной производительности динамических списков, мы должны также учитывать общие принципы их использования:

  1. Избегайте "тяжелых" вычислений: Не включайте в запрос динамического списка сложные функции, такие как ПОДСТРОКА, ЛЕВ(...,...), ВЫБОР КОГДА с множеством условий или вычисляемые поля, если их можно предрассчитать или получить проще.
  2. Работа с группировками и итогами: Динамические списки не предназначены для вывода отчетов с группировками и итогами. Если вам нужна такая функциональность, лучше использовать стандартные отчеты на СКД.
  3. Флаг "Динамическое считывание данных": Этот флаг управляет размером порций данных.
    • Если запрос выполняется медленно, но количество строк не очень большое, включение этого флага (по умолчанию 45 строк) может улучшить первоначальное время отображения, но может вызывать "подвисания" при прокрутке.
    • Если запрос работает быстро и строк много, отключение флага (по умолчанию 1000 строк) обеспечит более плавную прокрутку.
  4. Обработчик ПриПолученииДанныхНаСервере: Если в списке есть колонки, которые не используются для отбора, сортировки или группировки, но их данные сложны для получения основным запросом, можно получать их в этом обработчике. В нем можно одним запросом получить данные для всех отображаемых строк, уже после того, как основной запрос динамического списка выбрал базовые данные.
  5. Удаляем лишние колонки: Убедитесь, что в настройках формы динамического списка нет скрытых, но ненужных колонок. Даже если они не отображаются пользователю, их данные все равно извлекаются, что влияет на производительность.
  6. Анализируем план запроса: Используйте такие инструменты, как Консоль запросов 1С, а также SQL Server Profiler (или аналогичные для вашей СУБД), чтобы получить план выполнения запроса. Анализ плана позволит нам точно определить, на каких этапах СУБД тратит больше всего времени и выявить "узкие места".
  7. Регламентные операции СУБД: Регулярное обслуживание базы данных (обновление статистики, реорганизация/перестроение индексов) также имеет большое значение для общей производительности системы.

Заключение

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

Если это невозможно, тогда детальная оптимизация запроса с использованием ЛЕВОЕ СОЕДИНЕНИЕ, временных таблиц и внимательным подходом к условиям ГДЕ и индексам поможет значительно улучшить ситуацию. Всегда стремитесь к максимально простому запросу для динамического списка, а сложные вычисления выносите за его пределы.

← На главную