При разработке печатных форм, отчетов или механизмов проведения документов в комплексной конфигурации 1С 7.7 (а также в конфигурации «Торговля и склад») часто возникает задача быстрого вывода текущего долга контрагента. Казалось бы, задача тривиальная. Однако на практике разработчики сталкиваются с серьезной архитектурной особенностью платформы 1С 7.7: взаиморасчеты с покупателями и поставщиками ведутся в разрезе измерений Фирма и Договор, в то время как измерение Контрагент в регистрах отсутствует.
Если у контрагента заведено несколько десятков или сотен договоров, стандартный перебор в цикле с вызовом функции СводныйОстаток по каждому отдельному договору приводит к лавинообразному росту обращений к базе данных. На крупных базах данных (размером от 10 Гб до 40 Гб и более) это гарантированно вешает систему и парализует работу пользователя. В этой статье мы подробно разберем, как обойти это ограничение платформы 1С 7.7, проанализируем внутреннее устройство индексов и рассмотрим четыре наиболее эффективных программных способа получения общего долга контрагента.
Для начала выясним причину, почему нативный метод Регистр.Покупатели.СводныйОстаток() при попытке опустить измерение договора работает крайне медленно. В базах данных 1С 7.7 (как в файловой DBF-версии, так и в SQL-версии) физический индекс таблицы итогов регистров строится строго в порядке следования измерений, заданном в конфигураторе. Например:
Если мы пытаемся получить остаток по фирме без указания конкретного договора, вызывая метод Рег.СводныйОстаток(ВыбФирма, , , , "СуммаРуб"), СУБД физически не может применить индекс по всей цепочке. Из-за пропуска второго измерения (договора) база данных вынуждена переходить от быстрого поиска по индексу (Index Seek) к полному сканированию таблицы итогов (Table/Index Scan). СУБД считывает абсолютно все записи по указанной фирме и вручную суммирует их. На больших базах данных это приводит к колоссальным задержкам.
Поэтому перед нами стоит задача: получить свернутый долг по конкретному контрагенту, минимизировав количество обращений к базе данных и заставив систему использовать индексы. Рассмотрим эффективные решения этой задачи.
Самый простой способ быстро получить итоги без привлечения сторонних библиотек — это использование стандартного встроенного языка запросов 1С. Платформа 1С 7.7 умеет самостоятельно транслировать обращения через точку (реквизиты типа «Справочник») в SQL-соединения или DBF-выборки.
Мы можем обратиться к измерению Договор, получить его реквизит Владелец (которым и является наш контрагент) и наложить условие непосредственно в тексте запроса. Посмотрим на практический пример реализации:
Процедура ПолучитьДолгЧернымЗапросом(ВыбКонтрагент, ВыбФирма)
Перем Запрос, ТекстЗапроса;
Запрос = СоздатьОбъект("Запрос");
ТекстЗапроса = "
|Период с (ТекущаяДата()) по (ТекущаяДата());
|ФирмаЗ = Регистр.Покупатели.Фирма;
|ДоговорЗ = Регистр.Покупатели.Договор;
|КонтрЗ = Регистр.Покупатели.Договор.Владелец;
|СуммаРуб = Регистр.Покупатели.СуммаРуб;
|Функция ДолгПокупателя = КонОст(СуммаРуб);
|Группировка ФирмаЗ;
|Условие (ФирмаЗ = ВыбФирма);
|Условие (КонтрЗ = ВыбКонтрагент);
|";
Если Запрос.Выполнить(ТекстЗапроса) = 0 Тогда
Предупреждение("Не удалось выполнить запрос по долгу!");
Возврат;
КонецЕсли;
Пока Запрос.Группировка(1) = 1 Цикл
Сообщить("Общий долг по покупателю " + ВыбКонтрагент + " составляет: " + Запрос.ДолгПокупателя);
КонецЦикла;
КонецПроцедуры
Важное условие для быстрой работы запроса: Чтобы этот запрос отрабатывал за доли секунды (в пределах 0.1 - 0.5 сек), в конфигураторе для измерения Договор в регистрах «Покупатели» и «Поставщики» обязательно должно быть включено свойство «Отбор итогов». Это заставляет систему поддерживать дополнительный индекс по договору, что существенно ускоряет отборы.
Если нам по какой-то причине не подходит стандартный запрос или мы хотим получить данные в виде таблицы значений для дальнейшего анализа, мы можем использовать связку методов УстановитьЗначениеФильтра и ВыгрузитьИтоги.
Напрямую передать список договоров в метод СводныйОстаток нельзя — это ограничение платформы. Но мы можем установить множественный фильтр на сам объект регистра. Разберем этот алгоритм по шагам:
СписокЗначений.Договор с типом сравнения "по списку" (параметр равен 2).ТаблицуЗначений.Посмотрим на пример кода, реализующего этот алгоритм:
Функция ПолучитьДолгЧерезВыгрузкуИтогов(ВыбКонтрагент, ВыбФирма) Экспорт
Перем СписокДоговоров, Запрос, ТекстЗапроса, ТЗ_Договоры;
Перем РегПокупатели, ТабИтогов, ИтоговыйДолг;
СписокДоговоров = СоздатьОбъект("СписокЗначений");
// Шаг 1. Получаем список договоров контрагента
Запрос = СоздатьОбъект("Запрос");
ТекстЗапроса = "обрабатывать все;
|Дог = Справочник.Договоры.ТекущийЭлемент;
|Контр = Справочник.Договоры.Владелец;
|Условие (Контр = ВыбКонтрагент);
|Группировка Дог без групп;";
Если Запрос.Выполнить(ТекстЗапроса) = 1 Тогда
ТЗ_Договоры = СоздатьОбъект("ТаблицаЗначений");
Запрос.Выгрузить(ТЗ_Договоры, 0, 0);
ТЗ_Договоры.Выгрузить(СписокДоговоров, , , "Дог");
КонецЕсли;
Если СписокДоговоров.РазмерСписка() = 0 Тогда
Возврат 0; // Нет договоров - нет долга
КонецЕсли;
// Шаг 2. Инициализируем регистр и устанавливаем фильтр
РегПокупатели = СоздатьОбъект("Регистр.Покупатели");
// Устанавливаем фильтр: 2 означает поиск по списку значений
РегПокупатели.УстановитьЗначениеФильтра("Договор", СписокДоговоров, 2);
РегПокупатели.УстановитьЗначениеФильтра("Фирма", ВыбФирма, 1);
// Шаг 3. Выгружаем итоги во временную таблицу значений
ТабИтогов = СоздатьОбъект("ТаблицаЗначений");
РегПокупатели.ВыгрузитьИтоги(ТабИтогов, 1, 1);
// Шаг 4. Суммируем результат
ИтоговыйДолг = ТабИтогов.Итог("СуммаРуб");
Возврат ИтоговыйДолг;
КонецФункции
Этот метод работает в разы быстрее, чем поочередное обращение к базе в цикле, поскольку обращение к таблице итогов регистра происходит всего один раз, а фильтрация выполняется на уровне движка базы данных.
Для клиент-серверных версий (на базе 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, мы можем в рамках одного запроса объединить итоги по регистрам «Покупатели» и «Поставщики», получив свернутое сальдо взаиморасчетов.
Иногда разработчики предлагают использовать механизм бухгалтерского учета для получения долга, ссылаясь на то, что на счетах расчетов (например, 62 «Расчеты с покупателями и заказчиками») субконто Контрагенты присутствует напрямую как первое субконто, что избавляет от необходимости возиться с договорами.
Рассмотрим пример кода получения долга через бухгалтерские итоги:
Функция ПолучитьДолгЧерезБухИтоги(ВыбКонтрагент)
Перем БИ;
БИ = СоздатьОбъект("БухгалтерскиеИтоги");
БИ.ИспользоватьСубконто(Субконто.Контрагенты, ВыбКонтрагент, 2);
// Выполняем расчет на текущую дату
БИ.ВыполнитьЗапрос(ТекущаяДата(), ТекущаяДата(), "62");
Возврат БИ.СКД("С"); // Сальдо конечное дебетовое по счету
КонецФункции
Почему этот метод не рекомендуется использовать в комплексной конфигурации 1С 7.7:
ВыполнитьЗапрос заставляет систему обращаться к тяжелым бухгалтерским таблицам. Если запускать этот расчет для каждого контрагента при выводе строк в печатных формах или динамических списках, производительность базы резко упадет.Проанализируем все рассмотренные методы, чтобы выбрать наиболее подходящий под ваши задачи:
| Метод | Скорость работы | Сложность реализации | Плюсы | Минусы |
|---|---|---|---|---|
| Стандартный запрос (Черный) | Высокая (до 0.5 сек) | Низкая | Не требует сторонних библиотек, стабильно работает везде. | Требует включенного отбора итогов по договору в конфигураторе. |
| ВыгрузитьИтоги с фильтром | Средняя | Средняя | Идеально для DBF-баз, гибкая работа с полученной таблицей значений. | Требует предварительной выборки договоров во временный список. |
| Прямой SQL (1С++) | Максимальная (2-10 мс) | Высокая | Моментальный результат на базах любого объема. | Работает только на SQL-версии, требует установки ВК 1С++. |
| Бухгалтерские итоги | Низкая | Низкая | Простой код, не требует учета договоров в структуре. | Неактуальные данные в оперативном учете, высокая нагрузка на сервер. |
Подведем итоги. Если вы работаете на файловой (DBF) версии 1С 7.7 или вам запрещено использовать внешние ВК, мы рекомендуем использовать Решение 2 (ВыгрузитьИтоги с фильтром по списку договоров) — оно обеспечивает максимальную стабильность и хорошую скорость. Если база переведена на MS SQL Server и у вас установлена библиотека 1С++, без сомнений используйте Решение 3 (Прямой SQL-запрос) — это гарантирует мгновенный отклик интерфейса при печати любых документов.