Разработчики, сопровождающие исторические базы данных на платформе 1С:Предприятие 7.7 SQL, регулярно сталкиваются с архитектурными ограничениями штатного генератора запросов (так называемого «черного запроса»). Один из самых коварных и трудноуловимых багов связан с фильтрацией данных по реквизитам подчиненных документов, когда тип реквизита-основания задан как неопределенный (то есть имеет общий тип Документ без указания конкретного вида).
В этой статье мы подробно разберем, почему возникает эта ошибка трансляции SQL, как ведет себя платформа в файловом (DBF) и серверном (SQL) режимах, и рассмотрим три практических способа обхода этой проблемы: от простых корректировок штатного запроса до построения высокопроизводительных прямых запросов с помощью внешней компоненты 1С++.
Рассмотрим классическую ситуацию. У нас есть документ РасходнаяНакладная, у которого реквизит ДокументОснование имеет неопределенный тип (может ссылаться на Заявку, Счет, Договор и т.д.). Нам необходимо выбрать все расходные накладные за определенный период, у которых дата документа-основания соответствует строго заданной дате.
Типичный встроенный запрос, который пытается написать разработчик, выглядит следующим образом:
// Пример проблемного запроса
Обрабатывать Все;
Период с гНачДата по гКонДата;
ОбрабатыватьДокументы Все;
ПеремОбъект = Документ.РасходнаяНакладная.ТекущийДокумент;
Группировка ПеремОбъект;
Без Итогов;
Перем1 = Документ.РасходнаяНакладная.ДокументОснование.ДатаДок;
Условие (Перем1 = гУсл1);
На уровне логики платформы этот запрос выглядит абсолютно корректным. Однако при работе на SQL-версии этот запрос либо возвращает пустой результат, либо игнорирует фильтр, отбирая документы вообще без учета даты основания. Проанализировав работу СУБД через SQL Server Profiler, мы увидим причину: встроенный парсер платформы, находящийся в библиотеке bkend.dll, некорректно транслирует обращение «через точку» к реквизитам неопределенного вида.
Поскольку вид документа-основания не определен в метаданных жестко, генератор SQL-запросов 1С 7.7 не знает заранее, с какими таблицами (например, DH123 или DH456) ему нужно связать (сделать LEFT JOIN) таблицу журнала документов _1SJOURN. В результате транслятор путается в генерации псевдонимов (aliases) таблиц. Вместо того чтобы наложить условие на поле даты таблицы-основания (например, T2.DATE_TIME_IDDOC), он ошибочно накладывает условие на дату самого основного документа (T1.DATE_TIME_IDDOC). Таким образом, условие поиска полностью подменяется.
Важно отметить различие между DBF и SQL версиями:
Попытки найти универсальный бинарный патч для bkend.dll, который исправлял бы этот баг парсера, не увенчались успехом — логика разбора запросов зашита глубоко внутри ядра платформы. Поэтому нам необходимо использовать обходные пути на уровне написания кода.
Если объем данных в вашей базе относительно невелик, или задача выполняется редко (например, ночной регламент), можно скорректировать синтаксис встроенного запроса так, чтобы платформа не пыталась вычислять реквизиты «через точку» в секции описания переменных запроса.
Мы можем убрать объявление промежуточной переменной Перем1 и перенести обращение к реквизиту непосредственно в секцию Условие, обращаясь к нему через группировочную переменную:
// Рабочий вариант встроенного запроса
Обрабатывать Все;
Период с гНачДата по гКонДата;
ОбрабатыватьДокументы Все;
ПеремОбъект = Документ.РасходнаяНакладная.ТекущийДокумент;
Группировка ПеремОбъект;
Без Итогов;
Условие (ПеремОбъект.ДокументОснование.ДатаДок = гУсл1);
Как это работает? При таком синтаксисе транслятор 1С 7.7 отказывается от попыток построить сложный SQL-запрос с объединением таблиц на стороне сервера. СУБД возвращает платформе плоский список документов за указанный период (с гНачДата по гКонДата), а фильтрация по условию даты документа-основания происходит уже на стороне клиента (в оперативной памяти компьютера пользователя). Это решает проблему неверного отбора, но порождает серьезный минус — низкую скорость работы на больших периодах, так как через сеть прокачивается избыточный объем данных.
Чтобы заставить SQL-сервер эффективно отфильтровать данные без использования сторонних компонент, мы можем разбить задачу на два простых и быстрых шага. Этот подход полностью исключает медленную фильтрацию на клиенте и не вызывает сбоев транслятора псевдонимов таблиц.
Выполним задачу по шагам:
СписокЗначений.ДокументОснование в этот заранее подготовленный список.Рассмотрим пример реализации этого алгоритма:
Процедура СформироватьОтбор()
// Шаг 1. Собираем документы-основания за нужную дату
СписокОснований = СоздатьОбъект("СписокЗначений");
// Предположим, основаниями могут быть документы "Счет" и "Заявка"
СписокВидов = СоздатьОбъект("СписокЗначений");
СписокВидов.ДобавитьЗначение("Счет");
СписокВидов.ДобавитьЗначение("ЗаявкаПокупателя");
Для Ном = 1 По СписокВидов.РазмерСписка() Цикл
ВидДока = СписокВидов.ПолучитьЗначение(Ном);
Выборка = СоздатьОбъект("Выборка.Документы." + ВидДока);
// Выбираем строго за нужную нам дату основания
Если Выборка.ВыбратьДокументы(гУсл1, гУсл1) = 1 Тогда
Пока Выборка.ПолучитьДокумент() = 1 Цикл
СписокОснований.ДобавитьЗначение(Выборка.ТекущийДокумент);
КонецЦикла;
EndЕсли;
КонецЦикла;
// Шаг 2. Основной запрос с фильтрацией по списку
Запрос = СоздатьОбъект("Запрос");
ТекстЗапроса = "
|Период с гНачДата по гКонДата;
|ОбрабатыватьДокументы Все;
|Без Итогов;
|Док = Документ.РасходнаяНакладная.ТекущийДокумент;
|Основание = Документ.РасходнаяНакладная.ДокументОснование;
|Группировка Док;
|Условие (Основание В СписокОснований);
|";
Если Запрос.Выполнить(ТекстЗапроса) = 1 Тогда
Пока Запрос.Группировка() = 1 Цикл
// Обработка результатов запроса
Сообщить("Найден документ: " + Запрос.Док);
КонецЦикла;
КонецЕсли;
КонецПроцедуры
Этот метод работает чрезвычайно быстро. SQL-сервер мгновенно отрабатывает конструкцию IN (...), используя существующие индексы по полю документа-основания в таблице шапки расходных накладных. Мы избегаем «грязных» соединений неопределенных типов данных и получаем гарантированно точный результат.
Если ваша база данных имеет внушительные размеры (десятки и сотни гигабайт), лучшим решением будет полный отказ от встроенного генератора запросов 1С 7.7 в пользу прямых запросов к СУБД с использованием внешней компоненты 1С++.
Для понимания логики прямого запроса важно знать, как 1С 7.7 хранит ссылки на документы неопределенного вида на уровне таблиц MS SQL Server. В таблице шапки документа (например, DH999) реквизит неопределенного вида хранится как символьное поле длиной 23 символа. Последние 9 символов этой строки содержат физический IDDOC (уникальный идентификатор документа-источника в базе данных), а первые символы определяют его вид.
В общем журнале документов _1SJOURN хранятся все важные метаданные, включая дату документа DATE_TIME_IDDOC (поле содержит дату и время в специальном строковом формате). Мы можем связать таблицу шапки расходной накладной с общим журналом дважды: первый раз для получения данных самой накладной, а второй раз — для получения даты документа-основания, вычленив его IDDOC из строки неопределенного типа.
Рассмотрим пример реализации прямого SQL-запроса:
Процедура ВыполнитьПрямойЗапрос()
// Инициализируем класс ODBCRecordset из внешней компоненты 1С++
БазаДанных = СоздатьОбъект("ODBCRecordset");
// Формируем текст T-SQL запроса
// Использована конструкция метаданных $Документ.РасходнаяНакладная для автоматического определения таблиц
ТекстЗапроса = "
|SELECT
| ЖурналНакладных.IDDOC [Накладная $Документ.РасходнаяНакладная],
| ЖурналНакладных.DOCNO as НомерНакладной,
| CAST(LEFT(ЖурналНакладных.DATE_TIME_IDDOC, 8) AS datetime) as ДатаНакладной
|FROM
| _1SJOURN as ЖурналНакладных
|INNER JOIN
| $Документ.РасходнаяНакладная as ШапкаНакладной ON ШапкаНакладной.IDDOC = ЖурналНакладных.IDDOC
|-- Связываем с журналом документов для получения данных документа-основания
|LEFT JOIN
| _1SJOURN as ЖурналОснований ON ЖурналОснований.IDDOC = RIGHT(ШапкаНакладной.$ДокОснование, 9)
|WHERE
| ЖурналНакладных.DATE_TIME_IDDOC BETWEEN :НачДата AND :КонДата~~
| AND ЖурналОснований.DATE_TIME_IDDOC BETWEEN :УслДатаНач AND :УслДатаКон~~
|";
// Передаем параметры в запрос. 1С++ автоматически приведет даты к формату SQL
БазаДанных.УстановитьТекстовыйПараметр("НачДата", гНачДата);
БазаДанных.УстановитьТекстовыйПараметр("КонДата", гКонДата);
// Задаем диапазон для даты основания (в пределах одних суток)
БазаДанных.УстановитьТекстовыйПараметр("УслДатаНач", гУсл1);
БазаДанных.УстановитьТекстовыйПараметр("УслДатаКон", гУсл1);
// Выполняем запрос и получаем результат в Таблицу Значений
ТаблицаРезультатов = БазаДанных.ВыполнитьИнструкцию(ТекстЗапроса);
// Выводим результат в окно сообщений
ТаблицаРезультатов.ВыбратьСтроки();
Пока ТаблицаРезультатов.ПолучитьСтроку() = 1 Цикл
Сообщить("Документ: " + ТаблицаРезультатов.Накладная + " № " + ТаблицаРезультатов.НомерНакладной);
КонецЦикла;
КонецПроцедуры
Преимущества прямого запроса:
IDDOC в таблице _1SJOURN.При возникновении проблем с отборами по реквизитам неопределенных видов документов в 1С 7.7 SQL, мы рекомендуем придерживаться следующих правил выбора решения: