При разработке на платформе «1С:Предприятие 8» программисты часто сталкиваются с необходимостью фильтрации и сопоставления данных из различных таблиц значений. Однако неверный выбор инструментов для этой задачи может привести к катастрофическому падению производительности системы. В этой статье мы подробно разберем реальный пример кода, содержащий целый букет классических ошибок разработки, проанализируем причины их возникновения и рассмотрим два правильных, оптимизированных варианта решения проблемы.
Для начала проанализируем фрагмент кода, который был представлен автором на обсуждение. Процедура предназначена для обхода таблицы НДФЛ и получения отфильтрованных данных из таблицы начислений по каждому сотруднику и статье финансирования:
Процедура Сформировать(ТаблицаПроводок,ДопПарам,ТаблицаНДФЛ,ТаблицаНачислений)
Если ТаблицаНДФЛ.Количество() = 0 Тогда
Возврат;
КонецЕсли;
НомерСтроки = 0;
Для каждого СтрокаТаблицы Из ТаблицаНДФЛ Цикл
ОтобратьТаблицуПоСотруднику(ТаблицаНачислений,СтрокаТаблицы.ФизическоеЛицо,СтрокаТаблицы.СтатьяФинансирования);
КонецЦикла;
КонецПроцедуры
Функция ОтобратьТаблицуПоСотруднику(Таблица,ФизЛицо,КостЦентр)
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ТаблицаДляОтбора",Таблица);
Запрос.УстановитьПараметр("ФизическоеЛицо",Физлицо);
Запрос.УстановитьПараметр("КостЦентр",КостЦентр);
Запрос.Текст =
"Выбрать
| *
|ПОМЕСТИТЬ ВТ_ТаблицаДляОтбора
| ИЗ
| &ТаблицаДляОтбора КАК ТаблицаДляОтбора
| ;
| //////////////////////////////////////////////
|Выбрать
| *
| ИЗ
| ВТ_ТаблицаДляОтбора КАК ВТ_ТаблицаДляОтбора
| ГДЕ
| ВТ_ТаблицаДляОтбора.ФизическоеЛицо = &ФизическоеЛицо
| И ВТ_ТаблицаДляОтбора.СтатьяФинансирования = &КостЦентр";
Возврат Запрос.Выполнить.Выгрузить();
КонецФункции
Этот код содержит как грубые архитектурные ошибки (антипаттерны), так и синтаксические баги, из-за которых программа просто не сможет выполниться. Давайте подробно разберем каждую проблему по шагам.
В процедуре Сформировать мы видим цикл Для каждого СтрокаТаблицы Из ТаблицаНДФЛ, внутри которого на каждой итерации вызывается функция ОтобратьТаблицуПоСотруднику, выполняющая запрос к базе данных. Если в таблице НДФЛ будет 10 000 строк, система выполнит 10 000 отдельных обращений к СУБД. Это порождает колоссальную нагрузку на сетевой стек, сервер приложений и саму СУБД из-за постоянной компиляции SQL-запросов и создания временных таблиц. Такое решение абсолютно не масштабируемо.
Использование конструкции &ТаблицаДляОтбора в качестве источника данных для запроса накладывает жесткие ограничения. Чтобы платформа могла передать таблицу значений в СУБД, все колонки таблицы должны быть строго типизированы. Если хотя бы у одной колонки тип будет неопределен или задан как «Произвольный», система выдаст критическую ошибку: «Тип не может быть выбран в запросе». Кроме того, передача больших таблиц в параметры запроса заставляет платформу на лету создавать временную таблицу в СУБД и построчно записывать туда данные, что крайне неэффективно внутри цикла.
Официальные стандарты разработки «1С» категорически запрещают использовать «*» в рабочих запросах. Во-первых, это приводит к избыточности данных (overfetching): если в таблице начислений 40 колонок, а нам нужны только 2, система будет пересылать по сети и держать в памяти лишний объем информации. Во-вторых, это делает код нестабильным: при изменении состава колонок исходной таблицы запрос может упасть или вернуть непредсказуемый результат.
Проанализируем строку Возврат Запрос.Выполнить.Выгрузить();. В ней пропущены круглые скобки после метода Выполнить. Корректный вызов должен выглядеть так: Запрос.Выполнить().Выгрузить(). Без скобок платформа выдаст ошибку компиляции.
Кроме того, в цикле результат вызова функции ОтобратьТаблицуПоСотруднику никак не обрабатывается и никуда не сохраняется — функция фактически работает вхолостую, а полученные данные просто уничтожаются сборщиком мусора.
Теперь, когда все слабые места исходного кода очевидны, рассмотрим два правильных способа решения этой задачи.
Если объемы данных велики, или нам в любом случае требуется выполнить сопоставление на стороне СУБД, мы должны полностью отказаться от выполнения запросов в цикле. Вместо этого мы передадим обе таблицы значений в один-единственный пакетный запрос и свяжем их с помощью левого или внутреннего соединения.
Разберем этот подход на примере кода:
Процедура СформироватьОптимальноЗапросом(ТаблицаНДФЛ, ТаблицаНачислений)
Если ТаблицаНДФЛ.Количество() = 0 Или ТаблицаНачислений.Количество() = 0 Тогда
Возврат;
КонецЕсли;
// Обеспечим строгую типизацию колонок таблиц перед передачей в запрос,
// если они не были типизированы ранее при создании.
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ТаблицаНДФЛ", ТаблицаНДФЛ);
Запрос.УстановитьПараметр("ТаблицаНачислений", ТаблицаНачислений);
Запрос.Текст =
"ВЫБРАТЬ
| Т_НДФЛ.ФизическоеЛицо КАК ФизическоеЛицо,
| Т_НДФЛ.СтатьяФинансирования КАК СтатьяФинансирования
|ПОМЕСТИТЬ ВТ_НДФЛ
|ИЗ
| &ТаблицаНДФЛ КАК Т_НДФЛ
|;
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Т_Начисления.ФизическоеЛицо КАК ФизическоеЛицо,
| Т_Начисления.СтатьяФинансирования КАК СтатьяФинансирования,
| Т_Начисления.Сумма КАК Сумма // Указываем только нужные колонки явно!
|ПОМЕСТИТЬ ВТ_Начисления
|ИЗ
| &ТаблицаНачислений КАК Т_Начисления
|;
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_Начисления.ФизическоеЛицо КАК ФизическоеЛицо,
| ВТ_Начисления.СтатьяФинансирования КАК СтатьяФинансирования,
| ВТ_Начисления.Сумма КАК Сумма
|ИЗ
| ВТ_Начисления КАК ВТ_Начисления
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_НДФЛ КАК ВТ_НДФЛ
| ПО ВТ_Начисления.ФизическоеЛицо = ВТ_НДФЛ.ФизическоеЛицо
| И ВТ_Начисления.СтатьяФинансирования = ВТ_НДФЛ.СтатьяФинансирования";
РезультатЗапроса = Запрос.Выполнить().Выгрузить();
// Дальнейшая обработка единой таблицы результатов
// ...
КонецПроцедуры
Преимущества данного подхода: мы совершаем ровно один сеанс связи с СУБД. Вся тяжелая работа по сопоставлению строк выполняется оптимизатором СУБД в оперативной памяти сервера базы данных, что дает колоссальный прирост скорости на больших объемах данных.
Если обе таблицы уже находятся в оперативной памяти на сервере приложений 1С, обращаться к базе данных через запрос вообще не имеет смысла. Мы можем быстро отфильтровать данные средствами самой платформы. Но чтобы избежать медленного линейного перебора строк, нам обязательно нужно использовать встроенные индексы таблицы значений.
Рассмотрим, как правильно реализовать этот алгоритм:
Процедура СформироватьОптимальноВПамяти(ТаблицаНДФЛ, ТаблицаНачислений)
Если ТаблицаНДФЛ.Количество() = 0 Или ТаблицаНачислений.Количество() = 0 Тогда
Возврат;
КонецЕсли;
// Шаг 1. Строим индекс по ключевым полям поиска.
// Это превращает поиск из линейного O(N) в мгновенный поиск по хэш-карте O(1).
ТаблицаНачислений.Индексы.Добавить("ФизическоеЛицо, СтатьяФинансирования");
// Шаг 2. Подготовим структуру для быстрого отбора
СтруктураПоиска = Новый Структура("ФизическоеЛицо, СтатьяФинансирования");
// Шаг 3. В цикле осуществляем быстрый поиск в памяти без обращений к СУБД
Для каждого СтрокаНДФЛ Из ТаблицаНДФЛ Цикл
ЗаполнитьЗначенияСвойств(СтруктураПоиска, СтрокаНДФЛ);
// Метод НайтиСтроки() на проиндексированной таблице работает молниеносно
НайденныеСтрокиНачислений = ТаблицаНачислений.НайтиСтроки(СтруктураПоиска);
Для каждого СтрокаНачисления Из НайденныеСтрокиНачислений Цикл
// Обрабатываем найденные строки
// ...
КонецЦикла;
КонецЦикла;
КонецПроцедуры
Почему это работает быстро? Добавление индекса в объект ТаблицаЗначений заставляет платформу построить в памяти внутренний индекс (хэш-таблицу). После этого метод НайтиСтроки() перестает последовательно перебирать всю таблицу сверху вниз и сразу обращается к нужным областям памяти. На таблицах в десятки тысяч строк данный подход работает практически мгновенно и не создает никакой нагрузки на SQL-сервер.
Для эффективной работы систем на базе «1С:Предприятие 8» всегда придерживайтесь следующих золотых правил:
НайтиСтроки(), но обязательно предварительно проиндексируйте исходную таблицу по полям поиска.ВНУТРЕННЕЕ СОЕДИНЕНИЕ на уровне СУБД.ВЫБРАТЬ * в запросах. Всегда перечисляйте необходимые поля явно, заботясь о расходе оперативной памяти и производительности сети.