Почему запрос 1С в цикле не возвращает данные, если параметр длиннее поля, и как оптимизировать такие запросы?

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

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

Предположим, у нас есть процедура, в которой выполняется вложенный цикл. Во внешнем цикле мы обходим некие "Колонки", выполняем первый запрос и, если находятся совпадения, создаем КопияТЗ. Затем во внутреннем цикле мы обходим строки этой КопияТЗ, и для каждой строки снова выполняем новый запрос, передавая в него значение из первой колонки текущей строки (Стр[0]). Основная проблема возникает, когда второй запрос перестает возвращать результаты для определенных значений, хотя в консоли запросов или при ручной проверке данные существуют.


// Исходная проблемная структура кода
Для каждого Колонка Из Колонки Цикл
    Запрос = Новый Запрос;
    Запрос.Текст = "ВЫБРАТЬ Наименование ИЗ Справочник1 ГДЕ Идентификатор = &Идентификатор";
    Запрос.УстановитьПараметр("Идентификатор", Колонка.Имя);
    Выборка = Запрос.Выполнить().Выбрать();
    Совпадение = Неопределено;
    Пока Выборка.Следующий() Цикл
        Совпадение = Выборка.Наименование; // Запоминаем только последнее совпадение
    КонецЦикла;

    Если Совпадение <> Неопределено Тогда
        // ТЗ - это какая-то существующая ТаблицаЗначений
        КопияТЗ = ТЗ.Скопировать(); 
        КопияТЗ.Свернуть(Колонка.Имя); // Сворачиваем по имени колонки, чтобы получить уникальные значения

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

Основная причина проблемы: Несоответствие длины строкового параметра и поля базы данных

Как выяснилось в ходе обсуждения на форуме, наиболее частая и неочевидная причина, по которой запрос не возвращает данные, даже если они, казалось бы, есть, кроется в несоответствии длины строкового параметра и максимальной длины поля в базе данных. В нашем случае, значение, которое передавалось в параметр &Значение, имело длину 140 символов, в то время как соответствующее поле в справочнике Справочник2 было ограничено 100 символами.

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

  1. Автоматическое обрезание: Система управления базами данных (СУБД) может автоматически обрезать передаваемое значение до максимально допустимой длины поля. Если в базе данных хранится полное значение (140 символов), а запрос ищет совпадение по обрезанному значению (100 символов), то, очевидно, совпадение не будет найдено.
  2. Несоответствие при сравнении: Даже если СУБД не обрезает значение явным образом, при выполнении оператора сравнения (например, =) она будет сравнивать две строки разной длины. "Длинный текст (140 символов)" не равен "Длинный текст (первые 100 символов)".

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

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

  1. Откройте конфигурацию в режиме "Конфигуратор".
  2. Перейдите к объекту метаданных (например, справочнику Справочник2), в котором находится проблемное поле.
  3. Найдите нужный реквизит (например, "ЗначениеПоля").
  4. В свойствах реквизита измените "Длину" на значение, которое заведомо больше максимальной длины ваших данных (например, 150 или 200).
  5. Сохраните конфигурацию и обновите базу данных.

После увеличения длины поля, запросы должны начать корректно находить совпадения.

Оптимизация запросов в цикле: Уход от "Запрос.Выполнить" в каждом шаге

Помимо решения проблемы с длиной строки, крайне важно обратить внимание на саму структуру кода. Использование Запрос.Выполнить() внутри цикла Для каждого Стр Из КопияТЗ Цикл является серьезным источником снижения производительности. Каждый вызов Запрос.Выполнить() требует установления соединения с базой данных (или использования пула соединений), компиляции запроса, его выполнения и получения результатов. Многократное повторение этих операций в цикле приводит к значительным накладным расходам и замедлению работы системы.

