При разработке отчетов и обработок в среде 1С:Предприятие 7.7 программисты часто сталкиваются со специфическим поведением штатного механизма запросов (так называемых «черных» запросов) при работе на СУБД MS SQL Server. Типичная ситуация: в коде формируется список значений, содержащий счета бухгалтерского учета, который затем передается в секцию Условие запроса. Если в списке находится один счет — запрос отрабатывает корректно. Но стоит добавить туда второй и последующие счета, как запрос начинает возвращать абсолютно пустой результат.
В этой статье мы подробно разберем, почему возникает эта проблема, проанализируем внутренние механизмы платформы 1С 7.7 на SQL и рассмотрим несколько эффективных способов решения этой задачи — от простых штатных обходов до профессионального использования прямых запросов.
Для того чтобы понять природу этой ошибки, необходимо заглянуть под капот транслятора запросов 1С 7.7. Когда база данных работает в файловом режиме (DBF), разбор условий выполняет сам движок 1С, который корректно сравнивает объекты типа Счет. Однако в SQL-версии платформы штатный генератор запросов пытается перевести «черный» запрос на язык T-SQL.
Здесь и кроется главная проблема, состоящая из нескольких факторов:
Счет является сложным агрегатным типом данных, жестко привязанным к конкретному плану счетов (в нашем случае — к пользовательскому или типовому плану счетов). На уровне таблиц SQL Server счета хранятся в специальной системной таблице _1SACCS. Идентификация счетов в проводках происходит по их внутренним уникальным 9-символьным идентификаторам (ID), а не по кодам счетов (таким как "44.2" или "23.1.1").Условие (КорСчет в СписокКорСчетов), транслятор запросов 1С пытается преобразовать список значений в конструкцию SQL-запроса IN (...) или построить временную таблицу. При обработке списка объектов типа Счет транслятор 1С часто некорректно преобразует внутренние идентификаторы счетов в SQL-строки (сериализует их с ошибками или вовсе передает пустые значения). В результате SQL-сервер ищет совпадения по несуществующим ID, возвращая пустую выборку.в по умолчанию подразумевает иерархическое сравнение (вхождение субсчетов в группы счетов). На SQL-версии 1С 7.7 алгоритм раскрытия иерархии для списков счетов работает со сбоями, особенно если используются нетиповые (пользовательские) планы счетов.Даже использование штатного метода списка значений Принадлежит() в виде Условие (СписокКорСчетов.Принадлежит(КорСчет) = 1) не решает проблему на SQL-базе, поскольку платформа пытается выполнить ту же самую некорректную трансляцию иерархического сравнения.
Рассмотрим рабочие способы обхода этого ограничения платформы.
Как было замечено в обсуждении на форуме, если написать условие в запросе жестко через операторы сравнения и связки ИЛИ, то запрос отрабатывает идеально:
|Условие ((КорСчет = Счет_44) или (КорСчет = Счет_231) или (КорСчет = Счет_232));
Такой подход работает потому, что транслятор 1С в данном случае имеет дело с одиночными, заранее определенными переменными типа Счет, чьи внутренние ID он может безошибочно сопоставить с базой данных SQL. Но жестко прописывать счета в коде — плохая практика, ведь при добавлении нового счета придется переписывать программный код.
Мы можем автоматизировать этот процесс и формировать строковое условие запроса динамически в цикле, обходя каждый элемент нашего списка значений. Разберем этот алгоритм по шагам.
Поскольку запрос должен "видеть" переменные, которые мы сравниваем, мы объявим их в контексте модуля или создадим ограниченный массив переменных. Для этого напишем универсальный код:
// Создаем список счетов для отбора
СписокКорСчетов = СоздатьОбъект("СписокЗначений");
СписокКорСчетов.ДобавитьЗначение(СчетПоКоду("23.1.1", ПланыСчетов.Новый));
СписокКорСчетов.ДобавитьЗначение(СчетПоКоду("23.1.2", ПланыСчетов.Новый));
СписокКорСчетов.ДобавитьЗначение(СчетПоКоду("44.2", ПланыСчетов.Новый));
// Формируем строковое условие динамически
СтрокаУсловия = "";
КоличествоСчетов = СписокКорСчетов.РазмерСписка();
Если КоличествоСчетов > 0 Тогда
СтрокаУсловия = "Условие (";
Для х = 1 По КоличествоСчетов Цикл
// Динамически формируем имя переменной, например: СчетОтбора_1, СчетОтбора_2
ИмяПеременной = "СчетОтбора_" + х;
// Назначаем значение переменной в контексте модуля через Шаблон
Шаблон("[" + ИмяПеременной + " = СписокКорСчетов.ПолучитьЗначение(" + х + ")]");
// Добавляем условие в общую строку
СтрокаУсловия = СтрокаУсловия + "(КорСчет = " + ИмяПеременной + ")";
Если х < КоличествоСчетов Тогда
СтрокаУсловия = СтрокаУсловия + " Или ";
КонецЕсли;
КонецЦикла;
СтрокаУсловия = СтрокаУсловия + ");";
КонецЕсли;
Теперь мы можем собрать наш запрос, внедрив в него подготовленную строку условия:
ТекстЗапроса = "
|//{{ЗАПРОС(Сформировать)
|Период с (НачМесяца(ДатаДок)) по (КонМесяца(ДатаДок));
|Сумма = Операция.Сумма;
|Количество = ...
|... (остальные переменные запроса)
|Группировка Материалы;
|" + СтрокаУсловия + "
|//}}ЗАПРОС
|";
Запрос = СоздатьОбъект("Запрос");
Если Запрос.Выполнить(ТекстЗапроса) = 1 Тогда
// Обрабатываем результаты запроса
КонецЕсли;
Преимущества метода: Полная универсальность, независимость от количества счетов в списке, сохранение высокой скорости работы штатного SQL-оптимизатора запросов.
Еще один элегантный способ обойти ограничения транслятора — перенести проверку вхождения счета в список из SQL-сервера на сторону клиента 1С. Для этого мы можем использовать пользовательскую функцию в условии запроса.
Для начала опишем в модуле функцию, которая будет проверять, принадлежит ли переданный счет нашему списку:
Функция ПроверитьСчетВСписке(ТекущийСчет) Экспорт
// СписокКорСчетов должен быть доступен в контексте модуля (например, объявлен как Перем)
Возврат СписокКорСчетов.Принадлежит(ТекущийСчет);
КонецФункции
Теперь в тексте запроса мы заменяем проблемное условие на вызов нашей функции:
|Условие (ПроверитьСчетВСписке(КорСчет) = 1);
Важное примечание: Как только транслятор запросов видит в секции Условие вызов пользовательской функции, он автоматически отключает SQL-оптимизацию для этой части выборки. Платформа будет вынуждена получить из SQL-сервера все записи без этого фильтра, а затем построчно отфильтровать их на клиенте с помощью функции ПроверитьСчетВСписке.
Если объем данных за выбранный период невелик, этот способ является самым простым и удобным. На больших базах данных это может привести к заметному снижению производительности.
В среде разработчиков 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-архитектуру, не подвержен багам транслятора черных запросов и корректно учитывает иерархию счетов и субсчетов.
Если ваша база данных имеет внушительный размер, а отчеты должны формироваться мгновенно, лучшим выбором будет использование внешней компоненты 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, масштабируемость решения под любые задачи.
Выбор оптимального решения зависит от архитектуры вашей конфигурации и требований к скорости работы: