Почему не работает метод Запрос.Получить() в 1С 7.7 при использовании групп справочников и как это исправить

Программист 1С v7.7 Управленческий учет Торговля и дистрибуция
← На главную

При разработке и сопровождении конфигураций на платформе 1С:Предприятие 7.7 программисты часто сталкиваются со специфическим поведением встроенного («черного») языка запросов. Одним из классических и наиболее труднодиагностируемых затруднений является сбой в работе метода Запрос.Получить(). В этой статье мы подробно разберем, почему данный метод возвращает 0 (ложь) при попытке позиционирования на группах (папках) справочников, изучим внутренние механизмы работы платформы и рассмотрим три эффективных способа обхода этой проблемы.

Суть проблемы и анализ исходного кода

Рассмотрим стандартную задачу: нам необходимо сопоставить данные из Таблицы Значений (ТЗ) с результатами выполнения запроса. Исходный код запроса выглядит следующим образом:


Период С НачДата По КонДата;
|ОбрабатыватьДокументы Проведенные;
|Склад = Документ.ПеремещениеТоваров.Склад;
|Магазин = Документ.ПеремещениеТоваров.СкладПолучатель.Родитель;
|Артикул = Документ.ПеремещениеТоваров.Товар.Родитель;
|Количество = Документ.ПеремещениеТоваров.Количество;
|Функция Кол = Сумма(Количество);
|Условие (Склад В ОсновнойСклад);
|Условие (Магазин В СписокМагазинов);
|Условие (Артикул В СписокАртикулов);
|Группировка Магазин;
|Группировка Артикул;

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


ТаблМагазины.ВыбратьСтроки();
Пока ТаблМагазины.ПолучитьСтроку() = 1 Цикл
    Запрос.ВНачалоВыборки();
    Если Запрос.Получить(ТаблМагазины.Магазин, ТаблМагазины.Артикул) = 1 Тогда
        ТаблМагазины.КолПриход = Запрос.Кол;
    КонецЕсли;
КонецЦикла;

При выполнении этого кода метод Запрос.Получить() всегда возвращает ложь, хотя при визуальной выгрузке результатов запроса в отладочную таблицу значений все данные присутствуют, а типы данных в ТаблМагазины (колонки Магазин и Артикул) полностью идентичны значениям группировок запроса. Обе колонки содержат ссылки на группы (родителей) справочников «Склады» и «Номенклатура» соответственно.

Выясняем причину: особенности архитектуры 1С 7.7

Чтобы понять, почему происходит сбой, нам необходимо проанализировать внутреннее устройство движка запросов 1С 7.7. Встроенный SQL-подобный интерпретатор («черный запрос») оптимизирован исключительно для работы со значениями типа «элемент». При построении дерева итогов в оперативной памяти платформа индексирует записи.

Когда мы объявляем переменную запроса через обращение к родителю элемента:

Магазин = Документ.ПеремещениеТоваров.СкладПолучатель.Родитель;

платформа получает ссылку на группу справочника. Физически в базе данных (будь то DBF-формат или СУБД MS SQL Server) у этой записи признак «ЭтоГруппа» (поле ISFOLDER) равен 1.

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

Важно отметить, что добавление в текст запроса инструкции Группировка Магазин Без групп; полностью уничтожит выборку. Так как переменная Магазин изначально инициализирована как Родитель (то есть всегда является группой), ключевое слово Без групп заставит платформу отфильтровать абсолютно все строки результатов запроса.


Решение 1. Использование промежуточной Таблицы Значений (Родной метод)

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

Шаг 1. Изменение текста запроса. Мы переводим выборку на уровень конечных элементов (складов и товаров), убирая обращение к родителю непосредственно в теле запроса:


Период С НачДата По КонДата;
|ОбрабатыватьДокументы Проведенные;
|Склад = Документ.ПеремещениеТоваров.Склад;
|СкладПолучатель = Документ.ПеремещениеТоваров.СкладПолучатель;
|Товар = Документ.ПеремещениеТоваров.Товар;
|Количество = Документ.ПеремещениеТоваров.Количество;
|Функция Кол = Сумма(Количество);
|Условие (Склад В ОсновнойСклад);
|Группировка СкладПолучатель Без групп;
|Группировка Товар Без групп;

Шаг 2. Выгрузка и постобработка в Таблице Значений. Мы выгружаем результат запроса в плоскую таблицу значений, добавляем в нее колонки для групп, заполняем их программно и сворачиваем таблицу по целевым группировкам:


// Выгружаем результат запроса в ТЗ
ТЗЗапроса = СоздатьОбъект("ТаблицаЗначений");
Запрос.Выгрузить(ТЗЗапроса, 0, 0);

// Добавляем новые колонки для хранения групп
ТЗЗапроса.НоваяКолонка("Магазин", "Справочник.Склады");
ТЗЗапроса.НоваяКолонка("Артикул", "Справочник.Номенклатура");

// Заполняем новые колонки родительскими группами
ТЗЗапроса.ВыбратьСтроки();
Пока ТЗЗапроса.ПолучитьСтроку() = 1 Цикл
    ТЗЗапроса.Магазин = ТЗЗапроса.СкладПолучатель.Родитель;
    ТЗЗапроса.Артикул = ТЗЗапроса.Товар.Родитель;
КонецЦикла;

// Сворачиваем таблицу по группам, суммируя показатели
ТЗЗапроса.Свернуть("Магазин, Артикул", "Кол");

Шаг 3. Сопоставление данных через поиск в ТЗ. Теперь мы можем легко сопоставить данные нашей исходной таблицы ТаблМагазины с обработанной ТЗЗапроса. Поиск в ТЗ по ссылкам на группы справочников работает безошибочно:


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

