Как быстро получить общий долг контрагента по всем договорам в 1С 7.7?

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

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

Если у контрагента заведено несколько десятков или сотен договоров, стандартный перебор в цикле с вызовом функции СводныйОстаток по каждому отдельному договору приводит к лавинообразному росту обращений к базе данных. На крупных базах данных (размером от 10 Гб до 40 Гб и более) это гарантированно вешает систему и парализует работу пользователя. В этой статье мы подробно разберем, как обойти это ограничение платформы 1С 7.7, проанализируем внутреннее устройство индексов и рассмотрим четыре наиболее эффективных программных способа получения общего долга контрагента.

Почему стандартные механизмы работают медленно? Анализ проблемы индексов

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

  1. Фирма (первое измерение)
  2. Договор (второе измерение)
  3. КредДокумент (третье измерение)

Если мы пытаемся получить остаток по фирме без указания конкретного договора, вызывая метод Рег.СводныйОстаток(ВыбФирма, , , , "СуммаРуб"), СУБД физически не может применить индекс по всей цепочке. Из-за пропуска второго измерения (договора) база данных вынуждена переходить от быстрого поиска по индексу (Index Seek) к полному сканированию таблицы итогов (Table/Index Scan). СУБД считывает абсолютно все записи по указанной фирме и вручную суммирует их. На больших базах данных это приводит к колоссальным задержкам.

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

Решение 1. Оптимальный стандартный запрос 1С (Черный запрос)

Самый простой способ быстро получить итоги без привлечения сторонних библиотек — это использование стандартного встроенного языка запросов 1С. Платформа 1С 7.7 умеет самостоятельно транслировать обращения через точку (реквизиты типа «Справочник») в SQL-соединения или DBF-выборки.

Мы можем обратиться к измерению Договор, получить его реквизит Владелец (которым и является наш контрагент) и наложить условие непосредственно в тексте запроса. Посмотрим на практический пример реализации:


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

Важное условие для быстрой работы запроса: Чтобы этот запрос отрабатывал за доли секунды (в пределах 0.1 - 0.5 сек), в конфигураторе для измерения Договор в регистрах «Покупатели» и «Поставщики» обязательно должно быть включено свойство «Отбор итогов». Это заставляет систему поддерживать дополнительный индекс по договору, что существенно ускоряет отборы.

Решение 2. Использование метода ВыгрузитьИтоги с фильтром по списку договоров

Если нам по какой-то причине не подходит стандартный запрос или мы хотим получить данные в виде таблицы значений для дальнейшего анализа, мы можем использовать связку методов УстановитьЗначениеФильтра и ВыгрузитьИтоги.

Напрямую передать список договоров в метод СводныйОстаток нельзя — это ограничение платформы. Но мы можем установить множественный фильтр на сам объект регистра. Разберем этот алгоритм по шагам:

  1. Быстрым запросом выбираем все договоры нашего контрагента в СписокЗначений.
  2. Передаем этот список в качестве фильтра для измерения Договор с типом сравнения "по списку" (параметр равен 2).
  3. Выгружаем временные итоги в ТаблицуЗначений.
  4. Берем итог по колонке суммы.

Посмотрим на пример кода, реализующего этот алгоритм:


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

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

Решение 3. Прямой SQL-запрос через внешнюю компоненту 1С++ (Самый быстрый способ)

Для клиент-серверных версий (на базе MS SQL Server) самым мощным инструментом оптимизации является использование внешних компонент, в частности, популярной библиотеки 1С++. Прямой запрос к СУБД минует объектную модель 1С, работает напрямую с таблицами SQL и выполняется за тысячные доли секунды.

В базе данных SQL 1С 7.7 остатки регистров хранятся в таблицах RG<ID> (например, RG328). Нам необходимо соединить таблицу остатков регистра с таблицей справочника договоров SC<ID> по полю договора и отфильтровать по полю владельца (в SQL это поле PARENTEXT), где лежит ID нашего контрагента.

Проанализируем пример кода с использованием класса ODBCRecordset:


