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

· 11 мин

Содержание

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

Какой метод выбрать, зависит от вопроса. «Таблицы совпадают целиком?» — это одна задача. «Каких строк не хватает и где разошлось количество?» — совсем другая. Всё ниже работает в платформе 8.3 и не зависит от конфигурации: это встроенный язык, а не механизм БП или УТ.

Что именно сравниваем: три постановки задачи

Прежде чем писать код, определите, какой ответ вам нужен. От этого зависит и алгоритм, и объём работы.

ПостановкаПримерЧем решать
🟢 Равны ли таблицы целикомПроверить, что после рефакторинга отчёт выдаёт то же самоеЗначениеВСтрокуВнутр, Свернуть со счётчиком
🟡 Какие строки есть только в однойДокумент до и после изменения: что добавили, что удалилиСвернуть со знаком, Соответствие, запрос
🔴 Где по ключу разошлись суммыОстатки по складу: учёт против инвентаризацииСвернуть с ресурсами, запрос с ПОЛНЫМ СОЕДИНЕНИЕМ

Во второй и третьей задаче надо заранее решить, какие колонки — ключ (номенклатура, склад, характеристика), а какие — ресурсы (количество, сумма). Без этого «расхождение» не определено.

Быстрая проверка на полное совпадение

Если нужен только ответ «да/нет», сериализуйте обе таблицы и сравните строки:

Функция ТаблицыРавны(Т1, Т2) Экспорт
	Если Т1.Количество() <> Т2.Количество() Тогда
		Возврат Ложь;
	КонецЕсли;
	Возврат ЗначениеВСтрокуВнутр(Т1) = ЗначениеВСтрокуВнутр(Т2);
КонецФункции

Метод строгий. В строку попадают имена и порядок колонок, их типы и порядок строк. Две таблицы с одинаковыми данными, но разной сортировкой, окажутся «разными». Перед сравнением отсортируйте обе по одним колонкам методом Сортировать().

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

Ни один из этих способов не скажет, где расхождение. Для этого — методы ниже.

Способ «свернуть со знаком»: самый короткий код для сравнения двух таблиц

Этот приём практики используют чаще всего. Идея простая: сложить обе таблицы в одну, у строк первой ресурсы оставить как есть, у второй — умножить на −1. После Свернуть() по ключевым колонкам совпавшие строки дадут ноль. Всё ненулевое — расхождение.

Функция РасхожденияПоКлючу(Было, Стало, Ключи, Ресурсы) Экспорт
	// Ключи = "Номенклатура, Склад", Ресурсы = "Количество, Сумма"
	Итог = Было.Скопировать(, Ключи + ", " + Ресурсы);
	МассивРесурсов = СтрРазделить(Ресурсы, ",", Ложь);

	Для Каждого СтрокаСтало Из Стало Цикл
		НоваяСтрока = Итог.Добавить();
		ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаСтало);
		Для Каждого ИмяРесурса Из МассивРесурсов Цикл
			ИмяРесурса = СокрЛП(ИмяРесурса);
			НоваяСтрока[ИмяРесурса] = -НоваяСтрока[ИмяРесурса];
		КонецЦикла;
	КонецЦикла;

	Итог.Свернуть(Ключи, Ресурсы);

	// Удаляем совпавшие строки обратным циклом
	Всего = Итог.Количество();
	Для Сч = 1 По Всего Цикл
		СтрокаИтога = Итог[Всего - Сч];
		ЕстьРазница = Ложь;
		Для Каждого ИмяРесурса Из МассивРесурсов Цикл
			Если СтрокаИтога[СокрЛП(ИмяРесурса)] <> 0 Тогда
				ЕстьРазница = Истина;
				Прервать;
			КонецЕсли;
		КонецЦикла;
		Если Не ЕстьРазница Тогда
			Итог.Удалить(СтрокаИтога);
		КонецЕсли;
	КонецЦикла;

	Возврат Итог; // положительное значение — «было больше», отрицательное — «стало больше»
КонецФункции

Читать результат легко. Строка с Количество = 5 значит, что в «Было» на 5 единиц больше. С Количество = -3 — что в «Стало» прибавилось 3 единицы. Строка, которой не было в первой таблице, придёт целиком со знаком минус.

Сравнение строк целиком, когда ресурсов нет

