Как передать список счетов в условие запроса в 1С 7.7 на SQL базе данных

Программист 1С v7.7 1C:Бухгалтерия Бухгалтерский учет IT и автоматизация бизнеса
← На главную

При разработке отчетов и обработок в среде 1С:Предприятие 7.7 программисты часто сталкиваются со специфическим поведением штатного механизма запросов (так называемых «черных» запросов) при работе на СУБД MS SQL Server. Типичная ситуация: в коде формируется список значений, содержащий счета бухгалтерского учета, который затем передается в секцию Условие запроса. Если в списке находится один счет — запрос отрабатывает корректно. Но стоит добавить туда второй и последующие счета, как запрос начинает возвращать абсолютно пустой результат.

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

Выясняем причину: почему стандартный запрос не работает со списком счетов на SQL?

Для того чтобы понять природу этой ошибки, необходимо заглянуть под капот транслятора запросов 1С 7.7. Когда база данных работает в файловом режиме (DBF), разбор условий выполняет сам движок 1С, который корректно сравнивает объекты типа Счет. Однако в SQL-версии платформы штатный генератор запросов пытается перевести «черный» запрос на язык T-SQL.

Здесь и кроется главная проблема, состоящая из нескольких факторов:

  1. Особенности хранения счетов в SQL: В 1С 7.7 объект типа Счет является сложным агрегатным типом данных, жестко привязанным к конкретному плану счетов (в нашем случае — к пользовательскому или типовому плану счетов). На уровне таблиц SQL Server счета хранятся в специальной системной таблице _1SACCS. Идентификация счетов в проводках происходит по их внутренним уникальным 9-символьным идентификаторам (ID), а не по кодам счетов (таким как "44.2" или "23.1.1").
  2. Сбой трансляции оператора "В": Когда мы пишем конструкцию Условие (КорСчет в СписокКорСчетов), транслятор запросов 1С пытается преобразовать список значений в конструкцию SQL-запроса IN (...) или построить временную таблицу. При обработке списка объектов типа Счет транслятор 1С часто некорректно преобразует внутренние идентификаторы счетов в SQL-строки (сериализует их с ошибками или вовсе передает пустые значения). В результате SQL-сервер ищет совпадения по несуществующим ID, возвращая пустую выборку.
  3. Проблема проверки иерархии: Оператор в по умолчанию подразумевает иерархическое сравнение (вхождение субсчетов в группы счетов). На SQL-версии 1С 7.7 алгоритм раскрытия иерархии для списков счетов работает со сбоями, особенно если используются нетиповые (пользовательские) планы счетов.

Даже использование штатного метода списка значений Принадлежит() в виде Условие (СписокКорСчетов.Принадлежит(КорСчет) = 1) не решает проблему на SQL-базе, поскольку платформа пытается выполнить ту же самую некорректную трансляцию иерархического сравнения.

Рассмотрим рабочие способы обхода этого ограничения платформы.


Решение 1. Динамическое формирование текста запроса (штатный и надежный способ)

Как было замечено в обсуждении на форуме, если написать условие в запросе жестко через операторы сравнения и связки ИЛИ, то запрос отрабатывает идеально:


|Условие ((КорСчет = Счет_44) или (КорСчет = Счет_231) или (КорСчет = Счет_232));

Такой подход работает потому, что транслятор 1С в данном случае имеет дело с одиночными, заранее определенными переменными типа Счет, чьи внутренние ID он может безошибочно сопоставить с базой данных SQL. Но жестко прописывать счета в коде — плохая практика, ведь при добавлении нового счета придется переписывать программный код.

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

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


// Создаем список счетов для отбора
СписокКорСчетов = СоздатьОбъект("СписокЗначений");
СписокКорСчетов.ДобавитьЗначение(СчетПоКоду("23.1.1", ПланыСчетов.Новый));
СписокКорСчетов.ДобавитьЗначение(СчетПоКоду("23.1.2", ПланыСчетов.Новый));
СписокКорСчетов.ДобавитьЗначение(СчетПоКоду("44.2", ПланыСчетов.Новый));

// Формируем строковое условие динамически
СтрокаУсловия = "";
КоличествоСчетов = СписокКорСчетов.РазмерСписка();

Если КоличествоСчетов > 0 Тогда
    СтрокаУсловия = "Условие (";
    Для х = 1 По КоличествоСчетов Цикл
        // Динамически формируем имя переменной, например: СчетОтбора_1, СчетОтбора_2
        ИмяПеременной = "СчетОтбора_" + х;
        
        // Назначаем значение переменной в контексте модуля через Шаблон
        Шаблон("[" + ИмяПеременной + " = СписокКорСчетов.ПолучитьЗначение(" + х + ")]");
        
        // Добавляем условие в общую строку
        СтрокаУсловия = СтрокаУсловия + "(КорСчет = " + ИмяПеременной + ")";
        
        Если х < КоличествоСчетов Тогда
            СтрокаУсловия = СтрокаУсловия + " Или ";
        КонецЕсли;
    КонецЦикла;
    СтрокаУсловия = СтрокаУсловия + ");";
КонецЕсли;

Теперь мы можем собрать наш запрос, внедрив в него подготовленную строку условия:


ТекстЗапроса = "
|//{{ЗАПРОС(Сформировать)
|Период с (НачМесяца(ДатаДок)) по (КонМесяца(ДатаДок));
|Сумма = Операция.Сумма;
|Количество = ...
|... (остальные переменные запроса)
|Группировка Материалы;
|" + СтрокаУсловия + "
|//}}ЗАПРОС
|";

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

