Как исправить ошибку отбора по дате документа-основания в запросах 1С 7.7 на SQL?

Программист 1С v7.7 IT и автоматизация бизнеса
← На главную

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

В этой статье мы подробно разберем, почему возникает эта ошибка трансляции SQL, как ведет себя платформа в файловом (DBF) и серверном (SQL) режимах, и рассмотрим три практических способа обхода этой проблемы: от простых корректировок штатного запроса до построения высокопроизводительных прямых запросов с помощью внешней компоненты 1С++.

Почему возникает баг трансляции в SQL (анализ работы bkend.dll)

Рассмотрим классическую ситуацию. У нас есть документ РасходнаяНакладная, у которого реквизит ДокументОснование имеет неопределенный тип (может ссылаться на Заявку, Счет, Договор и т.д.). Нам необходимо выбрать все расходные накладные за определенный период, у которых дата документа-основания соответствует строго заданной дате.

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


// Пример проблемного запроса
Обрабатывать Все;
Период с гНачДата по гКонДата;
ОбрабатыватьДокументы Все;
ПеремОбъект = Документ.РасходнаяНакладная.ТекущийДокумент;
Группировка ПеремОбъект;              
Без Итогов;
Перем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 версиями:

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

Попытки найти универсальный бинарный патч для bkend.dll, который исправлял бы этот баг парсера, не увенчались успехом — логика разбора запросов зашита глубоко внутри ядра платформы. Поэтому нам необходимо использовать обходные пути на уровне написания кода.


Решение 1. Фильтрация через обращение к атрибутам объекта (Простой обход)

Если объем данных в вашей базе относительно невелик, или задача выполняется редко (например, ночной регламент), можно скорректировать синтаксис встроенного запроса так, чтобы платформа не пыталась вычислять реквизиты «через точку» в секции описания переменных запроса.

Мы можем убрать объявление промежуточной переменной Перем1 и перенести обращение к реквизиту непосредственно в секцию Условие, обращаясь к нему через группировочную переменную:


// Рабочий вариант встроенного запроса
Обрабатывать Все;
Период с гНачДата по гКонДата;
ОбрабатыватьДокументы Все;
ПеремОбъект = Документ.РасходнаяНакладная.ТекущийДокумент;
Группировка ПеремОбъект;              
Без Итогов;
Условие (ПеремОбъект.ДокументОснование.ДатаДок = гУсл1);

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


Решение 2. Двухэтапный запрос с использованием Списка Значений (Оптимальный штатный метод)

Чтобы заставить SQL-сервер эффективно отфильтровать данные без использования сторонних компонент, мы можем разбить задачу на два простых и быстрых шага. Этот подход полностью исключает медленную фильтрацию на клиенте и не вызывает сбоев транслятора псевдонимов таблиц.

Выполним задачу по шагам:

  1. Сначала мы выберем все возможные документы-основания конкретных видов за интересующую нас дату и поместим их в СписокЗначений.
  2. Затем во втором запросе мы отберем наши расходные накладные, наложив условие вхождения их реквизита ДокументОснование в этот заранее подготовленный список.

Рассмотрим пример реализации этого алгоритма:


Процедура СформироватьОтбор()
    // Шаг 1. Собираем документы-основания за нужную дату
    СписокОснований = СоздатьОбъект("СписокЗначений");
    
    // Предположим, основаниями могут быть документы "Счет" и "Заявка"
    СписокВидов = СоздатьОбъект("СписокЗначений");
    СписокВидов.ДобавитьЗначение("Счет");
    СписокВидов.ДобавитьЗначение("ЗаявкаПокупателя");
    
    Для Ном = 1 По СписокВидов.РазмерСписка() Цикл
        ВидДока = СписокВидов.ПолучитьЗначение(Ном);
        Выборка = СоздатьОбъект("Выборка.Документы." + ВидДока);
        // Выбираем строго за нужную нам дату основания
        Если Выборка.ВыбратьДокументы(гУсл1, гУсл1) = 1 Тогда
            Пока Выборка.ПолучитьДокумент() = 1 Цикл
                СписокОснований.ДобавитьЗначение(Выборка.ТекущийДокумент);
            КонецЦикла;
        EndЕсли;
    КонецЦикла;
    
    // Шаг 2. Основной запрос с фильтрацией по списку
    Запрос = СоздатьОбъект("Запрос");
    ТекстЗапроса = "
    |Период с гНачДата по гКонДата;
    |ОбрабатыватьДокументы Все;
    |Без Итогов;
    |Док = Документ.РасходнаяНакладная.ТекущийДокумент;
    |Основание = Документ.РасходнаяНакладная.ДокументОснование;
    |Группировка Док;
    |Условие (Основание В СписокОснований);
    |";
    
    Если Запрос.Выполнить(ТекстЗапроса) = 1 Тогда
        Пока Запрос.Группировка() = 1 Цикл
            // Обработка результатов запроса
            Сообщить("Найден документ: " + Запрос.Док);
        КонецЦикла;
    КонецЕсли;
КонецПроцедуры

Этот метод работает чрезвычайно быстро. SQL-сервер мгновенно отрабатывает конструкцию IN (...), используя существующие индексы по полю документа-основания в таблице шапки расходных накладных. Мы избегаем «грязных» соединений неопределенных типов данных и получаем гарантированно точный результат.


Решение 3. Прямой SQL-запрос через 1С++ (Максимальная производительность)

Если ваша база данных имеет внушительные размеры (десятки и сотни гигабайт), лучшим решением будет полный отказ от встроенного генератора запросов 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 Цикл
        Сообщить("Документ: " + ТаблицаРезультатов.Накладная + " № " + ТаблицаРезультатов.НомерНакладной);
    КонецЦикла;
КонецПроцедуры

Преимущества прямого запроса:


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

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

← На главную