Если в таблицах нет чисел (например, список пользователей с ролями), добавьте служебную колонку-счётчик:

  1. Скопируйте первую таблицу и добавьте колонку Счетчик с типом Новый ОписаниеТипов("Число").
  2. Всем её строкам поставьте 1, строкам из второй таблицы — -1.
  3. Сверните по всем остальным колонкам, суммируя Счетчик.
  4. Строки с ненулевым счётчиком есть только в одной таблице. Знак подскажет, в какой.

Тот же приём ловит дубли: значение 2 значит, что строка в первой таблице встречается на один раз больше, чем во второй.

Свернуть() суммирует только числа. Если колонка ресурса создана без типа и в неё попала строка или Неопределено, итог будет неверным. Задавайте колонкам тип Число явно.

Поиск расхождений через Найти и НайтиСтроки

Самый очевидный путь — пройти по первой таблице и для каждой строки искать пару во второй. Здесь новички часто путают два метода.

  • 🔹 Найти(Значение, Колонки) ищет одно значение в перечисленных колонках и возвращает первую найденную строку. Искать по паре «номенклатура + склад» этим методом нельзя.
  • 🔹 НайтиСтроки(Отбор) принимает структуру с несколькими полями и возвращает массив подходящих строк. Для составного ключа нужен именно он.
Стало.Индексы.Добавить("Номенклатура, Склад");
Отбор = Новый Структура("Номенклатура, Склад");

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

Строку Стало.Индексы.Добавить(...) не пропускайте. Без индекса каждый вызов НайтиСтроки перебирает всю вторую таблицу. На двух таблицах по 50 000 строк это 2,5 млрд сравнений. С индексом поиск идёт по нему, и цикл укладывается в секунды.

Учтите два ограничения:

  1. Индекс помогает, только если поля отбора совпадают с полями индекса. Индекс по "Номенклатура, Склад" не ускорит отбор только по Склад.
  2. Этот цикл найдёт удалённые и изменённые строки, но не добавленные. Для них нужен второй проход — по таблице «Стало» с поиском в «Было».

Сравнение через Соответствие: один проход по каждой таблице

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

Главная сложность — составной ключ. Структуру ключом соответствия сделать нельзя: две разные структуры с одинаковыми полями — это два разных ключа. Ключ должен быть значением простого типа или ссылкой. Для нескольких колонок соберите строку:

Функция КлючСтроки(Строка)
	Возврат СтрШаблон("%1|%2",
		Строка.Номенклатура.УникальныйИдентификатор(),
		Строка.Склад.УникальныйИдентификатор());
КонецФункции

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

Добавленные = Новый Массив;
Для Каждого СтрокаСтало Из Стало Цикл
	Ключ = КлючСтроки(СтрокаСтало);
	Пара = Индекс.Получить(Ключ);
	Если Пара = Неопределено Тогда
		Добавленные.Добавить(СтрокаСтало);
	Иначе
		Если Пара.Количество <> СтрокаСтало.Количество Тогда
			// изменилось
		КонецЕсли;
		Индекс.Удалить(Ключ);
	КонецЕсли;
КонецЦикла;
// Всё, что осталось в Индекс, — строки, удалённые из «Было»

Почему уникальный идентификатор, а не представление? Представление Строка(Ссылка) у двух разных элементов может совпасть: два товара «Болт М8» в разных группах дадут одинаковый ключ. Идентификатор уникален всегда.

Если ключ — одна колонка ссылочного или простого типа, строку собирать не нужно. Кладите в соответствие само значение.

Если в первой таблице ключ повторяется, Вставить молча перезапишет предыдущую строку. Сверните таблицы по ключу заранее или храните в соответствии массив строк.

Сравнение таблиц значений запросом: временные таблицы и ПОЛНОЕ СОЕДИНЕНИЕ

Когда таблицы большие или к сравнению нужны данные из базы (например, текущие остатки), удобнее отдать работу СУБД. Таблицы значений передаются в запрос параметрами и выгружаются во временные таблицы.

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

Запрос.УстановитьПараметр("Было", Было);
Запрос.УстановитьПараметр("Стало", Стало);
Расхождения = Запрос.Выполнить().Выгрузить();

ПОЛНОЕ СОЕДИНЕНИЕ возвращает строки из обеих таблиц, даже если пары нет. У отсутствующей стороны поля равны NULL, поэтому без ЕСТЬNULL условие ГДЕ отбросит именно те строки, ради которых всё затевалось.

