Каждый разработчик 1С рано или поздно сталкивается с ситуацией, когда простой и логичный запрос к базе данных начинает сильно зависать. Типичный пример такой проблемы — получение движений конкретного документа по регистру бухгалтерии. Кажется, что база небольшая (или даже пустая файловая), документов «с гулькин нос», а банальная выборка проводок зависает на десятки секунд.
В этой статье мы подробно проанализируем, почему запрос к виртуальной таблице ДвиженияССубконто может выполняться крайне медленно, разберем ключевые архитектурные отличия запросов от объектного метода чтения наборов записей и рассмотрим пошаговые методы оптимизации производительности.
Рассмотрим классическую ситуацию. Разработчику необходимо получить движения одного или нескольких документов по определенным счетам дебета (например, 10.09 «Инвентарь и хозяйственные принадлежности» и 10.11 «Специальная оснастка и специальная одежда в эксплуатации»).
Для этого пишется следующий, на первый взгляд, простой и понятный запрос:
ВЫБРАТЬ
ХозрасчетныйДвиженияССубконто.СчетДт КАК СчетДт,
ХозрасчетныйДвиженияССубконто.Сумма КАК Сумма,
ХозрасчетныйДвиженияССубконто.СубконтоДт1 КАК Номенклатура,
ХозрасчетныйДвиженияССубконто.СубконтоДт3 КАК Сотрудник,
ХозрасчетныйДвиженияССубконто.Регистратор КАК Регистратор
ПОМЕСТИТЬ ВТДвижения
ИЗ
РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК ХозрасчетныйДвиженияССубконто
ГДЕ
ХозрасчетныйДвиженияССубконто.Регистратор В(&МассивОбъектов)
И (ХозрасчетныйДвиженияССубконто.СчетДт = &Счет10_9 ИЛИ ХозрасчетныйДвиженияССубконто.СчетДт = &Счет10_11)
При запуске этого кода на выполнение в файловой базе первый запуск занимает около 60 секунд, а последующие — в районе 21 секунды. При этом аналогичное чтение данных через объектную модель 1С (набор записей) выполняется практически мгновенно:
Движения = РегистрыБухгалтерии.Хозрасчетный.СоздатьНаборЗаписей();
Движения.Отбор.Регистратор.Установить(ВыборкаПоДокументам.Документ);
Движения.Прочитать();
Для Каждого Движение Из Движения Цикл
Если (Движение.СчетДт = ПланыСчетов.Хозрасчетный.ИнвентарьИХозяйственныеПринадлежностиВЭкспл)
ИЛИ (Движение.СчетДт = ПланыСчетов.Хозрасчетный.СпецодеждаВЭксплуатации) Тогда
НоваяСтрока = ТаблДвиж.Добавить();
НоваяСтрока.Сотрудник = Движение.СубконтоДт.РаботникиОрганизаций;
НоваяСтрока.Номенклатура = Движение.СубконтоДт.Номенклатура;
НоваяСтрока.Сумма = Движение.Сумма;
КонецЕсли;
КонецЦикла;
Выясним, почему возникает такая колоссальная разница в производительности и как заставить работать запрос так же быстро, как и объектный метод.
Для того чтобы понять корень проблемы, необходимо заглянуть на уровень СУБД (системы управления базами данных). Физическая структура хранения данных регистра бухгалтерии в 1С достаточно сложная.
Когда мы используем метод НаборЗаписей.Прочитать() с отбором по регистратору, платформа выполняет прямой и очень простой SQL-запрос к **физической таблице проводок** (движений) регистра. В этой таблице поле Регистратор входит в состав кластерного (основного) индекса. СУБД мгновенно находит нужные строки по индексу, считывает их и возвращает платформе. Аналитика (субконто) при этом дочитывается платформой из связанных таблиц по прямым ссылкам.
Но когда мы пишем запрос к виртуальной таблице ДвиженияССубконто, ситуация кардинально меняется:
ДвиженияССубконто физически не существует. Это динамически конструируемое представление.LEFT JOIN) основной таблицы движений, таблицы значений субконто и таблиц описания видов субконто.СубконтоДт1 и СубконтоДт3) платформа строит отдельное соединение таблиц.ГДЕ, СУБД сначала вынуждена построить всю огромную виртуальную таблицу со всеми соединениями для всей базы данных (или ее значительной части), и только потом отфильтровать результат по регистратору и счетам. Это приводит к сканированию всей базы (операции Index Scan или Table Scan) вместо быстрого поиска.Для ускорения работы запроса нам необходимо переписать его таким образом, чтобы СУБД могла эффективно применить индексы еще на этапе выборки строк, минимизируя количество соединений таблиц.
Самая грубая ошибка в исходном запросе — использование секции ГДЕ для фильтрации данных виртуальной таблицы. Условия по регистратору и счетам необходимо перенести непосредственно в параметры виртуальной таблицы. Это заставит платформу ограничить выборку записей еще на этапе обращения к физическим таблицам, до выполнения ресурсоемких соединений (JOIN).
Параметры виртуальной таблицы ДвиженияССубконто выглядят следующим образом: ДвиженияССубконто(&НачалоПериода, &КонецПериода, &УсловиеОтбора, &Порядок, &МаксимальноеКоличество).
Перенесем наши условия в третий параметр:
ВЫБРАТЬ
ХозрасчетныйДвиженияССубконто.СчетДт КАК СчетДт,
ХозрасчетныйДвиженияССубконто.Сумма КАК Сумма,
ХозрасчетныйДвиженияССубконто.СубконтоДт1 КАК Номенклатура,
ХозрасчетныйДвиженияССубконто.СубконтоДт3 КАК Сотрудник,
ХозрасчетныйДвиженияССубконто.Регистратор КАК Регистратор
ПОМЕСТИТЬ ВТДвижения
ИЗ
РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто(
,
,
СчетДт В (&МассивСчетов) И Регистратор В (&МассивОбъектов),
,
) КАК ХозрасчетныйДвиженияССубконто
В исходном запросе использовалось условие: СчетДт = &Счет10_9 ИЛИ СчетДт = &Счет10_11. Использование оператора ИЛИ (OR) в запросах к СУБД часто приводит к тому, что оптимизатор базы данных отказывается от использования индексов по полю СчетДт.
Правильным решением является объединение целевых счетов в массив (или список значений) и передача его в запрос одним параметром через оператор В (IN): СчетДт В (&МассивСчетов). Это позволяет оптимизатору СУБД построить эффективный план выполнения запроса с использованием индексов.
Индексы регистра бухгалтерии в СУБД строятся по композитному (составному) принципу, где первым полем практически всегда выступает Период (дата проводки). Если мы делаем запрос к виртуальной таблице без указания периода (первые два параметра), СУБД может начать сканирование таблиц с самого начала истории ведения учета в базе данных.
Даже если у вас есть жесткий отбор по регистраторам, всегда крайне рекомендуется ограничивать параметры начала и конца периода виртуальной таблицы. Период можно легко получить предварительным быстрым запросом к датам документов из &МассивОбъектов:
// Пример получения периода документов
ЗапросПериода = Новый Запрос;
ЗапросПериода.Текст =
"ВЫБРАТЬ
| МИНИМУМ(Док.Ссылка.Дата) КАК НачалоПериода,
| МАКСИМУМ(Док.Ссылка.Дата) КАК КонецПериода
|ИЗ
| Документ.ПередачаМатериаловВЭксплуатацию КАК Док
|ГДЕ
| Док.Ссылка В(&МассивОбъектов)";
ЗапросПериода.УстановитьПараметр("МассивОбъектов", МассивОбъектов);
Результат = ЗапросПериода.Выполнить().Выбрать();
Если Результат.Следующий() Тогда
ГраницаНачала = НачалоДня(Результат.НачалоПериода);
ГраницаКонца = КонецДня(Результат.КонецПериода);
КонецЕсли;
Передав эти границы в первый и второй параметры виртуальной таблицы, мы локализуем поиск СУБД в узком временном интервале.
Если в &МассивОбъектов передается большой список документов, СУБД может построить неоптимальный план из-за передачи списка ссылок в качестве литералов. Гораздо эффективнее сначала поместить этот массив во временную таблицу, проиндексировать её по полю ссылки, а затем соединять или использовать в параметрах виртуальной таблицы:
ВЫБРАТЬ
Документы.Ссылка КАК ДокументСсылка
ПОМЕСТИТЬ ВТ_ВыбранныеДокументы
ИЗ
&МассивОбъектов КАК Документы
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ХозрасчетныйДвиженияССубконто.СчетДт КАК СчетДт,
ХозрасчетныйДвиженияССубконто.Сумма КАК Сумма,
ХозрасчетныйДвиженияССубконто.СубконтоДт1 КАК Номенклатура,
ХозрасчетныйДвиженияССубконто.СубконтоДт3 КАК Сотрудник,
ХозрасчетныйДвиженияССубконто.Регистратор КАК Регистратор
ИЗ
РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто(
&НачалоПериода,
&КонецПериода,
СчетДт В (&МассивСчетов) И Регистратор В (ВЫБРАТЬ ДокументСсылка ИЗ ВТ_ВыбранныеДокументы),
,
) КАК ХозрасчетныйДвиженияССубконто
Важно знать, что начиная с версии платформы 8.3.15, фирма «1С» кардинально оптимизировала работу виртуальной таблицы ДвиженияССубконто. Был разработан механизм оптимизации, при котором значения субконто начали дублироваться непосредственно в полях основной физической таблицы проводок. Это позволило отказаться от тяжелых соединений (LEFT JOIN) при обращении к виртуальной таблице.
Однако эта оптимизация работает только при соблюдении следующих условий:
Автор исходного сообщения обратил внимание на разницу во времени выполнения: при первом запуске запрос отрабатывает за 60 секунд, а при последующих — за 21 секунду. Это поведение напрямую связано со спецификой файловой СУБД 1С.
Встроенная файловая СУБД имеет очень простой оптимизатор запросов по сравнению с промышленными решениями (MS SQL, PostgreSQL, Oracle). Она не умеет строить сложные и гибкие планы выполнения для вложенных соединений.
Разница во времени выполнения объясняется механизмом кэширования данных:
Для того чтобы ваши запросы к движениям документов по регистрам бухгалтерии всегда отрабатывали мгновенно, придерживайтесь следующих правил:
ГДЕ запроса к виртуальным таблицам.НачалоПериода и КонецПериода), предварительно получая даты анализируемых документов.ИЛИ в условиях отбора. Заменяйте их на передачу списка значений через оператор В.РегистрБухгалтерииНаборЗаписей и методу Прочитать() — этот способ работает на уровне физических таблиц СУБД и является самым быстрым из возможных.