Как сделать запрос по всем документам с отбором по сериям и номенклатуре

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

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

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

Решение 1. Динамическое построение запроса через обход метаданных

Если нам действительно неизвестно, в каких именно документах и табличных частях содержатся реквизиты Номенклатура и СерияНоменклатуры, мы можем обойти дерево метаданных программно, найти нужные объекты и динамически сконструировать текст запроса.

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

  1. Отсутствие фильтрации по серии в самом запросе: Автор установил параметр СерияНоменклатуры, но забыл добавить условие фильтрации в текст запроса, из-за чего отбор происходил только по номенклатуре.
  2. Использование ОБЪЕДИНИТЬ вместо ОБЪЕДИНИТЬ ВСЕ: Оператор ОБЪЕДИНИТЬ (UNION) заставляет СУБД проверять строки на уникальность и удалять дубликаты, что сильно замедляет работу. Поскольку ссылки на документы уникальны по определению, необходимо использовать ОБЪЕДИНИТЬ ВСЕ (UNION ALL).
  3. Поздняя фильтрация данных: Автор собирал данные со всех документов во временную таблицу без отбора, а фильтр накладывал уже при чтении из временной таблицы. На реальной базе данных это приведет к переполнению базы временных таблиц (tempdb) и зависанию системы. Фильтровать данные нужно непосредственно при выборке из каждой табличной части.
  4. Разыменование полей: Использование конструкции ВременнаяТаблица.Номенклатура.Ссылка вместо прямой проверки значения ВременнаяТаблица.Номенклатура создает лишнее неявное соединение таблиц на уровне СУБД.

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


Процедура НайтиДокументыПоНоменклатуреИСерии(Кнопка)
    
    // Определяем искомые значения
    СсылкаНоменклатура = Справочники.Номенклатура.НайтиПоКоду("Н0021225");
    СсылкаСерия = Справочники.СерииНоменклатуры.НайтиПоКоду("000030631", Истина);
    
    Если СсылкаНоменклатура.Пустая() Или СсылкаСерия.Пустая() Тогда
        Предупреждение("Не найдены элементы для поиска! Проверьте коды.");
        Возврат;
    КонецЕсли;
    
    Запрос = Новый Запрос;
    Запрос.УстановитьПараметр("СсылкаНоменклатура", СсылкаНоменклатура);
    Запрос.УстановитьПараметр("СерияНоменклатуры", СсылкаСерия);
    
    МассивЗапросов = Новый Массив;
    
    // Обходим метаданные документов
    Для Каждого Документ Из Метаданные.Документы Цикл
        
        Для Каждого ТабличнаяЧасть Из Документ.ТабличныеЧасти Цикл
            
            ЕстьНоменклатура = Ложь;
            ЕстьСерия = Ложь;
            
            // Проверяем наличие нужных реквизитов в табличной части
            Для Каждого Реквизит Из ТабличнаяЧасть.Реквизиты Цикл
                Если Реквизит.Имя = "СерияНоменклатуры" Тогда
                    ЕстьСерия = Истина;
                ИначеЕсли Реквизит.Имя = "Номенклатура" Тогда
                    ЕстьНоменклатура = Истина;
                КонецЕсли;
            КонецЦикла;
            
            // Если оба реквизита присутствуют, формируем часть запроса
            Если ЕстьНоменклатура И ЕстьСерия Тогда
                
                ШаблонЗапроса = "
                |ВЫБРАТЬ
                |    ТЧ.Ссылка КАК Ссылка,
                |    ТЧ.Номенклатура КАК Номенклатура,
                |    ТЧ.СерияНоменклатуры КАК СерияНоменклатуры
                |ИЗ
                |    Документ.[ИмяДокумента].[ИмяТЧ] КАК ТЧ
                |ГДЕ
                |    ТЧ.Номенклатура = &СсылкаНоменклатура
                |    И ТЧ.СерияНоменклатуры = &СерияНоменклатуры";
                
                ТекстПодзапроса = СтрЗаменить(ШаблонЗапроса, "[ИмяДокумента]", Документ.Имя);
                ТекстПодзапроса = СтрЗаменить(ТекстПодзапроса, "[ИмяТЧ]", ТабличнаяЧасть.Имя);
                
                МассивЗапросов.Добавить(ТекстПодзапроса);
                
            КонецЕсли;
            
        КонецЦикла;
        
    КонецЦикла;
    
    // Если подходящих табличных частей не найдено, выходим
    Если МассивЗапросов.Количество() = 0 Тогда
        Сообщить("В конфигурации не найдены документы с подходящей структурой табличных частей.");
        Возврат;
    КонецЕсли;
    
    // Объединяем все запросы через ОБЪЕДИНИТЬ ВСЕ для максимальной производительности
    Разделитель = "
    |ОБЪЕДИНИТЬ ВСЕ
    |";
    
    Запрос.Текст = СтрСоединить(МассивЗапросов, Разделитель) + "
    |УПОРЯДОЧИТЬ ПО
    |    Ссылка";
    
    // Выполняем запрос
    ТаблицаРезультатов = Запрос.Выполнить().Выгрузить();
    
    // Выводим результат пользователю
    Для Каждого Строка Из ТаблицаРезультатов Цикл
        Сообщить(ШаблонСообщения("Найден документ: %1, Номенклатура: %2, Серия: %3", 
            Строка.Ссылка, Строка.Номенклатура, Строка.СерияНоменклатуры));
    КонецЦикла;
    
    Сообщить("Всего найдено строк: " + ТаблицаРезультатов.Количество());
    
КонецПроцедуры