Три правила, без которых запрос не заработает или соврёт:

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

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

Вариант через СКД

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

  1. В схеме создайте два набора данных типа «Объект» с именами объектов Было и Стало. Удобнее положить их в один набор-объединение.
  2. В первом наборе количество положите в поле КоличествоБыло, во втором — в КоличествоСтало. Оба поля сделайте ресурсами с функцией Сумма.
  3. Добавьте вычисляемое поле Разница (КоличествоСтало - КоличествоБыло) и тоже сделайте его ресурсом.
  4. Сгруппируйте по номенклатуре и в настройках поставьте отбор Разница не равно 0.

Исполнение из кода:

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

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

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

Для вывода в табличный документ генератор не указывайте и используйте ПроцессорВыводаРезультатаКомпоновкиДанныхВТабличныйДокумент. Для разовой проверки СКД избыточна: запрос с соединением короче.

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

Самые неприятные расхождения — когда в отладчике значения выглядят одинаково, а сравнение говорит «разные». Проверьте эти случаи:

  • 🔸 Регистр строк. Во встроенном языке "Болт" = "БОЛТ" даёт Ложь. В запросе строки могут сравниться без учёта регистра. Из-за этого код и запрос дают разные результаты на одних данных. Приведите строки к одному виду через ВРег() до сравнения.
  • 🔸 Пробелы. Значение "А-100 " с хвостовым пробелом из загруженного файла не равно "А-100". Чистите через СокрЛП().
  • 🔸 Пустая ссылка и Неопределено. Справочники.Склады.ПустаяСсылка() не равна Неопределено. Если одна таблица пришла из документа, а другая собрана вручную, пустые поля часто различаются именно так.
  • 🔸 NULL из запроса. После левого соединения в колонке может оказаться NULL. Он не равен ни нулю, ни пустой ссылке. Заменяйте через ЕСТЬNULL ещё в запросе.
  • 🔸 Дата со временем. 01.10.2026 00:00:00 и 01.10.2026 14:35:12 — разные значения. Если важен только день, приведите к НачалоДня().
  • 🔸 Число как строка. Колонка без типа может хранить "5" в одной таблице и 5 в другой. Такие значения не равны.
  • 🔸 Точность чисел. Суммы, посчитанные делением, дают хвосты вроде 0,0000001. Перед сравнением округлите: Окр(Сумма, 2).

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

Если таблицы пришли из разных источников с разными именами колонок, сначала приведите их к одной схеме. ЗаполнитьЗначенияСвойств копирует только колонки с совпадающими именами, остальные молча пропустит.

Скорость сравнения больших таблиц: какой метод выбрать

На таблицах в сотни строк годится любой способ. Разница проявляется от десятков тысяч строк.

МетодСложностьКогда братьСлабое место
Найти / НайтиСтроки без индексаn × mДо ~1 000 строк, разовый скриптКвадратичный рост времени
НайтиСтроки с индексом≈ nНужно найти пару и что-то с ней сделатьИндекс тратит память, второй проход для добавленных
Соответствие≈ n + mСоставной ключ, логика по каждой пареНадо собирать ключ, следить за дублями
Свернуть со знаком≈ n + mНужен только список расхожденийНе видно «было/стало» в одной строке
Запрос с ПОЛНЫМ СОЕДИНЕНИЕМна стороне СУБДСотни тысяч строк, нужны данные из базыПередача таблиц на сервер СУБД, типизация колонок

Что ещё ускоряет сравнение:

  1. Сократите колонки до сравнения. Было.Скопировать(, "Номенклатура, Количество") вернёт таблицу только с нужными полями. Сворачивать и передавать в запрос её быстрее.
  2. Не гоняйте таблицы между клиентом и сервером. Каждый серверный вызов с таблицей в параметре сериализует её целиком. Получайте и сравнивайте данные в одной серверной процедуре, на клиент отдавайте только итог.
  3. Не обращайтесь к реквизитам ссылок в цикле. Строка.Номенклатура.Артикул внутри цикла — это обращение к базе на каждой итерации. Нужные реквизиты получите запросом заранее.
  4. Удаляйте строки обратным циклом или собирайте результат в новую таблицу. Удаление в прямом цикле Для Каждого пропускает строки.
Что вам нужно получить?

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

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