Ищем расхождения между двумя таблицами значений на встроенном языке
Содержание
Готового метода «сравнить две таблицы значений» в платформе 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)";
Запрос.УстановитьПараметр("ТаблицаУчет", ТЗУчет);
Запрос.УстановитьПараметр("ТаблицаФакт", ТЗФакт);
Расхождения = Запрос.Выполнить().Выгрузить();
Что здесь происходит:
- Первые два пакета переносят таблицы значений во временные таблицы.
- Следующие два сворачивают дубли ключа. Без этого шага
ПОЛНОЕ СОЕДИНЕНИЕразмножит строки: две строки учёта на две строки факта дадут четыре. - Последний пакет соединяет таблицы.
ПОЛНОЕ СОЕДИНЕНИЕоставляет строки, которые есть только с одной стороны, аЕСТЬNULLпревращает отсутствие в ноль. - Условие
ГДЕотбрасывает совпавшие строки.
Альтернатива ПОЛНОМУ СОЕДИНЕНИЮ — ОБЪЕДИНИТЬ ВСЕ. Из учёта берут Количество КАК ПоУчету, 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, Текущее) + СтрокаФакта.Количество);
КонецЦикла;
Для Каждого СтрокаУчета Из ТЗУчет Цикл
Факт = ФактПоТовару[СтрокаУчета.Номенклатура];
Если Факт = Неопределено Тогда
// Товара нет в факте
ИначеЕсли Факт <> СтрокаУчета.Количество Тогда
// Количество расходится
КонецЕсли;
КонецЦикла;
Каждый поиск по соответствию идёт по хешу ключа и почти не зависит от размера.
Что ещё ускоряет
- Сравнивайте только нужные колонки:
ТЗ.Скопировать(, "Номенклатура, Количество")вместо всей таблицы с двадцатью колонками. - Добавляйте индекс один раз до цикла, а не внутри.
- Не гоняйте таблицу между клиентом и сервером.
ТаблицаЗначенийв тонком клиенте недоступна. Реквизит формы переводите на сервере черезРеквизитФормыВЗначениеи сравнивайте там же. - Для сотен тысяч строк не держите обе таблицы в памяти: если данные приходят из базы, сравнивайте их запросом сразу, без выгрузки в таблицы значений.