Общая идея оптимизации заключается в том, чтобы собрать все необходимые параметры или данные заранее и выполнить один большой запрос (или несколько, но не в цикле), вместо множества мелких.

  1. Передача массивов значений в параметр запроса

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

    Рассмотрим, как можно оптимизировать проблемный фрагмент кода, где для каждой строки КопияТЗ выполнялся отдельный запрос:

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

    Теперь давайте оптимизируем его с использованием СписокЗначений:

    
    // Оптимизированный фрагмент с использованием СписокЗначений
    // ...
    КопияТЗ = ТЗ.Скопировать(); 
    КопияТЗ.Свернуть(Колонка.Имя); 
    
    СписокЗначенийДляПоиска = Новый СписокЗначений();
    Для каждого Стр Из КопияТЗ Цикл
        // Убедимся, что передаем правильное значение из строки ТЗ
        СписокЗначенийДляПоиска.Добавить(Стр[0]); 
    КонецЦикла;
    
    Если СписокЗначенийДляПоиска.Количество() > 0 Тогда
        Запрос2 = Новый Запрос;
        // Теперь запрос будет выполнен ОДИН раз для всех значений
        Запрос2.Текст = "ВЫБРАТЬ ИД, ЗначениеПоля
                       |ИЗ Справочник2
                       |ГДЕ ЗначениеПоля В (&СписокЗначений)";
        Запрос2.УстановитьПараметр("СписокЗначений", СписокЗначенийДляПоиска);
                
        Выборка2 = Запрос2.Выполнить().Выбрать();
                
        // Сохраняем найденные ИД в соответствие для быстрой проверки
        СоответствиеНайденныхИД = Новый Соответствие();
        Пока Выборка2.Следующий() Цикл
            СоответствиеНайденныхИД.Вставить(Выборка2.ЗначениеПоля, Выборка2.ИД);
        КонецЦикла;
    
        // Теперь можем пройти по исходной КопияТЗ и проверить
        Для каждого Стр Из КопияТЗ Цикл
            Идентификатор = СоответствиеНайденныхИД.Получить(Стр[0]);
            Если Идентификатор = Неопределено Тогда
                // Значение не найдено, создаем новый элемент
                // СоздатьНовыйЭлементСправочника(Стр[0]);
                Сообщить("Для значения '" + Стр[0] + "' элемент справочника не найден и будет создан.");
            КонецЕсли;
        КонецЦикла;
    КонецЕсли;
    // ...
    

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

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

    Метод с временными таблицами особенно полезен, когда у вас есть сложный набор данных, по которым нужно выполнить несколько запросов, или когда массив значений слишком велик. Вы можете "поместить" ваши данные (например, из ТаблицыЗначений или результатов предыдущего запроса) во временную таблицу в самой базе данных, а затем обращаться к этой временной таблице в последующих частях запроса.

    Для работы с временными таблицами используйте МенеджерВременныхТаблиц.

    
    // Пример использования временных таблиц
    МВТ = Новый МенеджерВременныхТаблиц();
    
    // 1. Создадим ТаблицуЗначений, которую хотим поместить во временную таблицу
    ТаблицаИсходныхЗначений = Новый ТаблицаЗначений();
    ТаблицаИсходныхЗначений.Колонки.Добавить("ЗначениеДляПоиска", Новый ОписаниеТипов("Строка", , , 150)); // Учитываем длину строки!
    
    // Заполняем ТаблицаИсходныхЗначений из КопияТЗ
    КопияТЗ = ТЗ.Скопировать();
    КопияТЗ.Свернуть(Колонка.Имя); // Убедитесь, что Колонка.Имя - это правильное имя колонки
    Для каждого Стр Из КопияТЗ Цикл
        НоваяСтрока = ТаблицаИсходныхЗначений.Добавить();
        НоваяСтрока.ЗначениеДляПоиска = Стр[0]; // Важно: Стр[0] должно быть корректно
    КонецЦикла;
    
    // 2. Помещаем ТаблицуЗначений во временную таблицу
    ЗапросМВТ = Новый Запрос;
    ЗапросМВТ.МенеджерВременныхТаблиц = МВТ;
    ЗапросМВТ.Текст = 
        "ВЫБРАТЬ
        |    ЗначениеДляПоиска
        |ПОМЕСТИТЬ ВТ_ИсходныеЗначения
        |ИЗ
        |    &ТаблицаИсходныхЗначений КАК ТаблицаИсходныхЗначений";
    ЗапросМВТ.УстановитьПараметр("ТаблицаИсходныхЗначений", ТаблицаИсходныхЗначений);
    ЗапросМВТ.Выполнить(); // Теперь ВТ_ИсходныеЗначения доступна в МВТ
    
    // 3. Выполняем основной запрос, используя временную таблицу
    ЗапросОсновной = Новый Запрос;
    ЗапросОсновной.МенеджерВременныхТаблиц = МВТ;
    ЗапросОсновной.Текст = 
        "ВЫБРАТЬ
        |    Исходные.ЗначениеДляПоиска,
        |    Справочник2.ИД
        |ИЗ
        |    ВТ_ИсходныеЗначения КАК Исходные
        |        ЛЕВОЕ СОЕДИНЕНИЕ Справочник2 КАК Справочник2
        |        ПО Исходные.ЗначениеДляПоиска = Справочник2.ЗначениеПоля";
            
    ВыборкаРезультат = ЗапросОсновной.Выполнить().Выбрать();
    
    // 4. Обрабатываем результаты
    // Можно выгрузить в Соответствие для быстрой работы
    СоответствиеНайденныхИД = Новый Соответствие();
    Пока ВыборкаРезультат.Следующий() Цикл
        Если ВыборкаРезультат.ИД <> Неопределено Тогда
            СоответствиеНайденныхИД.Вставить(ВыборкаРезультат.ЗначениеДляПоиска, ВыборкаРезультат.ИД);
        КонецЕсли;
    КонецЦикла;
    
    // Использование СоответствиеНайденныхИД для дальнейшей логики
    Для каждого Стр Из КопияТЗ Цикл
        Идентификатор = СоответствиеНайденныхИД.Получить(Стр[0]);
        Если Идентификатор = Неопределено Тогда
            // Значение не найдено, создаем новый элемент
            // СоздатьНовыйЭлементСправочника(Стр[0]);
            Сообщить("Для значения '" + Стр[0] + "' элемент справочника не найден и будет создан.");
        КонецЕсли;
    КонецЦикла;
    

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

  3. Пакетные запросы

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

    
    // Пример пакетного запроса
    ЗапросПакет = Новый Запрос;
    ЗапросПакет.Текст = 
        "ВЫБРАТЬ
        |    Ссылка,
        |    Наименование
        |ИЗ
        |    Справочник1
        |ГДЕ
        |    ЭтоГруппа = ЛОЖЬ
        |;
        |
        |ВЫБРАТЬ
        |    Ссылка,
        |    Наименование
        |ИЗ
        |    Справочник2
        |ГДЕ
        |    ЭтоГруппа = ИСТИНА";
                
    РезультатПакета = ЗапросПакет.ВыполнитьПакет();
    
    // Первая выборка
    ВыборкаСправочник1 = РезультатПакета.Следующий();
    Пока ВыборкаСправочник1.Следующий() Цикл
        // Обработка данных из Справочник1
    КонецЦикла;
    
    // Вторая выборка
    ВыборкаСправочник2 = РезультатПакета.Следующий();
    Пока ВыборкаСправочник2.Следующий() Цикл
        // Обработка данных из Справочник2
    КонецЦикла;
    

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

  4. Использование виртуальных таблиц

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

  5. Собирание данных и обработка вне запроса

    В некоторых случаях, если объем данных не критически велик, можно выгрузить все необходимые данные из справочника или другого объекта в ТаблицуЗначений или Массив одним запросом. Затем дальнейшую фильтрацию, поиск или свертку выполнять уже средствами 1С (методами ТаблицыЗначений, Массива, Соответствия). Это переносит нагрузку с базы данных на сервер 1С, что может быть быстрее, чем множество мелких запросов.

