Ищем отличия между двумя таблицами значений в 1С 8.3: от быстрой проверки до отчёта о расхождениях

· 11 мин

Содержание

Сравнить две таблицы значений в 1С оператором ТЗ1 = ТЗ2 нельзя: он сравнивает ссылки на объекты, а не содержимое, и для двух разных таблиц вернёт Ложь даже при одинаковых данных. Для проверки «совпадают целиком или нет» хватит ЗначениеВСтрокуВнутр(ТЗ1) = ЗначениеВСтрокуВнутр(ТЗ2). Чтобы получить список отличий — какие строки добавлены, удалены или изменены, — нужен перебор по ключу через Соответствие, приём со Свернуть() или запрос с ПОЛНЫМ СОЕДИНЕНИЕМ.

Выбор метода зависит от трёх вещей: важен ли порядок строк, есть ли у строк уникальный ключ и сколько строк в таблицах. Все примеры ниже — для платформы 8.3 и выполняются на сервере: на тонком клиенте объект ТаблицаЗначений недоступен.

С чего начать сравнение таблиц значений: структура и ключ

Прежде чем писать код, ответьте себе на три вопроса.

  • 🔑 По какому полю сопоставлять строки? Номенклатура, код, номер строки или пара «Номенклатура + Характеристика». Без ключа можно сказать только «таблицы разные», но не «что именно поменялось».
  • 📐 Одинаковы ли колонки? Имена, состав и типы. Если в одной таблице колонка Сумма, а в другой СуммаСНДС, сначала приведите их к одной схеме.
  • 🔁 Есть ли дубли ключа? Две строки с одной номенклатурой ломают сопоставление «один к одному». Их надо свернуть или сравнивать с учётом количества повторов.

Проверку состава колонок удобно вынести в отдельную функцию и вызывать её первой:

Функция СоставКолонокСовпадает(ТЗ1, ТЗ2)

	Если ТЗ1.Колонки.Количество() <> ТЗ2.Колонки.Количество() Тогда
		Возврат Ложь;
	КонецЕсли;

	Для Каждого Колонка Из ТЗ1.Колонки Цикл
		Если ТЗ2.Колонки.Найти(Колонка.Имя) = Неопределено Тогда
			Возврат Ложь;
		КонецЕсли;
	КонецЦикла;

	Возврат Истина;

КонецФункции

Дубли ключа проверяются одной строкой: скопируйте колонку ключа, добавьте счётчик и сверните.

Копия = ТЗ.Скопировать(, "Номенклатура");
Копия.Колонки.Добавить("Повторов", Новый ОписаниеТипов("Число"));
Копия.ЗаполнитьЗначения(1, "Повторов");
Копия.Свернуть("Номенклатура", "Повторов");
ЕстьДубли = Копия.Количество() < ТЗ.Количество();

Если таблицы пришли из табличных частей документов, получите их через Объект.Товары.Выгрузить(). Колонки при этом будут типизированы, что пригодится для сравнения запросом.

Проверка на полное совпадение без перебора строк

Когда нужен только ответ «да/нет» — например, чтобы не перезаписывать данные, если ничего не изменилось, — сериализуйте обе таблицы в строку и сравните строки.

&НаСервере
Функция ТаблицыИдентичны(ТЗ1, ТЗ2)

	Если ТЗ1.Количество() <> ТЗ2.Количество() Тогда
		Возврат Ложь;
	КонецЕсли;

	Возврат ЗначениеВСтрокуВнутр(ТЗ1) = ЗначениеВСтрокуВнутр(ТЗ2);

КонецФункции

Проверка количества строк в начале — дешёвый фильтр: если строк разное число, сериализация не нужна.

У этого способа строгие условия. Таблицы считаются равными, только если совпадает всё:

  • 📋 порядок строк;
  • 📋 порядок и имена колонок;
  • 📋 описание типов колонок (колонка Число(15,2) и колонка без типа — уже разница);
  • 📋 сами значения, включая пробелы в конце строк.

Поэтому перед такой проверкой отсортируйте обе таблицы одинаково: ТЗ1.Сортировать("Номенклатура, Количество") и то же для ТЗ2. Если колонки создавались в разном порядке, сначала приведите их к общему виду — например, скопируйте обе таблицы с явным списком колонок: ТЗ1.Скопировать(, "Номенклатура, Количество, Сумма").