В этом варианте фильтрация происходит на самом раннем этапе — на уровне СУБД при сканировании каждой конкретной таблицы. База данных использует индексы табличных частей (если они созданы для реквизитов Номенклатура и СерияНоменклатуры), что обеспечивает высокую скорость выполнения даже на крупных информационных базах.

Как отлаживать временные таблицы в запросах 1С

Во время обсуждения на форуме у автора возник вопрос: почему при установке точки останова на строке выполнения запроса он не видит данные временной таблицы. Разберем этот важный нюанс отладки.

Когда мы используем конструкцию ПОМЕСТИТЬ ВременнаяТаблица, запрос не возвращает результат в привычном виде. Данные сохраняются во временном хранилище СУБД, привязанном к менеджеру временных таблиц МВТ. Чтобы посмотреть содержимое временной таблицы в режиме отладки (начиная с платформы 8.3.15), выполните следующие действия:

  1. Установите точку останова сразу после выполнения запроса или на строчке ТаблицаЗначений = Запрос.Выполнить().Выгрузить();.
  2. Вызовите окно «Вычислить выражение» (сочетание клавиш Shift + F9).
  3. Введите имя переменной Запрос.
  4. Раскройте свойства объекта в дереве значений, найдите свойство МенеджерВременныхТаблиц.
  5. Внутри менеджера вы увидите коллекцию временных таблиц, их структуру и сможете просмотреть их содержимое прямо в режиме отладки.

Решение 2. Архитектурно правильный подход — запрос к регистрам

Хотя динамический запрос по метаданным решает задачу "в лоб", в реальной практике разработки в среде 1С обращение напрямую к табличным частям всех документов считается плохим тоном (антипаттерном) и используется в крайних случаях (например, при написании универсальных обработок поиска и замены дубликатов).

В типовых конфигурациях (таких как ERP, Комплексная автоматизация, Управление торговлей или УНФ) документы никогда не хранят данные изолированно. При проведении они всегда записывают информацию в регистры накопления или сведения (например, ТоварыНаСкладах, ДвиженияСерийНоменклатуры, ВыручкаИПродажи).

Вместо обхода десятков тяжелых таблиц документов гораздо эффективнее сделать простой и быстрый запрос к виртуальной таблице оборотов или остатков соответствующего регистра. Измерение Регистратор в результате такого запроса и будет являться искомым документом. Рассмотрим пример такого запроса к регистру накопления ТоварыНаСкладах:


Запрос = Новый Запрос;
Запрос.Текст = "
|ВЫБРАТЬ РАЗЛИЧНЫЕ
|    ТоварыНаСкладах.Регистратор КАК Документ,
|    ТоварыНаСкладах.Номенклатура КАК Номенклатура,
|    ТоварыНаСкладах.Серия КАК Серия
|ИЗ
|    РегистрНакопления.ТоварыНаСкладах КАК ТоварыНаСкладах
|ГДЕ
|    ТоварыНаСкладах.Номенклатура = &СсылкаНоменклатура
|    И ТоварыНаСкладах.Серия = &СерияНоменклатуры
|УПОРЯДОЧИТЬ ПО
|    Документ";

Запрос.УстановитьПараметр("СсылкаНоменклатура", НужнаяНоменклатура);
Запрос.УстановитьПараметр("СерияНоменклатуры", НужнаяСерия);

РезультатЗапроса = Запрос.Выполнить().Выгрузить();

Преимущества этого подхода:

Решение 3. Использование платформенного метода НайтиПоСсылкам()

Если вам нужно найти абсолютно все упоминания номенклатуры или серии во всей базе данных (не только в табличных частях документов, но и в реквизитах шапки, задачах, бизнес-процессах или справочниках), платформа предоставляет мощный оптимизированный механизм — метод НайтиПоСсылкам().

Проанализируем пример использования этого метода для решения нашей задачи:


// Поиск мест использования номенклатуры
МассивПоиска = Новый Массив;
МассивПоиска.Добавить(СсылкаНоменклатура);

ТаблицаМестИспользования = НайтиПоСсылкам(МассивПоиска);

// ТаблицаМестИспользования содержит колонки:
// Ссылка - объект, в котором найдена ссылка
// Данные - объект метаданных (или реквизит), где именно найдена ссылка

Для Каждого Строка Поиска Из ТаблицаМестИспользования Цикл
    
    СсылкаНаОбъект = СтрокаПоиска.Ссылка;
    МетаданныеОбъекта = СсылкаНаОбъект.Метаданные();
    
    // Проверяем, является ли найденный объект документом
    Если Метаданные.Документы.Содержит(МетаданныеОбъекта) Тогда
        
        // Теперь мы можем программно проверить, есть ли в этом документе нужная нам серия
        // (например, прочитав табличную часть найденного документа)
        
    КонецЕсли;
    
КонецЦикла;

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

Решение 4. Использование Критериев отбора

В платформе 1С:Предприятие 8 существует специализированный объект метаданных — Критерии отбора. Он предназначен именно для быстрого поиска документов по определенным значениям реквизитов (например, критерий отбора СвязанныеДокументы или ДокументыПоНоменклатуре).

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


Запрос = Новый Запрос;
Запрос.Текст = "
|ВЫБРАТЬ
|    Критерий.Ссылка КАК Документ
|ИЗ
|    КритерийОтбора.СвязанныеДокументы(&Номенклатура) КАК Критерий";

Запрос.УстановитьПараметр("Номенклатура", НужнаяНоменклатура);
ТаблицаДокументов = Запрос.Выполнить().Выгрузить();

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

← На главную