Сравнение таблиц значений во встроенном языке 1С: поиск расхождений по строкам, ключам и суммам
Содержание
Две таблицы значений в 1С нельзя сравнить оператором =: выражение Таблица1 = Таблица2 вернёт Истина только если обе переменные указывают на один и тот же объект. Содержимое оно не проверяет. Чтобы сравнить данные, нужен один из приёмов: свернуть обе таблицы в одну со знаком «+1/−1», построить Соответствие по ключу, найти строки через НайтиСтроки с индексом или передать таблицы в запрос и соединить их ПОЛНЫМ СОЕДИНЕНИЕМ.
Какой метод выбрать, зависит от вопроса. «Таблицы совпадают целиком?» — это одна задача. «Каких строк не хватает и где разошлось количество?» — совсем другая. Всё ниже работает в платформе 8.3 и не зависит от конфигурации: это встроенный язык, а не механизм БП или УТ.
Что именно сравниваем: три постановки задачи
Прежде чем писать код, определите, какой ответ вам нужен. От этого зависит и алгоритм, и объём работы.
| Постановка | Пример | Чем решать |
|---|---|---|
| 🟢 Равны ли таблицы целиком | Проверить, что после рефакторинга отчёт выдаёт то же самое | ЗначениеВСтрокуВнутр, Свернуть со счётчиком |
| 🟡 Какие строки есть только в одной | Документ до и после изменения: что добавили, что удалили | Свернуть со знаком, Соответствие, запрос |
| 🔴 Где по ключу разошлись суммы | Остатки по складу: учёт против инвентаризации | Свернуть с ресурсами, запрос с ПОЛНЫМ СОЕДИНЕНИЕМ |
Во второй и третьей задаче надо заранее решить, какие колонки — ключ (номенклатура, склад, характеристика), а какие — ресурсы (количество, сумма). Без этого «расхождение» не определено.
Быстрая проверка на полное совпадение
Если нужен только ответ «да/нет», сериализуйте обе таблицы и сравните строки:
Функция ТаблицыРавны(Т1, Т2) Экспорт
Если Т1.Количество() <> Т2.Количество() Тогда
Возврат Ложь;
КонецЕсли;
Возврат ЗначениеВСтрокуВнутр(Т1) = ЗначениеВСтрокуВнутр(Т2);
КонецФункции
Метод строгий. В строку попадают имена и порядок колонок, их типы и порядок строк. Две таблицы с одинаковыми данными, но разной сортировкой, окажутся «разными». Перед сравнением отсортируйте обе по одним колонкам методом Сортировать().
В конфигурациях на БСП есть функция ОбщегоНазначения.ДанныеСовпадают(Данные1, Данные2). Она сравнивает коллекции по содержимому, а не по ссылке. Удобна, когда не хочется думать о сериализации.
Ни один из этих способов не скажет, где расхождение. Для этого — методы ниже.
Способ «свернуть со знаком»: самый короткий код для сравнения двух таблиц
Этот приём практики используют чаще всего. Идея простая: сложить обе таблицы в одну, у строк первой ресурсы оставить как есть, у второй — умножить на −1. После Свернуть() по ключевым колонкам совпавшие строки дадут ноль. Всё ненулевое — расхождение.
Функция РасхожденияПоКлючу(Было, Стало, Ключи, Ресурсы) Экспорт
// Ключи = "Номенклатура, Склад", Ресурсы = "Количество, Сумма"
Итог = Было.Скопировать(, Ключи + ", " + Ресурсы);
МассивРесурсов = СтрРазделить(Ресурсы, ",", Ложь);
Для Каждого СтрокаСтало Из Стало Цикл
НоваяСтрока = Итог.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаСтало);
Для Каждого ИмяРесурса Из МассивРесурсов Цикл
ИмяРесурса = СокрЛП(ИмяРесурса);
НоваяСтрока[ИмяРесурса] = -НоваяСтрока[ИмяРесурса];
КонецЦикла;
КонецЦикла;
Итог.Свернуть(Ключи, Ресурсы);
// Удаляем совпавшие строки обратным циклом
Всего = Итог.Количество();
Для Сч = 1 По Всего Цикл
СтрокаИтога = Итог[Всего - Сч];
ЕстьРазница = Ложь;
Для Каждого ИмяРесурса Из МассивРесурсов Цикл
Если СтрокаИтога[СокрЛП(ИмяРесурса)] <> 0 Тогда
ЕстьРазница = Истина;
Прервать;
КонецЕсли;
КонецЦикла;
Если Не ЕстьРазница Тогда
Итог.Удалить(СтрокаИтога);
КонецЕсли;
КонецЦикла;
Возврат Итог; // положительное значение — «было больше», отрицательное — «стало больше»
КонецФункции
Читать результат легко. Строка с Количество = 5 значит, что в «Было» на 5 единиц больше. С Количество = -3 — что в «Стало» прибавилось 3 единицы. Строка, которой не было в первой таблице, придёт целиком со знаком минус.
Сравнение строк целиком, когда ресурсов нет
Если в таблицах нет чисел (например, список пользователей с ролями), добавьте служебную колонку-счётчик:
- Скопируйте первую таблицу и добавьте колонку
Счетчикс типомНовый ОписаниеТипов("Число"). - Всем её строкам поставьте
1, строкам из второй таблицы —-1. - Сверните по всем остальным колонкам, суммируя
Счетчик. - Строки с ненулевым счётчиком есть только в одной таблице. Знак подскажет, в какой.
Тот же приём ловит дубли: значение 2 значит, что строка в первой таблице встречается на один раз больше, чем во второй.
Свернуть() суммирует только числа. Если колонка ресурса создана без типа и в неё попала строка или Неопределено, итог будет неверным. Задавайте колонкам тип Число явно.
Поиск расхождений через Найти и НайтиСтроки
Самый очевидный путь — пройти по первой таблице и для каждой строки искать пару во второй. Здесь новички часто путают два метода.
- 🔹
Найти(Значение, Колонки)ищет одно значение в перечисленных колонках и возвращает первую найденную строку. Искать по паре «номенклатура + склад» этим методом нельзя. - 🔹
НайтиСтроки(Отбор)принимает структуру с несколькими полями и возвращает массив подходящих строк. Для составного ключа нужен именно он.
Стало.Индексы.Добавить("Номенклатура, Склад");
Отбор = Новый Структура("Номенклатура, Склад");
Для Каждого СтрокаБыло Из Было Цикл
ЗаполнитьЗначенияСвойств(Отбор, СтрокаБыло);
Найденные = Стало.НайтиСтроки(Отбор);
Если Найденные.Количество() = 0 Тогда
// строка удалена
ИначеЕсли Найденные[0].Количество <> СтрокаБыло.Количество Тогда
// количество изменилось
КонецЕсли;
КонецЦикла;
Строку Стало.Индексы.Добавить(...) не пропускайте. Без индекса каждый вызов НайтиСтроки перебирает всю вторую таблицу. На двух таблицах по 50 000 строк это 2,5 млрд сравнений. С индексом поиск идёт по нему, и цикл укладывается в секунды.
Учтите два ограничения:
- Индекс помогает, только если поля отбора совпадают с полями индекса. Индекс по
"Номенклатура, Склад"не ускорит отбор только поСклад. - Этот цикл найдёт удалённые и изменённые строки, но не добавленные. Для них нужен второй проход — по таблице «Стало» с поиском в «Было».
Сравнение через Соответствие: один проход по каждой таблице
Соответствие ищет по ключу почти мгновенно, поэтому сравнение получается линейным: один цикл по первой таблице, один — по второй.
Главная сложность — составной ключ. Структуру ключом соответствия сделать нельзя: две разные структуры с одинаковыми полями — это два разных ключа. Ключ должен быть значением простого типа или ссылкой. Для нескольких колонок соберите строку:
Функция КлючСтроки(Строка)
Возврат СтрШаблон("%1|%2",
Строка.Номенклатура.УникальныйИдентификатор(),
Строка.Склад.УникальныйИдентификатор());
КонецФункции
Индекс = Новый Соответствие;
Для Каждого СтрокаБыло Из Было Цикл
Индекс.Вставить(КлючСтроки(СтрокаБыло), СтрокаБыло);
КонецЦикла;
Добавленные = Новый Массив;
Для Каждого СтрокаСтало Из Стало Цикл
Ключ = КлючСтроки(СтрокаСтало);
Пара = Индекс.Получить(Ключ);
Если Пара = Неопределено Тогда
Добавленные.Добавить(СтрокаСтало);
Иначе
Если Пара.Количество <> СтрокаСтало.Количество Тогда
// изменилось
КонецЕсли;
Индекс.Удалить(Ключ);
КонецЕсли;
КонецЦикла;
// Всё, что осталось в Индекс, — строки, удалённые из «Было»
Почему уникальный идентификатор, а не представление? Представление Строка(Ссылка) у двух разных элементов может совпасть: два товара «Болт М8» в разных группах дадут одинаковый ключ. Идентификатор уникален всегда.
Если ключ — одна колонка ссылочного или простого типа, строку собирать не нужно. Кладите в соответствие само значение.
Если в первой таблице ключ повторяется, Вставить молча перезапишет предыдущую строку. Сверните таблицы по ключу заранее или храните в соответствии массив строк.
Сравнение таблиц значений запросом: временные таблицы и ПОЛНОЕ СОЕДИНЕНИЕ
Когда таблицы большие или к сравнению нужны данные из базы (например, текущие остатки), удобнее отдать работу СУБД. Таблицы значений передаются в запрос параметрами и выгружаются во временные таблицы.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Т.Номенклатура КАК Номенклатура,
| Т.Количество КАК Количество
|ПОМЕСТИТЬ ВТ_Было
|ИЗ
| &Было КАК Т
|;
|
|ВЫБРАТЬ
| Т.Номенклатура КАК Номенклатура,
| Т.Количество КАК Количество
|ПОМЕСТИТЬ ВТ_Стало
|ИЗ
| &Стало КАК Т
|;
|
|ВЫБРАТЬ
| ЕСТЬNULL(Б.Номенклатура, С.Номенклатура) КАК Номенклатура,
| ЕСТЬNULL(Б.Количество, 0) КАК КоличествоБыло,
| ЕСТЬNULL(С.Количество, 0) КАК КоличествоСтало
|ИЗ
| ВТ_Было КАК Б
| ПОЛНОЕ СОЕДИНЕНИЕ ВТ_Стало КАК С
| ПО Б.Номенклатура = С.Номенклатура
|ГДЕ
| ЕСТЬNULL(Б.Количество, 0) <> ЕСТЬNULL(С.Количество, 0)";
Запрос.УстановитьПараметр("Было", Было);
Запрос.УстановитьПараметр("Стало", Стало);
Расхождения = Запрос.Выполнить().Выгрузить();
ПОЛНОЕ СОЕДИНЕНИЕ возвращает строки из обеих таблиц, даже если пары нет. У отсутствующей стороны поля равны NULL, поэтому без ЕСТЬNULL условие ГДЕ отбросит именно те строки, ради которых всё затевалось.
Три правила, без которых запрос не заработает или соврёт:
- Колонки должны быть типизированы. Запрос берёт типы полей из описания колонок. Если таблица собрана кодом через
Колонки.Добавить("Количество")безОписаниеТипов, выполнение завершится ошибкой. Создавайте колонки так:Колонки.Добавить("Количество", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 3))). - Дубли ключей сверните. Если номенклатура встречается в таблице дважды, соединение размножит строки. Добавьте во временную таблицу
СУММА(Т.Количество)иСГРУППИРОВАТЬ ПО Т.Номенклатура. - Запрос выполняется на сервере. На клиенте объект
Запроснедоступен, так что сравнение вынесите в серверную процедуру или общий модуль.
Если временные таблицы нужны в нескольких запросах подряд, создайте Новый МенеджерВременныхТаблиц и присвойте его свойству Запрос.МенеджерВременныхТаблиц.
Вариант через СКД
Система компоновки данных пригодится, когда расхождения нужно не обработать кодом, а показать пользователю отчётом с группировками и отбором. Таблицы передаются как внешние наборы данных:
- В схеме создайте два набора данных типа «Объект» с именами объектов
БылоиСтало. Удобнее положить их в один набор-объединение. - В первом наборе количество положите в поле
КоличествоБыло, во втором — вКоличествоСтало. Оба поля сделайте ресурсами с функциейСумма. - Добавьте вычисляемое поле
Разница(КоличествоСтало - КоличествоБыло) и тоже сделайте его ресурсом. - Сгруппируйте по номенклатуре и в настройках поставьте отбор
Разница не равно 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") с одинаковым списком полей и сравнивайте их.Почему ЗначениеВСтрокуВнутр показывает разницу, хотя данные одинаковые?
Число, в другой — без типа. Отсортируйте таблицы и сверьте описания типов колонок.