В типовых конфигурациях на БСП есть готовая функция ОбщегоНазначения.ДанныеСовпадают(Данные1, Данные2). Она сравнивает коллекции с учётом вложенности и избавляет от ручной сериализации. В самописной конфигурации без БСП её нет — используйте вариант с ЗначениеВСтрокуВнутр.

Перебор строк и поиск отличий по ключу

Это главный рабочий способ, когда нужно знать, что именно изменилось. Идея простая: строки второй таблицы раскладываются в Соответствие по ключу, затем первая таблица проходится один раз, и для каждой строки сразу находится пара.

Почему не вложенный цикл: при 10 000 строк в каждой таблице это 100 млн сравнений. Соответствие ищет по ключу почти мгновенно, и общее время растёт линейно.

&НаСервере
Функция НайтиОтличия(ТЗСтарая, ТЗНовая, ИмяКлюча, СравниваемыеПоля)

	Поля = СтрРазделить(СравниваемыеПоля, ",", Ложь);

	Отличия = Новый ТаблицаЗначений;
	Отличия.Колонки.Добавить("Ключ");
	Отличия.Колонки.Добавить("ВидОтличия", Новый ОписаниеТипов("Строка"));
	Отличия.Колонки.Добавить("Поле", Новый ОписаниеТипов("Строка"));
	Отличия.Колонки.Добавить("Было");
	Отличия.Колонки.Добавить("Стало");

	НовыеПоКлючу = Новый Соответствие;
	Для Каждого СтрокаНовая Из ТЗНовая Цикл
		НовыеПоКлючу.Вставить(СтрокаНовая[ИмяКлюча], СтрокаНовая);
	КонецЦикла;

	Сопоставленные = Новый Соответствие;

	Для Каждого СтрокаСтарая Из ТЗСтарая Цикл

		Ключ = СтрокаСтарая[ИмяКлюча];
		СтрокаНовая = НовыеПоКлючу.Получить(Ключ);

		Если СтрокаНовая = Неопределено Тогда
			Отличие = Отличия.Добавить();
			Отличие.Ключ = Ключ;
			Отличие.ВидОтличия = "Удалена";
			Продолжить;
		КонецЕсли;

		Сопоставленные.Вставить(Ключ, Истина);

		Для Каждого Поле Из Поля Цикл
			ИмяПоля = СокрЛП(Поле);
			Если СтрокаСтарая[ИмяПоля] <> СтрокаНовая[ИмяПоля] Тогда
				Отличие = Отличия.Добавить();
				Отличие.Ключ = Ключ;
				Отличие.ВидОтличия = "Изменена";
				Отличие.Поле = ИмяПоля;
				Отличие.Было = СтрокаСтарая[ИмяПоля];
				Отличие.Стало = СтрокаНовая[ИмяПоля];
			КонецЕсли;
		КонецЦикла;

	КонецЦикла;

	Для Каждого СтрокаНовая Из ТЗНовая Цикл
		Если Сопоставленные.Получить(СтрокаНовая[ИмяКлюча]) = Неопределено Тогда
			Отличие = Отличия.Добавить();
			Отличие.Ключ = СтрокаНовая[ИмяКлюча];
			Отличие.ВидОтличия = "Добавлена";
		КонецЕсли;
	КонецЦикла;

	Возврат Отличия;

КонецФункции

Вызов: Отличия = НайтиОтличия(ТЗДо, ТЗПосле, "Номенклатура", "Количество, Цена, Сумма");

Результат — одна строка на каждое изменённое поле. Если у номенклатуры поменялись и количество, и сумма, в таблице отличий будет две строки с видом «Изменена».

Соответствие.Вставить() молча перезаписывает значение при повторе ключа. При дублях в новой таблице сравнится только последняя строка с этим ключом. Проверьте дубли заранее (см. первый раздел).

Составной ключ

Если строка определяется двумя полями, есть два пути:

  1. Склеить ключ в строку и использовать его в Соответствие. Для ссылок берите уникальный идентификатор: Строка(Ссылка.УникальныйИдентификатор()), а не представление — у двух товаров может быть одинаковое наименование.
  2. Искать через НайтиСтроки() со структурой отбора, предварительно создав индекс:
ТЗНовая.Индексы.Добавить("Номенклатура, Характеристика");
Отбор = Новый Структура("Номенклатура, Характеристика");

Для Каждого СтрокаСтарая Из ТЗСтарая Цикл
	ЗаполнитьЗначенияСвойств(Отбор, СтрокаСтарая);
	Найденные = ТЗНовая.НайтиСтроки(Отбор);
	Если Найденные.Количество() = 0 Тогда
		// строка удалена
	Иначе
		СтрокаНовая = Найденные[0];
		// сравнение полей
	КонецЕсли;
