При разработке внешних обработок разработчики нередко сталкиваются с необходимостью найти повторяющиеся данные в табличной части с выводом номеров строк, в которых обнаружены дубли. При попытке написать прямой запрос к табличной части возникает исключение: {(6, 5)}: Таблица не найдена "Объект.ТабличнаяЧасть1". Проанализируем, почему возникает такое поведение, разберем типичные логические ошибки и рассмотрим эффективные варианты решения задачи.
Выясним причину этой распространенной ошибки. Механизм запросов 1С транслирует код на языке запросов в инструкции для СУБД (PostgreSQL, MS SQL Server и др.). СУБД хранит только реальные таблицы базы данных — справочники, документы, регистры и их табличные части. Табличная часть внешней обработки, которая открыта на клиенте или формируется в оперативной памяти, физически не существует в структуре базы данных СУБД. Именно поэтому сервер баз данных сообщает, что запрошенная таблица не найдена.
Для того чтобы обработать данные таблицы запросом, их необходимо предварительно передать в запрос в виде параметра и поместить во временную таблицу.
Посмотрим на исходную попытку построения выборки:
ВЫБРАТЬ
ТабЧасть.НомерСтроки,
КОЛИЧЕСТВО(ТабЧасть.Значение) КАК Всего,
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ТабЧасть.Значение) КАК Количество
ИЗ
...
СГРУППИРОВАТЬ ПО
ТабЧасть.НомерСтроки,
ТабЧасть.Значение
ИМЕЮЩИЕ
КОЛИЧЕСТВО(ТабЧасть.Значение) <> КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ТабЧасть.Значение)
В этой конструкции допущена принципиальная логическая ошибка. В секцию СГРУППИРОВАТЬ ПО включено поле НомерСтроки. Поскольку номер строки уникален для каждой записи, каждая группа будет состоять ровно из одной строки. Агрегатная функция КОЛИЧЕСТВО для единичной строки всегда будет возвращать единицу, и условие в секции ИМЕЮЩИЕ никогда не сработает. Чтобы найти дубликаты, группировать данные нужно только по проверяемому значению, а исходные номера строк получать через дополнительное соединение или оконные функции.
Разберем по шагам классический способ поиска дублей с помощью временных таблиц (ВТ). Алгоритм состоит из трех этапов:
ТаблицаЗначений и передаем в параметр запроса.Рассмотрим реализацию:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Т.НомерСтроки КАК НомерСтроки,
| Т.Значение КАК Значение
|ПОМЕСТИТЬ ВТ_ИсходныеДанные
|ИЗ
| &ТЧ КАК Т
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ.Значение КАК Значение
|ПОМЕСТИТЬ ВТ_Дубликаты
|ИЗ
| ВТ_ИсходныеДанные КАК ВТ
|
|СГРУППИРОВАТЬ ПО
| ВТ.Значение
|
|ИМЕЮЩИЕ
| КОЛИЧЕСТВО(*) > 1
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ.НомерСтроки КАК НомерСтроки,
| ВТ.Значение КАК Значение
|ИЗ
| ВТ_ИсходныеДанные КАК ВТ
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Дубликаты КАК ВТ_Дубликаты
| ПО ВТ.Значение = ВТ_Дубликаты.Значение
|
|УПОРЯДОЧИТЬ ПО
| НомерСтроки";
// В контексте объекта или серверной процедуры управляемой формы:
ТаблицаДляЗапроса = Объект.ТабличнаяЧасть1.Выгрузить();
Запрос.УстановитьПараметр("ТЧ", ТаблицаДляЗапроса);
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Сообщить("Строка № " + Выборка.НомерСтроки + ": дубликат значения " + Выборка.Значение);
КонецЦикла;
Важные нюансы работы с параметром запроса:
Начиная с версии платформы 8.3.20, в языке запросов 1С поддерживаются оконные функции. Мы можем упростить запрос и избавиться от лишнего самосоединения (ВНУТРЕННЕЕ СОЕДИНЕНИЕ), вычислив количество повторений прямо в первой временной таблице.
Посмотрим на пример реализации:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Т.НомерСтроки КАК НомерСтроки,
| Т.Значение КАК Значение,
| КОЛИЧЕСТВО(*) НАД (РАЗДЕЛИТЬ ПО Т.Значение) КАК КоличествоПовторов
|ПОМЕСТИТЬ ВТ_ДанныеСКоличеством
|ИЗ
| &ТЧ КАК Т
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ.НомерСтроки КАК НомерСтроки,
| ВТ.Значение КАК Значение
|ИЗ
| ВТ_ДанныеСКоличеством КАК ВТ
|ГДЕ
| ВТ.КоличествоПовторов > 1
|
|УПОРЯДОЧИТЬ ПО
| ВТ.НомерСтроки";
Запрос.УстановитьПараметр("ТЧ", Объект.ТабличнаяЧасть1.Выгрузить());
Результат = Запрос.Выполнить();
Выборка = Результат.Выбрать();
Пока Выборка.Следующий() Цикл
Сообщить("Строка " + Выборка.НомерСтроки + " содержит дубль: " + Выборка.Значение);
КонецЦикла;
Если таблица содержит несколько сотен или тысяч строк, выполнять запрос к СУБД неэффективно. Намного быстрее и легче выполнить проверку прямо в оперативной памяти сервера с помощью объекта Соответствие. Такой алгоритм отрабатывает за один проход со сложностью порядка O(N).
Разберем реализацию без использования запроса:
// Коллекция для хранения первого вхождения: Ключ = Значение, Значение = НомерСтроки
НайденныеЗначения = Новый Соответствие;
// Коллекция номеров строк, являющихся дублями
НомераДублей = Новый Массив;
Для Каждого СтрТЧ Из Объект.ТабличнаяЧасть1 Цикл
ТекущееЗначение = СтрТЧ.Значение;
ПервыйНомерСтроки = НайденныеЗначения[ТекущееЗначение];
Если ПервыйНомерСтроки = Неопределено Тогда
// Запоминаем номер строки первого появления значения
НайденныеЗначения.Вставить(ТекущееЗначение, СтрТЧ.НомерСтроки);
Иначе
// Значение уже встречалось ранее - фиксируем дубликат
Если ПервыйНомерСтроки <> Истина Тогда
НомераДублей.Добавить(ПервыйНомерСтроки);
НайденныеЗначения.Вставить(ТекущееЗначение, Истина); // Флаг, чтобы не дублировать первую строку
КонецЕсли;
НомераДублей.Добавить(СтрТЧ.НомерСтроки);
Сообщить("Найдена дублирующаяся строка: " + СтрТЧ.НомерСтроки + " (значение: " + ТекущееЗначение + ")");
КонецЕсли;
КонецЦикла;
Этот способ не задействует ресурсы СУБД, не требует выгрузки данных во временные структуры диска и выполняется мгновенно даже при динамическом заполнении формы.
Еще один вариант работы исключительно в оперативной памяти — использование методов объекта ТаблицаЗначений:
ТЗКопия = Объект.ТабличнаяЧасть1.Выгрузить();
ТЗКопия.Колонки.Добавить("КоличествоДублей", Новый ОписаниеТипов("Число"));
ТЗКопия.ЗаполнитьЗначения(1, "КоличествоДублей");
// Сворачиваем по проверяемому полю, суммируя счетчик
ТЗКопия.Свернуть("Значение", "КоличествоДублей");
// Отбираем значения, которые встретились более одного раза
ПараметрыОтбора = Новый Структура("КоличествоДублей");
Для Каждого Стр Из ТЗКопия Цикл
Если Стр.КоличествоДублей > 1 Тогда
// Находим все строки с этим значением в исходной табличной части
СтрокиИсходные = Объект.ТабличнаяЧасть1.НайтиСтроки(Новый Структура("Значение", Стр.Значение));
Для Каждого СтрИсходная Из СтрокиИсходные Цикл
Сообщить("Дубль в строке № " + СтрИсходная.НомерСтроки + " со значением: " + Стр.Значение);
КонецЦикла;
КонецЕсли;
КонецЦикла;
Выбор оптимального способа зависит от структуры данных: для быстрого клиент-серверного взаимодействия без нагрузки на СУБД предпочтителен поиск через Соответствие, а при необходимости сложного комбинированного анализа данных совместно с регистрами базы — запрос с помещением таблицы во временную таблицу через параметр.