Решение 2. Быстрый поиск через Индексированную Таблицу (компонента 1С++)

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

Рассмотрим пример реализации с использованием 1С++:


// Подготавливаем ТЗЗапроса с группами, как описано в Решении 1
// ... (код заполнения колонок Магазин и Артикул) ...

// Создаем объект класса ИндексированнаяТаблица
ИндексированнаяТЗ = СоздатьОбъект("ИндексированнаяТаблица");
ИндексированнаяТЗ.Загрузить(ТЗЗапроса);

// Строим совмещенный индекс по двум колонкам (группам)
ИндексированнаяТЗ.ДобавитьИндекс("ИндексМагазинАртикул", "Магазин, Артикул");

// Выполняем быстрый поиск по индексу в цикле сопоставления
ТаблМагазины.ВыбратьСтроки();
Пока ТаблМагазины.ПолучитьСтроку() = 1 Цикл
    НомерНайденнойСтроки = 0;
    
    // Метод ПолучитьСтрокуПоРеквизитам осуществляет мгновенный поиск по построенному индексу
    Если ИндексированнаяТЗ.ПолучитьСтрокуПоРеквизитам(
        ТаблМагазины.Магазин, 
        ТаблМагазины.Артикул, 
        "Магазин, Артикул", 
        НомерНайденнойСтроки
    ) = 1 Тогда
        ИндексированнаяТЗ.ПолучитьСтрокуПоНомеру(НомерНайденнойСтроки);
        ТаблМагазины.КолПриход = ИндексированнаяТЗ.Кол;
    КонецЕсли;
КонецЦикла;

Данный подход является отраслевым стандартом для высоконагруженных систем на платформе 1С 7.7, так как время поиска сокращается с квадратичной зависимости $O(N^2)$ до константной $O(1)$.


Решение 3. Использование прямого SQL-запроса (Для SQL-версий 1С 7.7)

Если база данных работает под управлением MS SQL Server, лучшим решением будет полный отказ от встроенного интерпретатора запросов 1С в пользу прямых SQL-запросов через объект ODBCRecordset библиотеки 1С++. В реляционной базе данных MS SQL Server группы справочников хранятся в одной физической таблице с элементами и представляют собой обычные строки. Прямой запрос вернет готовую плоскую таблицу, в которой сопоставление по группам выполнится непосредственно на стороне СУБД силами SQL-сервера.

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


// Создаем объект для работы с прямым запросом к SQL Server
ЗапросSQL = СоздатьОбъект("ODBCRecordset");

ТекстЗапросаSQL = "
|SELECT 
|    ParentСклады.ID AS [Магазин $Справочник.Склады],
|    ParentТовары.ID AS [Артикул $Справочник.Номенклатура],
|    SUM(DocTab.SP450) AS Кол -- Предположим, SP450 - это реквизит Количество в таблице документа
|FROM 
|    [Document.ПеремещениеТоваров] AS DocHeader
|INNER JOIN 
|    [DocumentTab.ПеремещениеТоваров] AS DocTab ON DocHeader.IDDoc = DocTab.IDDoc
|INNER JOIN 
|    [Reference.Склады] AS Склады ON DocHeader.SP455 = Склады.ID -- Реквизит СкладПолучатель
|INNER JOIN 
|    [Reference.Склады] AS ParentСклады ON Склады.ParentID = ParentСклады.ID -- Выход на родителя склада
|INNER JOIN 
|    [Reference.Номенклатура] AS Товары ON DocTab.SP460 = Товары.ID -- Реквизит Товар
|INNER JOIN 
|    [Reference.Номенклатура] AS ParentТовары ON Товары.ParentID = ParentТовары.ID -- Выход на родителя товара
|WHERE 
|    DocHeader.Date_Time_IDDoc BETWEEN :НачДата AND :КонДата~~
|    AND DocHeader.Closed & 1 = 1 -- Только проведенные документы
|GROUP BY 
|    ParentСклады.ID, ParentТовары.ID
|";

// Передаем параметры дат в запрос
ЗапросSQL.УстановитьТекстовыйПараметр("НачДата", НачДата);
ЗапросSQL.УстановитьТекстовыйПараметр("КонДата", КонДата);

// Выполняем SQL-запрос и выгружаем результат в Индексированную Таблицу
РезультатSQL = ЗапросSQL.ВыполнитьИнструкцию(ТекстЗапросаSQL);

Этот способ полностью нивелирует любые архитектурные недостатки встроенного языка запросов платформы 1С 7.7. На уровне СУБД группировка по родителям выполняется максимально быстро благодаря индексам по полям ParentID (физическое поле родительской группы в таблицах справочников 1С 7.7).

Резюме и рекомендации

Подводя итоги нашего разбора, выделим ключевые правила работы с выборками в 1С 7.7:

  1. Никогда не используйте метод Запрос.Получить(), если в качестве параметров позиционирования выступают ссылки на группы справочников. Платформа аппаратно не поддерживает этот механизм.
  2. Для простых отчетов в небольших файловых базах данных (DBF) используйте Решение 1: делайте запрос по элементам, вычисляйте родителей в цикле обхода ТЗ, затем сворачивайте таблицу.
  3. Для крупных баз данных с интенсивным документооборотом применяйте Решение 2 (компоненту 1С++ и индексированные таблицы), чтобы избежать критического падения производительности системы при поиске данных.
  4. Если ваша база развернута на SQL-сервере, переводите тяжелые алгоритмы на прямые SQL-запросы (Решение 3). Это полностью исключит влияние ограничений платформы 1С 7.7 на скорость и стабильность работы ваших алгоритмов.
← На главную