КонецЦикла;

Без строки с Индексы.Добавить поиск НайтиСтроки каждый раз просматривает таблицу целиком, и выигрыш над вложенным циклом исчезает.

Приём со Свернуть: сравнение таблиц значений без запроса

Малоизвестный, но очень удобный способ. Строкам первой таблицы ставится знак +1, строкам второй −1, обе складываются в одну таблицу и сворачиваются по всем сравниваемым колонкам. Одинаковые строки взаимно гасятся до нуля, остаются только расхождения.

&НаСервере
Функция РасхожденияЧерезСвернуть(ТЗ1, ТЗ2, Колонки)

	Общая = ТЗ1.Скопировать(, Колонки);
	Общая.Колонки.Добавить("Знак", Новый ОписаниеТипов("Число"));
	Общая.ЗаполнитьЗначения(1, "Знак");

	Для Каждого Строка2 Из ТЗ2 Цикл
		НоваяСтрока = Общая.Добавить();
		ЗаполнитьЗначенияСвойств(НоваяСтрока, Строка2);
		НоваяСтрока.Знак = -1;
	КонецЦикла;

	Общая.Свернуть(Колонки, "Знак");

	Для Каждого Строка Из Общая.НайтиСтроки(Новый Структура("Знак", 0)) Цикл
		Общая.Удалить(Строка);
	КонецЦикла;

	Возврат Общая;

КонецФункции

Как читать результат:

ЗнакЧто означает
+1 (или больше)строка есть только в первой таблице или встречается там чаще
−1 (или меньше)строка есть только во второй таблице или встречается там чаще
пара +1 и −1 с одним ключомстрока изменилась: в +1 старые значения, в −1 новые

Плюсы: работает без ключа, правильно учитывает дубли (две одинаковые строки против одной дадут остаток +1), а Свернуть() выполняется быстро даже на сотнях тысяч строк.

Минус: изменение одного поля выглядит как пара «удалена + добавлена», и для отчёта по полям придётся сопоставить эти пары отдельно.

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

Сравнение двух таблиц значений запросом

Запрос удобен, когда сравнение нужно дополнить данными из базы (например, подтянуть артикул или остаток) или когда логику проще выразить на языке запросов. Таблица значений передаётся параметром, помещается во временную таблицу, а потом две временные таблицы соединяются ПОЛНЫМ СОЕДИНЕНИЕМ.

Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
|	Т.Номенклатура КАК Номенклатура,
|	Т.Количество КАК Количество
|ПОМЕСТИТЬ ВТ_Старая
|ИЗ
|	&ТаблицаСтарая КАК Т
|
|ИНДЕКСИРОВАТЬ ПО
|	Номенклатура
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
|	Т.Номенклатура КАК Номенклатура,
|	Т.Количество КАК Количество
|ПОМЕСТИТЬ ВТ_Новая
|ИЗ
|	&ТаблицаНовая КАК Т
|
|ИНДЕКСИРОВАТЬ ПО
|	Номенклатура
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
|	ЕСТЬNULL(Старая.Номенклатура, Новая.Номенклатура) КАК Номенклатура,
|	ЕСТЬNULL(Старая.Количество, 0) КАК КоличествоБыло,
|	ЕСТЬNULL(Новая.Количество, 0) КАК КоличествоСтало,
|	ВЫБОР
|		КОГДА Старая.Номенклатура ЕСТЬ NULL
|			ТОГДА ""Добавлена""
|		КОГДА Новая.Номенклатура ЕСТЬ NULL
|			ТОГДА ""Удалена""
|		ИНАЧЕ ""Изменена""
|	КОНЕЦ КАК ВидОтличия
|ИЗ
|	ВТ_Старая КАК Старая
|		ПОЛНОЕ СОЕДИНЕНИЕ ВТ_Новая КАК Новая
|		ПО Старая.Номенклатура = Новая.Номенклатура
|ГДЕ
|	(Старая.Номенклатура ЕСТЬ NULL
|			ИЛИ Новая.Номенклатура ЕСТЬ NULL
|			ИЛИ Старая.Количество <> Новая.Количество)";

Запрос.УстановитьПараметр("ТаблицаСтарая", ТЗСтарая);
Запрос.УстановитьПараметр("ТаблицаНовая", ТЗНовая);

Отличия = Запрос.Выполнить().Выгрузить();

