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

· 10 мин

Содержание

Готового метода «сравнить две таблицы значений» в платформе 1С 8.3 нет. Оператор ТЗ1 = ТЗ2 сравнивает ссылки на объекты, а не содержимое, и вернёт Ложь даже для двух одинаковых таблиц. Поэтому сравнивать таблицы значений приходится одним из четырёх способов: сериализацией в строку, перебором строк в цикле, сверткой со знаком или запросом к временным таблицам.

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

Что умеет платформа и где её методы заканчиваются

У объекта ТаблицаЗначений нет метода сравнения, но есть методы, из которых сравнение собирается:

  • 📋 Скопировать(Строки, Колонки) — копия таблицы целиком или только нужных колонок. Нужна, чтобы не испортить исходник.
  • 🔁 Свернуть(КолонкиГруппировок, КолонкиСуммирования) — группировка с суммированием. Меняет саму таблицу, на которой вызван.
  • 🔎 НайтиСтроки(Структура) — массив строк по отбору. С индексом работает быстро.
  • 🗂 Индексы.Добавить("Колонка1, Колонка2") — индекс для поиска.
  • 📤 ВыгрузитьКолонку(Колонка) — массив значений одной колонки.
  • ➕ Итог(Колонка) — сумма по числовой колонке.

Быстрая проверка через строку

Если нужен только ответ «да/нет», сравните внутренние строковые представления:

Функция ТаблицыСовпадаютПолностью(ТЗ1, ТЗ2)
	Возврат ЗначениеВСтрокуВнутр(ТЗ1) = ЗначениеВСтрокуВнутр(ТЗ2);
КонецФункции

Способ строгий. Он учитывает порядок строк, порядок и имена колонок, описание типов колонок. Две таблицы с одинаковыми данными, но с колонкой Число(15,2) в одной и Число(15,3) в другой, дадут Ложь.

Сравнение итогов — дешёвая первая проверка перед дорогой. Если ТЗ1.Итог("Сумма") <> ТЗ2.Итог("Сумма") или не совпадает Количество(), дальше можно не идти: таблицы точно разные.

Функция из БСП

В конфигурациях на Библиотеке стандартных подсистем (БП 3.0, ЗУП 3.1, УТ 11) в общем модуле ОбщегоНазначения есть функция КоллекцииИдентичны. Она сравнивает две коллекции по колонкам и умеет игнорировать порядок строк. Сигнатуру и параметры посмотрите в модуле вашей версии БСП: набор параметров менялся от редакции к редакции.

Перебор строк в цикле: проверка «один в один»

Ручной цикл нужен, когда сериализация слишком строгая. Например, типы колонок у таблиц разные, а значения одинаковые. Алгоритм проверяет количество строк, состав колонок и потом значения в каждой ячейке.

Функция ТаблицыИдентичны(ТЗ1, ТЗ2) Экспорт

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

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

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

	Для Индекс = 0 По ТЗ1.Количество() - 1 Цикл
		Строка1 = ТЗ1[Индекс];
		Строка2 = ТЗ2[Индекс];
		Для Каждого Колонка Из ТЗ1.Колонки Цикл
			Если Строка1[Колонка.Имя] <> Строка2[Колонка.Имя] Тогда
				Возврат Ложь;
			КонецЕсли;
		КонецЦикла;
	КонецЦикла;

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

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

Функция выходит на первом же расхождении, поэтому на разных таблицах работает быстро. Колонки сравниваются по имени, а не по позиции: таблица с колонками «Товар, Цена» равна таблице «Цена, Товар» с теми же данными.

Порядок строк здесь важен. Если строки могут идти в разном порядке, отсортируйте обе копии одинаково (Сортировать("Номенклатура, Склад")) или переходите к сравнению по ключу.

Не сравнивайте так таблицы, в ячейках которых лежат другие таблицы значений, массивы или структуры. Оператор <> для них сравнивает ссылки на объекты, и функция вернёт Ложь для одинаковых данных.

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

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

Способ 1. Свертка со знаком

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

Функция РасхожденияТаблиц(ТЗ1, ТЗ2, КлючевыеКолонки, СуммовыеКолонки) Экспорт

	Результат = ТЗ1.Скопировать(, КлючевыеКолонки + "," + СуммовыеКолонки);
	ИменаСумм = СтрРазделить(СуммовыеКолонки, ",", Ложь);

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

	Результат.Свернуть(КлючевыеКолонки, СуммовыеКолонки);

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

	Возврат Результат;

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

Вызов: Расхождения = РасхожденияТаблиц(ТЗУчет, ТЗФакт, "Номенклатура, Склад", "Количество, Сумма");

Как читать результат. Учёт 10 шт., факт 7 шт. — в результате 3: по учёту больше на 3. Учёт 10, факт 12 — в результате -2: факта больше на 2. Товара нет в учёте, но есть в факте — строка с отрицательным количеством.

Удаление идёт с конца таблицы: индексы оставшихся строк при этом не сдвигаются. СтрРазделить есть в платформе начиная с 8.3.6.

Если суммовых колонок нет, а нужно понять, каких строк нет во второй таблице, добавьте в обе копии колонку Счетчик со значением 1 и сворачивайте по всем остальным колонкам. Ненулевой счётчик покажет «лишние» строки.

Способ 2. НайтиСтроки с индексом

Подходит, когда нужно не просто число, а построчное действие: записать расхождение в регистр, подсветить строку на форме.

ТЗФакт.Индексы.Добавить("Номенклатура, Склад");
Отбор = Новый Структура("Номенклатура, Склад");

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