Преимущества метода: Полная универсальность, независимость от количества счетов в списке, сохранение высокой скорости работы штатного SQL-оптимизатора запросов.


Решение 2. Вызов функции модуля в условии (простой способ)

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

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


Функция ПроверитьСчетВСписке(ТекущийСчет) Экспорт
    // СписокКорСчетов должен быть доступен в контексте модуля (например, объявлен как Перем)
    Возврат СписокКорСчетов.Принадлежит(ТекущийСчет);
КонецФункции

Теперь в тексте запроса мы заменяем проблемное условие на вызов нашей функции:

|Условие (ПроверитьСчетВСписке(КорСчет) = 1);

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

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


Решение 3. Использование объекта "БухгалтерскиеИтоги" (наиболее правильный подход)

В среде разработчиков 1С 7.7 использование объекта Запрос для получения бухгалтерских проводок и оборотов считается устаревшей практикой. Черные запросы работают медленно и нестабильно, особенно на SQL. Платформа предоставляет мощный, специализированный и полностью оптимизированный инструмент — БухгалтерскиеИтоги.

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


БИ = СоздатьОбъект("БухгалтерскиеИтоги");

// Задаем используемый план счетов
БИ.ИспользоватьПланСчетов(ПланыСчетов.Новый);

// Указываем фильтр по субконто, если это необходимо
// Например, если нам нужны только определенные материалы
// БИ.ИспользоватьСубконто(ВидыСубконто.Материалы, СписокМатериалов);

// Настраиваем отбор по корреспондирующим счетам. 
// Передаем наш СписокКорСчетов третьим параметром метода ИспользоватьКорСубконто или ИспользоватьСубконто
// Но для отбора по самому кор. счету мы передаем список прямо в метод ВыполнитьЗапрос

// Параметры ВыполнитьЗапрос:
// 1 - Начало периода, 2 - Конец периода, 
// 3 - Основной счет отбора (Счет 10), 
// 4 - Корреспондирующий счет (передаем наш СписокКорСчетов!)
// 5 - Валюта, 6 - Тип итогов (2 - обороты между счетами)
Если БИ.ВыполнитьЗапрос(НачМесяца(ДатаДок), КонМесяца(ДатаДок), Счет_10, СписокКорСчетов, , 2, , "С") = 1 Тогда
    // Обходим результаты по субконто и кор. счетам
    БИ.ВыбратьСубконто(1);
    Пока БИ.ПолучитьСубконто(1) = 1 Цикл
        Материал = БИ.Субконто(1);
        СуммаОборота = БИ.КО(); // Сумма кредитового оборота в дебет счета 10
        КоличествоОборота = БИ.КО("К"); 
        
        // Можем детализировать до конкретного кор. счета из списка
        БИ.ВыбратьКорСчетов();
        Пока БИ.ПолучитьКорСчет() = 1 Цикл
            КонкретныйКорСчет = БИ.КорСчет;
            СуммаПоСчету = БИ.КО();
        КонецЦикла;
    КонецЦикла;
КонецЕсли;

Преимущества метода: Это самый быстрый и стабильный способ получения бухгалтерских данных в 1С 7.7. Он полностью оптимизирован под SQL-архитектуру, не подвержен багам транслятора черных запросов и корректно учитывает иерархию счетов и субсчетов.


Решение 4. Прямые SQL-запросы через 1С++ (для высоконагруженных систем)

Если ваша база данных имеет внушительный размер, а отчеты должны формироваться мгновенно, лучшим выбором будет использование внешней компоненты 1С++. Она позволяет отправлять прямые SQL-запросы к таблицам MS SQL Server в обход штатных механизмов 1С, что дает колоссальный прирост скорости (в десятки и сотни раз).

При использовании прямых запросов мы можем загрузить наш список счетов во временную таблицу в памяти SQL-сервера, а затем связать ее с таблицей проводок через конструкцию INNER JOIN или оператор IN.

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


// Создаем объект для выполнения прямого запроса
ODBCRecordSet = СоздатьОбъект("ODBCRecordset");

// Укладываем наш список счетов во временную таблицу на SQL-сервере
// Компонента автоматически преобразует объекты 1С в их SQL-идентификаторы
ODBCRecordSet.УложитьСписокОбъектов(СписокКорСчетов, "#ВТ_Счета");

// Формируем текст прямого SQL-запроса к таблице проводок (_1SENTRY)
ТекстSQL = "
|SELECT 
|    Журнал.SP1234 as [Материал $Справочник.Материалы],
|    SUM(Журнал.SUM) as [СуммаОборот],
|    SUM(Журнал.QTY) as [КолОборот]
|FROM 
|    _1SENTRY as Журнал (NOLOCK)
|WHERE 
|    Журнал.DATE_TIME_IDDOC BETWEEN :НачДата AND :КонДата
|    AND Журнал.ACID = :ИДСчета10
|    -- Фильтруем по списку кор. счетов с помощью временной таблицы
|    AND Журнал.CORR_ACID IN (SELECT Val FROM #ВТ_Счета)
|GROUP BY 
|    Журнал.SP1234
|";

// Устанавливаем параметры запроса
ODBCRecordSet.УстановитьТекстовыйПараметр("НачДата", НачМесяца(ДатаДок));
ODBCRecordSet.УстановитьТекстовыйПараметр("КонДата", КонМесяца(ДатаДок) + "~");
ODBCRecordSet.УстановитьТекстовыйПараметр("ИДСчета10", Счет_10);

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

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


Резюме: какой метод выбрать?

Выбор оптимального решения зависит от архитектуры вашей конфигурации и требований к скорости работы:

← На главную