Временные таблицы внутри одного пакетного запроса видны друг другу без дополнительных объектов. МенеджерВременныхТаблиц понадобится, только если ВТ_Старая и ВТ_Новая нужно использовать в нескольких разных запросах.

Три условия, без которых запрос не заработает или даст неверный результат:

  1. Колонки должны иметь тип. Таблицу значений нельзя передать в запрос, если у колонки не задано ОписаниеТипов. Создавайте колонки так: ТЗ.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 3))).
  2. Параметр-таблицу сначала помещают во временную таблицу. Соединять &ТаблицаСтарая напрямую с другой таблицей в одном запросе нельзя.
  3. Строки неограниченной длины не сравниваются. Если ключ — строка без квалификатора длины, задайте длину при создании колонки (Новый ОписаниеТипов("Строка",, Новый КвалификаторыСтроки(150))) или приведите в запросе: ВЫРАЗИТЬ(Т.Артикул КАК СТРОКА(150)).

Для сравнения без ключа подходит та же идея, что и со Свернуть: ОБЪЕДИНИТЬ ВСЕ двух таблиц с полем 1 КАК Знак и -1 КАК Знак, затем СГРУППИРОВАТЬ ПО всем полям и ИМЕЮЩИЕ СУММА(Знак) <> 0.

Типы данных и форматы: почему одинаковые значения не совпадают

Чаще всего «ложные» расхождения дают не алгоритм, а данные. Значения выглядят одинаково на экране, но для платформы различаются.

СитуацияПримерКак привести
Пробелы в строке"Болт М8 " и "Болт М8"СокрЛП(Значение)
Регистр"ООО Ромашка" и "ооо ромашка"ВРег(СокрЛП(Значение))
Разная точность числа10.333 и 10.33Окр(Значение, 2)
Время в дате01.03 10:15:00 и 01.03 00:00:00НачалоДня(Значение)
Строка вместо числа"100" и 100Число(Значение) в попытке
Пусто разными способамиNull, Неопределено, пустая ссылкапроверка ЗначениеЗаполнено()

Сравнение = и <> значений разных типов ошибки не вызывает: строка "100" просто не равна числу 100. Поэтому такие расхождения легко пропустить — код отработает и покажет отличие там, где его по смыслу нет.

Удобно прогонять каждое значение через функцию нормализации перед сравнением:

Функция НормализованноеЗначение(Значение)

	Если Не ЗначениеЗаполнено(Значение) Тогда
		Возврат Неопределено;
	КонецЕсли;

	Тип = ТипЗнч(Значение);
	Если Тип = Тип("Строка") Тогда
		Возврат ВРег(СокрЛП(Значение));
	ИначеЕсли Тип = Тип("Число") Тогда
		Возврат Окр(Значение, 2);
	ИначеЕсли Тип = Тип("Дата") Тогда
		Возврат НачалоДня(Значение);
	КонецЕсли;

	Возврат Значение;

КонецФункции

В функции НайтиОтличия замените сравнение на НормализованноеЗначение(СтрокаСтарая[ИмяПоля]) <> НормализованноеЗначение(СтрокаНовая[ИмяПоля]).

Первая проверка приравнивает к «пусто» ноль, пустую строку, пустую дату и Ложь. Если для вас 0 и «не заполнено» — разные состояния (например, остаток 0 против отсутствия данных), уберите эту ветку и обрабатывайте Null отдельно.

Если в колонке составной тип, сначала сравните ТипЗнч() обоих значений, а уже потом сами значения. Так в отчёте будет видно «тип изменился», а не непонятная пара «Неопределено → 0».

Как ускорить сравнение больших таблиц значений

На сотне строк годится любой метод. От 10 000 строк и выше выбор начинает влиять на время ощутимо.

МетодСкорость при росте объёмаКогда братьОграничения
Вложенный циклпадает квадратичнодо нескольких сотен строкна 10 000 × 10 000 — 100 млн сравнений
НайтиСтроки без индексато же, что вложенный циклне беритекаждый поиск — полный просмотр
НайтиСтроки + Индексы.Добавитьлинейносоставной ключиндекс занимает память
Соответствие по ключулинейнопростой ключ, отчёт по полямдубли ключа перезаписываются
Свернуть со знакомбыстронет ключа, есть дублиизменение = пара строк
Запросзависит от СУБДнужны данные из базыколонки с типами, передача на СУБД
ЗначениеВСтрокуВнутрлинейнотолько «равны/нет»вся таблица целиком в строке в памяти

