Как в 1С найти документы с полностью одинаковыми табличными частями?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

Разберем интересную и нетривиальную задачу разработки в 1С. Допустим, у нас есть созданный документ с заполненной табличной частью. Нам необходимо найти в базе данных все остальные документы этого же типа, у которых состав строк табличной части, их значения и порядок следования абсолютно идентичны нашему эталону.

Часто разработчики пытаются решить эту задачу "в лоб", соединяя таблицу саму с собой по условию "не равно" (создавая декартово произведение). Проанализируем ситуацию: если в таблице сотни тысяч строк, такой запрос повесит сервер СУБД, так как фильтрация отработает только после перемножения огромного массива данных. Рассмотрим более оптимальные и архитектурно правильные способы решения этой проблемы.

Способ 1: Решение чистым запросом (Паттерн реляционного деления)

Посмотрим на то, как можно элегантно решить задачу быстрым запросом без перегрузки сервера. Мы будем использовать сравнение множеств. Логика заключается в том, чтобы посчитать количество совпавших строк и сравнить его с общим количеством строк в эталонном документе.

Разберем по шагам алгоритм построения такого запроса:

  1. Выгружаем искомую табличную часть (эталон) и передаем ее в запрос в качестве параметра. Помещаем эти данные во временную таблицу ВТ_Эталон.
  2. Обязательно подсчитываем общее количество строк в нашей ВТ_Эталон.
  3. Выполняем ВНУТРЕННЕЕ СОЕДИНЕНИЕ нашей временной таблицы с реальной табличной частью документов из базы данных. Условием связи будет равенство поля НомерСтроки (так как нам критически важен порядок) и равенство всех значимых реквизитов (например, Номенклатура, Количество, Цена).
  4. Группируем результат по ссылке на найденные документы и рассчитываем количество совпавших строк.
  5. Используем секцию ИМЕЮЩИЕ: накладываем фильтр, при котором количество совпавших строк должно быть строго равно количеству строк в ВТ_Эталон.

Важный технический нюанс: чтобы в результат не попали документы, у которых начало табличной части полностью совпадает с эталоном, но в конце присутствуют "лишние" строки, нам необходимо дополнительно присоединить ТЧ найденных документов для подсчета их общего количества строк. Это количество также должно совпадать с размером эталона.

Посмотрим на пример реализации подобного запроса:


ВЫБРАТЬ
    Эталон.НомерСтроки КАК НомерСтроки,
    Эталон.Номенклатура КАК Номенклатура,
    Эталон.Количество КАК Количество
ПОМЕСТИТЬ ВТ_Эталон
ИЗ
    &ТаблицаЭталон КАК Эталон
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    МАКСИМУМ(ВТ_Эталон.НомерСтроки) КАК ВсегоСтрокВЭталоне
ПОМЕСТИТЬ ВТ_КоличествоЭталон
ИЗ
    ВТ_Эталон КАК ВТ_Эталон
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ТЧБазы.Ссылка КАК СсылкаНаДокумент,
    КОЛИЧЕСТВО(ТЧБазы.НомерСтроки) КАК СовпавшихСтрок
ПОМЕСТИТЬ ВТ_Кандидаты
ИЗ
    ВТ_Эталон КАК ВТ_Эталон
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг.Товары КАК ТЧБазы
        ПО ВТ_Эталон.НомерСтроки = ТЧБазы.НомерСтроки
            И ВТ_Эталон.Номенклатура = ТЧБазы.Номенклатура
            И ВТ_Эталон.Количество = ТЧБазы.Количество
ГДЕ
    ТЧБазы.Ссылка <> &ТекущийДокумент // Исключаем сам эталон, если он уже в базе
СГРУППИРОВАТЬ ПО
    ТЧБазы.Ссылка
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ВТ_Кандидаты.СсылкаНаДокумент КАК НайденныйДокумент
ИЗ
    ВТ_Кандидаты КАК ВТ_Кандидаты
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_КоличествоЭталон КАК ВТ_КоличествоЭталон
        ПО (ВТ_Кандидаты.СовпавшихСтрок = ВТ_КоличествоЭталон.ВсегоСтрокВЭталоне)
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг.Товары КАК ТЧПроверкаКоличества
        ПО ВТ_Кандидаты.СсылкаНаДокумент = ТЧПроверкаКоличества.Ссылка
