Разработчики, работающие с платформой 1С:Предприятие 7.7, часто сталкиваются со странным поведением так называемых "черных" (обычных) запросов при работе с бухгалтерскими счетами. Типичная ситуация: в запрос передается список значений, содержащий счета (например, список корреспондирующих счетов), и накладывается условие фильтрации. Если в списке находится всего один счет, запрос отрабатывает абсолютно корректно и возвращает нужные данные. Но стоит добавить в этот список второй или третий счет, как запрос внезапно возвращает пустой результат, хотя проводки по этим счетам гарантированно есть в базе данных.
В рамках этой статьи мы подробно разберем, почему возникает эта ошибка, проанализируем внутренние механизмы работы платформы 1С 7.7 с базами данных SQL и рассмотрим несколько надежных практических способов обхода этой проблемы.
Описанная проблема — это классический и широко известный баг встроенного транслятора запросов 1С 7.7 при работе на СУБД MS SQL Server. На файловых базах данных (DBF) эта конструкция чаще всего работает корректно, поскольку локальный движок 1С выполняет фильтрацию своими силами. Однако на SQL-версии платформы происходит следующее:
Условие (КорСчет в СписокКорСчетов), она пытается транслировать ее в SQL-запрос с оператором IN.Счет (особенно если это конкретные субсчета разного уровня или группы), SQL-транслятор 1С сбивается и генерирует синтаксически некорректный или логически противоречивый T-SQL код.Поскольку транслятор не умеет корректно обрабатывать конструкцию В СписокЗначений для счетов на SQL, мы можем обойти это ограничение, заменив список явным перечислением условий равенства, соединенных логическим оператором ИЛИ. Вместо передачи списка мы программно сформируем текстовую строку условий.
Рассмотрим по шагам, как реализовать этот алгоритм в коде:
// Создаем список счетов для фильтрации
СписокКорСчетов = СоздатьОбъект("СписокЗначений");
СписокКорСчетов.ДобавитьЗначение(СчетПоКоду("23.1.1"));
СписокКорСчетов.ДобавитьЗначение(СчетПоКоду("23.1.2"));
СписокКорСчетов.ДобавитьЗначение(СчетПоКоду("44.2"));
// Программно собираем строку условий "ИЛИ"
СтрокаУсловий = "";
Для Икс = 1 По СписокКорСчетов.РазмерСписка() Цикл
ТекущийСчет = СписокКорСчетов.ПолучитьЗначение(Икс);
// Формируем имя переменной для запроса
ИмяПеременной = "ФильтрСчет_" + СокрЛП(Икс);
// Добавляем условие в строку
Если ПустаяСтрока(СтрокаУсловий) = 0 Тогда
СтрокаУсловий = СтрокаУсловий + " ИЛИ ";
КонецЕсли;
СтрокаУсловий = СтрокаУсловий + "(КорСчет = " + ИмяПеременной + ")";
КонецЦикла;
// Если список был пуст, создаем невыполнимое условие или пропускаем его
Если ПустаяСтрока(СтрокаУсловий) = 1 Тогда
СтрокаУсловий = "(1 = 0)"; // Запрос ничего не вернет
КонецЕсли;
Теперь мы должны внедрить динамически собранную строку условий в текст нашего запроса. Обратите внимание, что переменные, которые мы использовали в тексте условия (например, ФильтрСчет_1, ФильтрСчет_2), должны быть определены в контексте выполнения запроса как глобальные или локальные переменные модуля.
// Объявляем переменные, которые будут использоваться внутри запроса
// Количество переменных должно соответствовать размеру списка счетов
Для Икс = 1 По СписокКорСчетов.РазмерСписка() Цикл
ШаблонПеременной = "ФильтрСчет_" + СокрЛП(Икс);
ШаблонЗначения = "СписокКорСчетов.ПолучитьЗначение(" + Икс + ")";
Выполнить(ШаблонПеременной + " = " + ШаблонЗначения + ";");
КонецЦикла;
// Составляем текст запроса
ТекстЗапроса = "
|//{{ЗАПРОС(Сформировать)
|Период с (НачМесяца(ДатаДок)) по (КонМесяца(ДатаДок));
|Сумма = Операция.Сумма;
|Счет = Операция.Счет;
|КорСчет = ...; // Определение кор. счета
|// Другие реквизиты и группировки...
|Условие (" + СтрокаУсловий + ");
|"//}}ЗАПРОС
;
Запрос = СоздатьОбъект("Запрос");
Если Запрос.Выполнить(ТекстЗапроса) = 1 Тогда
// Обработка результатов запроса
КонецЕсли;
Этот метод гарантированно работает как на файловых, так и на SQL-версиях баз данных. Транслятор запросов без проблем переводит цепочку условий (КорСчет = ФильтрСчет_1) ИЛИ (КорСчет = ФильтрСчет_2) в стандартные SQL-конструкции равенства, которые отрабатывают быстро и корректно.
Еще один встроенный способ обойти некорректную трансляцию оператора В — использовать метод списка значений Принадлежит() непосредственно внутри текста условия запроса. Этот метод проверяет, входит ли переданное значение в указанный список.
Посмотрим на пример реализации данного подхода:
// Заполнение списка счетов
СписокКорСчетов = СоздатьОбъект("СписокЗначений");
СписокКорСчетов.ДобавитьЗначение(СчетПоКоду("23.1.1"));
СписокКорСчетов.ДобавитьЗначение(СчетПоКоду("23.1.2"));
СписокКорСчетов.ДобавитьЗначение(СчетПоКоду("44.2"));
// Формирование текста запроса с методом Принадлежит()
ТекстЗапроса = "
|//{{ЗАПРОС(Сформировать)
|Период с (НачМесяца(ДатаДок)) по (КонМесяца(ДатаДок));
|Сумма = Операция.Сумма;
|КорСчет = Операция.КорСчет;
|// ... группировки
|Условие (СписокКорСчетов.Принадлежит(КорСчет) = 1);
|"//}}ЗАПРОС
;
Важное предупреждение о производительности: Этот способ прост в написании, однако он имеет существенный недостаток при работе на SQL-версии базы данных. Транслятор 1С часто не может преобразовать метод Принадлежит() в эффективный SQL-код. В результате система выполняет фильтрацию "на клиенте", выгружая из базы данных избыточные объемы информации во временные локальные таблицы. На базах данных большого объема это может привести к колоссальному падению скорости выполнения отчета.
Если вам критически важно сохранить исходную лаконичную структуру запроса с оператором в СписокКорСчетов, вы можете временно запретить системе транслировать этот запрос в SQL-код. Для этого используется метод ВключитьSQL(0) объекта Запрос.
Проанализируем пример такого кода:
Запрос = СоздатьОбъект("Запрос");
// Принудительно отключаем выполнение запроса силами SQL-сервера
Запрос.ВключитьSQL(0);
ТекстЗапроса = "
|Период с (НачМесяца(ДатаДок)) по (КонМесяца(ДатаДок));
|Сумма = Операция.Сумма;
|КорСчет = Операция.КорСчет;
|Условие (КорСчет в СписокКорСчетов);
|";
Если Запрос.Выполнить(ТекстЗапроса) = 1 Тогда
// Результат будет корректным, так как фильтрация происходит локально
КонецЕсли;
Внимание: Этот метод категорически не рекомендуется использовать в рабочих высоконагруженных базах данных SQL. Отключение трансляции SQL заставит платформу выкачать всю таблицу проводок (или операций) за указанный период на локальный компьютер пользователя во временные DBF-файлы для последующей фильтрации. Это создает огромную нагрузку на сеть и дисковую подсистему сервера.
В ходе обсуждения разработчики часто предлагают использовать специализированный объект БухгалтерскиеИтоги (так называемый "бухгалтерский запрос") вместо универсального "черного" запроса. Бухгалтерские итоги работают значительно быстрее и оптимизированы для расчета остатков и оборотов.
Однако в архитектуре 1С 7.7 у объекта БухгалтерскиеИтоги есть жесткое ограничение: нельзя одновременно накладывать фильтрацию по аналитике основного счета и аналитике корр. счета. То есть вы не можете одновременно использовать методы ИспользоватьСубконто() для основного счета и ИспользоватьКорСубконто() для корр. счета в рамках одного запроса.
Если в вашем отчете требуется, например, выбрать данные, где субконто основного счета — это "Материалы", а корреспондирующее субконто — конкретный "Элемент затрат" (как в примере из темы форума), то объект БухгалтерскиеИтоги не позволит это сделать напрямую. Именно поэтому разработчикам приходится возвращаться к универсальным запросам по таблице операций и проводок, сталкиваясь со всеми багами SQL-транслятора.
Для крупных и производительных конфигураций на платформе 1С 7.7 SQL золотым стандартом решения подобных проблем является использование внешней компоненты 1С++. С ее помощью разработчик может написать прямой SQL-запрос к таблице проводок _1SENTRY, минуя внутренний транслятор 1С.
При использовании прямых запросов передача списка счетов реализуется через классическую SQL-конструкцию IN, которая отрабатывает мгновенно:
// Пример логики прямого SQL-запроса через 1С++
// Формируется классический SQL-запрос к таблице проводок
ТекстЗапроса = "
|SELECT
| Doc.IDDoc as [Документ $Документ],
| Entry.Summa as Сумма
|FROM
| _1SENTRY as Entry
|INNER JOIN
| _1SJOURN as Doc ON Entry.IDDoc = Doc.IDDoc
|WHERE
| Entry.CorrAccID IN (Select Val From :СписокСчетов)
|";
Этот подход полностью снимает все ограничения платформы по производительности и функциональности, однако требует установки внешней компоненты и базовых знаний синтаксиса T-SQL.
Если вы столкнулись с пустым результатом при передаче списка счетов в условие запроса 1С 7.7:
ВключитьSQL(0) и метода Принадлежит() на больших базах данных, так как это может парализовать работу пользователей из-за перегрузки сети.