Код предполагает, что ключ в каждой таблице уникален. Если нет — сначала сверните копии через Свернуть("Номенклатура, Склад", "Количество"). Обратный проход по ТЗФакт с тем же отбором покажет строки, которых нет в учёте.

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

Запрос удобен, когда таблицы большие или сравнение нужно дополнить данными базы: реквизитами номенклатуры, ценами, остатками регистра. Работает только на сервере.

Таблицу значений как источник обычно сразу помещают во временную таблицу, а соединение и группировку делают уже с временными таблицами:

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

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

Что здесь происходит:

  1. Первые два пакета переносят таблицы значений во временные таблицы.
  2. Следующие два сворачивают дубли ключа. Без этого шага ПОЛНОЕ СОЕДИНЕНИЕ размножит строки: две строки учёта на две строки факта дадут четыре.
  3. Последний пакет соединяет таблицы. ПОЛНОЕ СОЕДИНЕНИЕ оставляет строки, которые есть только с одной стороны, а ЕСТЬNULL превращает отсутствие в ноль.
  4. Условие ГДЕ отбрасывает совпавшие строки.

Альтернатива ПОЛНОМУ СОЕДИНЕНИЮ — ОБЪЕДИНИТЬ ВСЕ. Из учёта берут Количество КАК ПоУчету, 0 КАК ПоФакту, из факта наоборот, затем группируют по номенклатуре и оставляют ИМЕЮЩИЕ СУММА(ПоУчету) <> СУММА(ПоФакту). Это тот же принцип, что и свертка со знаком, только в запросе.

Колонки должны быть типизированы

Запрос к таблице значений требует, чтобы у колонок было описание типов. Колонка, созданная как Колонки.Добавить("Номенклатура") без типа, приведёт к ошибке выполнения запроса, обычно с текстом Тип не может быть выбран в запросе.

ТЗ.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
ТЗ.Колонки.Добавить("Артикул", Новый ОписаниеТипов("Строка", , Новый КвалификаторыСтроки(50)));
ТЗ.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 3)));

Строковой колонке задайте длину. Строку неограниченной длины нельзя использовать в условии соединения и в группировке.

Ключевые поля и составные типы: где сравнение обманывает

Сравнение «верно на тесте, врёт на рабочих данных» почти всегда упирается в типы.

  • ⚪ Неопределено и пустая ссылка — разные значения. Справочники.Номенклатура.ПустаяСсылка() <> Неопределено. Если одна таблица заполнена из документа, а другая из файла, пустые ячейки могут оказаться разного типа. Приводите к одному виду до сравнения.
  • ⚫ NULL появляется после запроса. Таблица, выгруженная из запроса с левым соединением, содержит NULL, а таблица, собранная в коде, — пустые значения. Заворачивайте поля в ЕСТЬNULL уже в исходном запросе.
  • 🕐 Дата со временем. 07.10.2026 00:00:00 и 07.10.2026 14:30:00 не равны. Если сравниваете по дню, приведите обе даты через НачалоДня().
  • 🔢 Дробные числа после деления. Число в 1С десятичное, но результат деления может нести много знаков. 100 / 3 * 3 не обязано дать ровно 100. Округляйте перед сравнением: Окр(Значение, 2).
  • 🔤 Регистр и пробелы в строках. "ООО Ромашка" и "ООО Ромашка " в коде не равны. Для ключа из внешнего файла используйте СокрЛП() и ВРег().
  • 🧩 Составной тип колонки. Если колонка хранит и СправочникСсылка.Контрагенты, и СправочникСсылка.ФизическиеЛица, совпадение представлений ничего не значит: «Иванов И.И.» может быть в обоих справочниках.

Ключ из нескольких колонок держите в одном месте: строка вида "Номенклатура, Склад, Характеристика" передаётся и в Свернуть, и в Индексы.Добавить, и в Новый Структура. Так ключ не разойдётся между частями кода.

Большие объёмы: как не повесить сервер

Наивный вложенный цикл «каждая строка первой таблицы против каждой строки второй» растёт квадратично. Две таблицы по 10 000 строк — это 100 млн проверок. Две по 1 000 — всего миллион, поэтому на тесте проблема не видна.

СпособСложностьКогда подходитРиски
Вложенный цикл без индексаквадратичнаядо нескольких сотен строкминуты на больших данных
НайтиСтроки с индексомблизка к линейнойпострочная обработка, тысячи строкиндекс забыли — снова квадрат
Соответствие по ключулинейнаяодин ключ-ссылкасоставной ключ надо собирать вручную
Свертка со знакомлинейнаянужны только разницы суммСвернуть портит исходник без копии
Запрос с временными таблицамина стороне СУБДдесятки тысяч строк и больше, связь с данными базынужна типизация колонок

Соответствие вместо поиска

Для ключа из одной колонки-ссылки быстрее всего Соответствие:

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

Для Каждого СтрокаУчета Из ТЗУчет Цикл
	Факт = ФактПоТовару[СтрокаУчета.Номенклатура];
	Если Факт = Неопределено Тогда
		// Товара нет в факте
	ИначеЕсли Факт <> СтрокаУчета.Количество Тогда
		// Количество расходится
	КонецЕсли;
КонецЦикла;

Каждый поиск по соответствию идёт по хешу ключа и почти не зависит от размера.

Что ещё ускоряет

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

Ошибки, из-за которых сравнение врёт или падает

Диагностика: выберите симптом

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

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