СГРУППИРОВАТЬ ПО
    ВТ_Кандидаты.СсылкаНаДокумент,
    ВТ_Кандидаты.СовпавшихСтрок
ИМЕЮЩИЕ
    КОЛИЧЕСТВО(ТЧПроверкаКоличества.НомерСтроки) = ВТ_Кандидаты.СовпавшихСтрок

Способ 2: Хеширование содержимого табличной части

Если задача проверки дублей является регулярной и высоконагруженной, то вычислять совпадения динамически каждый раз — затратно. В этом случае применим архитектурный подход с хешированием: при записи документа мы вычисляем уникальный "отпечаток" (хеш) его табличной части и сохраняем его. Поиск дублей тогда сводится к простейшему поиску по строке хеша.

Выясним причину частой ошибки разработчиков. Многие пытаются сериализовать ТЧ в строку с помощью функции ЗначениеВСтрокуВнутр() и затем получать от нее хеш. Это критическая ошибка! Дело в том, что внутреннее представление таблицы содержит скрытые системные поля: Ссылка (владелец) и КлючСтроки (уникальный идентификатор строки). Из-за этого две визуально абсолютно идентичные табличные части из разных документов выдадут разный внутренний текст и разные хеши.

Рассмотрим правильный алгоритм действий при хешировании:

  1. Выгружаем данные табличной части в структуру или Таблицу Значений.
  2. Удаляем колонку со ссылкой на объект.
  3. Обязательно оставляем колонку НомерСтроки (чтобы хеш учитывал порядок сортировки).
  4. Оставляем только значимые для сравнения колонки (исключаем служебные).
  5. Сериализуем очищенную структуру в формат JSON с помощью объекта ЗаписьJSON. Формат JSON не потянет за собой внутренние GUID-ы платформы.
  6. Вычисляем хеш от полученной строки JSON (например, по алгоритму MD5).

Посмотрим на пример кода формирования правильного хеша:


Функция ПолучитьХешТабличнойЧасти(ДокументОбъект)
    
    // Выгружаем только нужные колонки
    ТаблицаДанных = ДокументОбъект.Товары.Выгрузить( , "НомерСтроки, Номенклатура, Количество, Цена, Сумма");
    
    // Формируем массив структур для JSON
    МассивДанных = Новый Массив;
    Для Каждого СтрокаТЧ Из ТаблицаДанных Цикл
        СтруктураСтроки = Новый Структура("НомерСтроки, Номенклатура, Количество, Цена, Сумма");
        ЗаполнитьЗначенияСвойств(СтруктураСтроки, СтрокаТЧ);
        // Ссылочные типы преобразуем в уникальные идентификаторы или строки
        СтруктураСтроки.Номенклатура = Строка(СтруктураСтроки.Номенклатура.УникальныйИдентификатор());
        МассивДанных.Добавить(СтруктураСтроки);
    КонецЦикла;
    
    // Сериализуем в JSON
    ЗаписьJSON = Новый ЗаписьJSON;
    ЗаписьJSON.УстановитьСтроку();
    ЗаписатьJSON(ЗаписьJSON, МассивДанных);
    СтрокаJSON = ЗаписьJSON.Закрыть();
    
    // Вычисляем хеш
    Хеширование = Новый ХешированиеДанных(ХешФункция.MD5);
    Хеширование.Добавить(СтрокаJSON);
    
    Возврат ПолучитьHexСтрокуИзДвоичныхДанных(Хеширование.ХешСумма);
    
КонецФункции

Где хранить вычисленный хеш?

Проанализируем варианты хранения полученного значения (строки хеша):

Антипаттерны: как делать не нужно

В процессе поиска решений разработчики иногда предлагают использовать Систему Компоновки Данных (СКД) с вызовом экспортных функций для программного хеширования "на лету" прямо при формировании отчета. Разберем, почему это плохая идея.

Вызов серверной функции 1С (расчет хеша) из СКД для каждой анализируемой строки базы данных приведет к полному сканированию таблиц (Table Scan) СУБД с постоянным переключением контекста между SQL-сервером и сервером 1С. Это колоссально замедлит работу базы, сделав процесс неработоспособным по времени. Применять экспортные функции в СКД допустимо только к уже отфильтрованному и максимально узкому набору данных, но никак не для сплошного поиска по всей таблице документов.

← На главную