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