Корректная передача параметра из СтрокиТаблицыЗначений

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

Обращение к значению колонки в СтрокеТаблицыЗначений возможно двумя способами:

  1. По индексу: Стр[0], Стр[1] и т.д. Это обращается к колонке по её порядковому номеру, начиная с нуля.
  2. По имени: Стр.ИмяКолонки. Это обращается к колонке по её строковому имени.

В большинстве случаев, особенно для обеспечения читаемости и устойчивости кода к изменениям порядка колонок, рекомендуется использовать обращение по имени колонки. Если вы знаете, что колонка, содержащая "Значение", называется, например, "НаименованиеКолонки", то правильно будет:


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

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


// Пример, если Стр[0] - это ссылка на объект, и нам нужно его Наименование
Запрос.УстановитьПараметр("Значение", Стр[0].Наименование); 

Всегда проверяйте тип и содержимое Стр[0] в отладчике, чтобы точно знать, что вы передаете в запрос.

Дополнительные рекомендации и советы по отладке

Для повышения надежности и производительности вашего кода, а также для эффективной отладки, рекомендуем использовать следующие приемы:

  1. Используйте СокрЛП(): При передаче строковых параметров в запрос всегда желательно использовать функцию СокрЛП() (Сократить левые и правые пробелы). Это помогает избежать проблем, связанных с лишними пробелами в начале или конце строки, которые могут помешать точному совпадению в базе данных. Пример: Запрос.УстановитьПараметр("Значение", СокрЛП(Стр[0]));
  2. Проверка на пустоту выборки: Если вам нужно просто проверить наличие записей, а не обходить их все, используйте Если НЕ Выборка.Пустой() Тогда ... КонецЕсли;. Это более эффективно, чем полный цикл Пока Выборка.Следующий() Цикл ... КонецЦикла;, если вам нужна только первая запись или факт наличия.
  3. Отладчик – ваш лучший друг: Не пренебрегайте использованием отладчика. Он позволяет пошагово отслеживать выполнение кода, проверять значения переменных и параметров запросов непосредственно перед их выполнением. Это самый надежный способ понять, что именно передается в запрос и что возвращается.
  4. Консоль запросов: Всегда тестируйте сложные или проблемные запросы в Консоли запросов 1С. Это позволяет изолировать проблему от остального кода и быстро экспериментировать с параметрами и текстом запроса.
  5. Логика обработки Совпадение: В исходном коде переменная Совпадение в первом цикле Пока Выборка.Следующий() Цикл Совпадение = Выборка.Наименование; КонецЦикла; будет содержать только последнее значение из выборки, если их было несколько. Если вам нужно обрабатывать все совпадения или проверять просто факт наличия, пересмотрите эту логику. Например, можно использовать булеву переменную:
    
    НайденоСовпадение = Ложь; Если Выборка.Следующий() Тогда НайденоСовпадение = Истина; КонецЕсли;
    
    .

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

← На главную