Функция ПолучитьДолгПрямымSQLЗапросом(ВыбКонтрагент, ВыбФирма) Экспорт
    Перем ЗапросSQL, ТекстЗапроса, ТабРезультат, ОбщийДолг;
    
    ЗапросSQL = СоздатьОбъект("ODBCRecordset");
    
    // Используем типизацию метаданных 1С++ для автоматического определения имен SQL таблиц
    ТекстЗапроса = "
    |SELECT 
    |   SUM(Reg.SPSum) AS Debt
    |FROM 
    |   $РегистрОстатки.Покупатели(:ВыбДата,, (Фирма, Договор), (СуммаРуб), ) AS Reg
    |INNER JOIN 
    |   $Справочник.Договоры AS Contracts ON Contracts.ID = Reg.Договор
    |WHERE 
    |   Reg.Фирма = :ВыбФирма 
    |   AND Contracts.PARENTEXT = :ВыбКонтрагент
    |";
    
    ЗапросSQL.УстановитьТекстовыйПараметр("ВыбДата", ТекущаяДата());
    ЗапросSQL.УстановитьТекстовыйПараметр("ВыбФирма", ВыбФирма);
    ЗапросSQL.УстановитьТекстовыйПараметр("ВыбКонтрагент", ВыбКонтрагент);
    
    ТабРезультат = ЗапросSQL.ВыполнитьИнструкцию(ТекстЗапроса);
    
    Если ТабРезультат.КоличествоСтрок() > 0 Тогда
        ОбщийДолг = ТабРезультат.ПолучитьЗначение(1, "Debt");
        Возврат Число(ОбщийДолг);
    Иначе
        Возврат 0;
    КонецЕсли;
КонецФункции

Преимущества прямого запроса: Это абсолютный чемпион по скорости. На базе объемом 40 Гб время выполнения такого запроса составляет от 2 до 10 миллисекунд. Кроме того, используя SQL оператор UNION ALL, мы можем в рамках одного запроса объединить итоги по регистрам «Покупатели» и «Поставщики», получив свернутое сальдо взаиморасчетов.

Решение 4. Использование Бухгалтерских Итогов (БухИтоги)

Иногда разработчики предлагают использовать механизм бухгалтерского учета для получения долга, ссылаясь на то, что на счетах расчетов (например, 62 «Расчеты с покупателями и заказчиками») субконто Контрагенты присутствует напрямую как первое субконто, что избавляет от необходимости возиться с договорами.

Рассмотрим пример кода получения долга через бухгалтерские итоги:


Функция ПолучитьДолгЧерезБухИтоги(ВыбКонтрагент)
    Перем БИ;
    БИ = СоздатьОбъект("БухгалтерскиеИтоги");
    БИ.ИспользоватьСубконто(Субконто.Контрагенты, ВыбКонтрагент, 2);
    
    // Выполняем расчет на текущую дату
    БИ.ВыполнитьЗапрос(ТекущаяДата(), ТекущаяДата(), "62");
    
    Возврат БИ.СКД("С"); // Сальдо конечное дебетовое по счету
КонецФункции

Почему этот метод не рекомендуется использовать в комплексной конфигурации 1С 7.7:

  1. Разделение учета: В комплексных базах оперативный учет (регистры) и бухгалтерский учет разделены. Проводки часто формируются отложенно (в конце дня, недели или месяца). Данные на счете 62 на текущую секунду могут быть просто не актуальны.
  2. Производительность: Метод ВыполнитьЗапрос заставляет систему обращаться к тяжелым бухгалтерским таблицам. Если запускать этот расчет для каждого контрагента при выводе строк в печатных формах или динамических списках, производительность базы резко упадет.

Сводная таблица сравнения методов

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

Метод Скорость работы Сложность реализации Плюсы Минусы
Стандартный запрос (Черный) Высокая (до 0.5 сек) Низкая Не требует сторонних библиотек, стабильно работает везде. Требует включенного отбора итогов по договору в конфигураторе.
ВыгрузитьИтоги с фильтром Средняя Средняя Идеально для DBF-баз, гибкая работа с полученной таблицей значений. Требует предварительной выборки договоров во временный список.
Прямой SQL (1С++) Максимальная (2-10 мс) Высокая Моментальный результат на базах любого объема. Работает только на SQL-версии, требует установки ВК 1С++.
Бухгалтерские итоги Низкая Низкая Простой код, не требует учета договоров в структуре. Неактуальные данные в оперативном учете, высокая нагрузка на сервер.

Рекомендации по выбору решения

Подведем итоги. Если вы работаете на файловой (DBF) версии 1С 7.7 или вам запрещено использовать внешние ВК, мы рекомендуем использовать Решение 2 (ВыгрузитьИтоги с фильтром по списку договоров) — оно обеспечивает максимальную стабильность и хорошую скорость. Если база переведена на MS SQL Server и у вас установлена библиотека 1С++, без сомнений используйте Решение 3 (Прямой SQL-запрос) — это гарантирует мгновенный отклик интерфейса при печати любых документов.

← На главную