Практические приёмы:

  • ⚡ Выполняйте сравнение на сервере одним серверным вызовом. Каждый вызов сервера с клиента в цикле стоит дороже, чем само сравнение.
  • ⚡ Сокращайте таблицы до нужных колонок через Скопировать(, "Ключ, Поле1, Поле2"). Меньше колонок — меньше памяти и быстрее Свернуть.
  • ⚡ Обращайтесь к колонкам через переменную, а не вычисляйте имя в каждой итерации: в функции НайтиОтличия массив Поля разбирается один раз до цикла.
  • ⚡ Не пересчитывайте Количество() в условии цикла без нужды и не удаляйте строки внутри прямого Для Каждого — это ведёт к пропуску строк.
  • ⚡ Для таблиц в сотни тысяч строк проверяйте сначала количество строк и контрольные суммы числовых колонок (ТЗ.Итог("Сумма")). Если итоги не совпали, детальное сравнение точно нужно; если совпали — это ещё не гарантия, но повод начать с быстрой проверки.

Отчёт о расхождениях: куда вывести результат сравнения

Таблица отличий сама по себе нужна только коду. Человеку её нужно показать.

Самый быстрый вариант для отладки — сообщения пользователю:

Для Каждого Отличие Из Отличия Цикл
	Сообщить(СтрШаблон("%1: %2, поле %3: %4 → %5",
		Отличие.ВидОтличия, Отличие.Ключ, Отличие.Поле, Отличие.Было, Отличие.Стало));
КонецЦикла;

Таблица на форме. Добавьте реквизит формы типа ТаблицаЗначений с нужными колонками, выведите его на форму и заполните на сервере: ЗначениеВРеквизитФормы(Отличия, "ТаблицаОтличий"). Пользователь сможет сортировать, искать и выгружать список в Excel стандартными средствами формы.

Печатный отчёт через СКД. Подходит, когда нужны группировки по виду отличия и оформление. В схеме компоновки создайте набор данных «Объект» с именем Отличия и полями, как у таблицы отличий, затем передайте таблицу как внешний набор:

&НаСервере
Процедура ВывестиОтчетОбОтличиях(Отличия, ТабДок)

	Схема = ПолучитьМакет("СхемаОтличий");
	КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
	Макет = КомпоновщикМакета.Выполнить(Схема, Схема.НастройкиПоУмолчанию);

	ВнешниеНаборы = Новый Структура("Отличия", Отличия);
	Процессор = Новый ПроцессорКомпоновкиДанных;
	Процессор.Инициализировать(Макет, ВнешниеНаборы);

	ТабДок.Очистить();
	ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВТабличныйДокумент;
	ПроцессорВывода.УстановитьДокумент(ТабДок);
	ПроцессорВывода.Вывести(Процессор);

КонецПроцедуры

ПолучитьМакет() в таком виде работает в модуле объекта обработки или отчёта, где лежит макет схемы.

Полезно добавить в отчёт итоговую строку: сколько строк добавлено, удалено и изменено. Это одна свёртка копии таблицы отличий по колонке ВидОтличия со счётчиком — по тому же принципу, что поиск дублей выше.

Частые вопросы

Можно ли сравнить таблицы значений на клиенте?
На тонком клиенте и в веб-клиенте объект ТаблицаЗначений недоступен. На форме таблица хранится как ДанныеФормыКоллекция; для сравнения преобразуйте её на сервере через РеквизитФормыВЗначение().
Как сравнить таблицы, если колонки называются по-разному?
Создайте копию одной таблицы с нужными именами: добавьте колонки с именами второй таблицы и заполните их через ВыгрузитьКолонку() и ЗагрузитьКолонку(). После этого применяйте любой метод.
Как сравнить табличную часть документа до и после записи?
В ПередЗаписью получите сохранённую версию через Ссылка.Товары.Выгрузить(), а текущую — через Товары.Выгрузить(), и передайте обе в функцию сравнения. Для нового документа сохранённой версии нет: проверяйте ЭтоНовый().
Учитывает ли ЗначениеВСтрокуВнутр порядок строк?
Да. Таблицы с одинаковыми строками в разном порядке дадут разные строки. Перед проверкой отсортируйте обе таблицы по одному и тому же набору колонок.
Почему запрос пишет, что тип не может быть использован?
Чаще всего у колонки переданной таблицы не задан тип или в ней лежит значение недопустимого для запроса типа, например вложенная таблица значений. Задайте ОписаниеТипов каждой колонке и уберите такие колонки через Скопировать(, "…").