В процессе разработки часто возникает задача сопоставления двух наборов данных: например, когда необходимо отобрать только тех сотрудников, которые одновременно присутствуют в документе начисления и в документе фиксации выработки. Начинающие разработчики нередко пытаются выгружать результаты запросов во встроенные массивы и выполнять двойные циклы для поиска пересечений. Проанализируем различные подходы к решению этой задачи, выделим наиболее производительные способы на стороне СУБД и рассмотрим алгоритмы обработки данных в памяти.
Если оба набора данных хранятся в базе данных (в табличных частях документов, регистрах сведений или регистрах накопления), оптимальным решением является выполнение одного запроса с конструкцией ВНУТРЕННЕЕ СОЕДИНЕНИЕ (INNER JOIN). Внутреннее соединение отбирает только те записи, для которых выполняется условие сопоставления в обеих соединяемых таблицах. В этом случае отпадает необходимость в пакетных запросах и постобработке результатов во встроенном языке.
Рассмотрим пример запроса, выбирающего общих сотрудников из двух табличных частей:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| РаботаДанные.Сотрудник КАК Сотрудник
|ИЗ
| Документ.РаботаСотрудниковНаОбъекте.Данные КАК РаботаДанные
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.НачислениеЗарплаты.Начисления КАК НачислениеЗарплаты
| ПО РаботаДанные.Сотрудник = НачислениеЗарплаты.Сотрудник
|ГДЕ
| НачислениеЗарплаты.Ссылка = &СсылкаНачисление
| И РаботаДанные.Ссылка = &СсылкаРабота";
Запрос.УстановитьПараметр("СсылкаНачисление", ДокументНачислениеСсылка);
Запрос.УстановитьПараметр("СсылкаРабота", ДокументРаботаСсылка);
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
// Обработка общих сотрудников
КонецЦикла;
Разберем важные детали построения этого запроса:
ГДЕ для обоих документов. Если опустить условие по второму документу, СУБД выполнит полное сканирование всей таблицы базы данных, что приведет к существенным просадкам производительности.РаботаДанные.Сотрудник = НачислениеЗарплаты.Сотрудник. Не следует писать Сотрудник.Ссылка, так как реквизит Сотрудник уже является ссылочным типом.Когда требуется соединить большие массивы информации или предварительно сгруппировать данные, рекомендуется помещать промежуточные результаты во временные таблицы с явным созданием индексов по полям соединения.
Посмотрим на пример использования пакетного запроса с временными таблицами:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| НачислениеЗарплаты.Сотрудник КАК Сотрудник
|ПОМЕСТИТЬ ВТ_Начисления
|ИЗ
| Документ.НачислениеЗарплаты.Начисления КАК НачислениеЗарплаты
|ГДЕ
| НачислениеЗарплаты.Ссылка = &СсылкаНачисление
|
|ИНДЕКСИРОВАТЬ ПО
| Сотрудник
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ РАЗЛИЧНЫЕ
| РаботаДанные.Сотрудник КАК Сотрудник
|ПОМЕСТИТЬ ВТ_Работы
|ИЗ
| Документ.РаботаСотрудниковНаОбъекте.Данные КАК РаботаДанные
|ГДЕ
| РаботаДанные.Ссылка = &СсылкаРабота
|
|ИНДЕКСИРОВАТЬ ПО
| Сотрудник
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_Начисления.Сотрудник КАК Сотрудник
|ИЗ
| ВТ_Начисления КАК ВТ_Начисления
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Работы КАК ВТ_Работы
| ПО ВТ_Начисления.Сотрудник = ВТ_Работы.Сотрудник";
Запрос.УстановитьПараметр("СсылкаНачисление", ДокументНачислениеСсылка);
Запрос.УстановитьПараметр("СсылкаРабота", ДокументРаботаСсылка);
Результат = Запрос.Выполнить();
Индексация временных таблиц позволяет оптимизатору СУБД строить эффективные планы выполнения (например, с использованием оператора Merge Join или Index Lookup), что критически важно при обработке сотен тысяч строк.
Если один список элементов уже сформирован в коде 1С (например, получен в виде массива ссылок), а второй список нужно получить из базы данных, передаем массив напрямую в параметр запроса с оператором В (&Массив).
Рассмотрим программную реализацию:
МассивСотрудников = Новый Массив;
// Заполнение массива элементов
МассивСотрудников.Добавить(СсылкаНаСотрудника1);
МассивСотрудников.Добавить(СсылкаНаСотрудника2);
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| Начисления.Сотрудник КАК Сотрудник
|ИЗ
| Документ.НачислениеЗарплаты.Начисления КАК Начисления
|ГДЕ
| Начисления.Ссылка = &Ссылка
| И Начисления.Сотрудник В (&МассивСотрудников)";
Запрос.УстановитьПараметр("Ссылка", СсылкаНаДокумент);
Запрос.УстановитьПараметр("МассивСотрудников", МассивСотрудников);
Результат = Запрос.Выполнить();
Особенности работы с параметром-массивом:
&МассивСотрудников передать пустой массив, платформа автоматически трансформирует условие В (&Массив) в заведомо ложное (1 = 0), и запрос вернет пустой результат без возникновения ошибок.В (&Массив) коллекции, содержащие тысячи элементов. Это приводит к раздуванию текста SQL-запроса (оператор IN (...)) и превышению лимитов параметров СУБД. При больших объемах данных массив следует загрузить в ТаблицаЗначений, передать её в запрос и поместить во временную таблицу.Когда оба набора данных уже находятся в оперативной памяти и выполнять повторный запрос к СУБД нецелесообразно, сравнение производится средствами языка 1С. Важно выбрать правильную структуру данных, чтобы избежать квадратичной сложности алгоритма.
Типичная ошибка — использование метода Массив.Найти() внутри цикла обхода другого массива. Такой подход имеет сложность O(N * M) и при больших массивах приводит к зависанию сеанса. Для быстрого поиска со сложностью O(N + M) используем объект Соответствие:
Функция ПолучитьПересечениеМассивов(Массив1, Массив2) Экспорт
МассивРезультат = Новый Массив;
ХешТаблица = Новый Соответствие;
// Заполняем Соответствие ключами из первого массива
Для Каждого Элемент Из Массив1 Цикл
ХешТаблица.Вставить(Элемент, Истина);
КонецЦикла;
// Ищем совпадения за время O(1) для каждого элемента
Для Каждого Элемент Из Массив2 Цикл
Если ХешТаблица[Элемент] <> Неопределено Тогда
МассивРезультат.Добавить(Элемент);
// Удаляем найденный ключ, чтобы избежать дублей в результате
ХешТаблица.Удалить(Элемент);
КонецЕсли;
КонецЦикла;
Возврат МассивРезультат;
КонецФункции
Выясним основные нюансы, влияющие на скорость выполнения запросов и устойчивость системы при высоких нагрузках:
Поле.Ссылка): Если реквизит уже имеет ссылочный тип (например, Сотрудник ссылается на СправочникСсылка.Сотрудники), обращение Сотрудник.Ссылка заставляет платформу создавать неявное левое соединение с таблицей справочника. Это приводит к лишней нагрузке на СУБД и является грубой ошибкой разработки.ВЫБРАТЬ РАЗЛИЧНЫЕ: Оператор РАЗЛИЧНЫЕ заставляет сервер базы данных выполнять дополнительную сортировку или операцию хеширования для устранения дубликатов. Применяйте его только тогда, когда дубли действительно возможны и их нельзя устранить правильным построением соединений или группировкой СГРУППИРОВАТЬ ПО.