Как добавить колонку в ТаблицуЗначений из другой таблицы и обработать отсутствующие значения?

Программист IT и автоматизация бизнеса
← На главную

Часто в разработке на 1С возникает задача: нужно обогатить одну таблицу значений данными из другой. Например, у нас есть список сотрудников в одной таблице, а их текущие состояния (в отпуске, работает, уволен) — в другой. Нам нужно для каждого сотрудника из первой таблицы проставить его состояние из второй. Рассмотрим, как это сделать, и разберем потенциальные проблемы, которые могут возникнуть в процессе.

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

Решение "в лоб": перебор в цикле

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

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


// Предположим, ТЗ1 и ТЗ2 уже существуют и заполнены.

// Создадим результирующую таблицу, скопировав структуру и данные из ТЗ1.
ТЗРез = ТЗ1.Скопировать();

// Добавим в нее новую колонку "Состояние", если ее еще нет.
// Важно правильно определить тип для этой колонки!
Если ТЗРез.Колонки.Найти("Состояние") = Неопределено Тогда
    // Задаем составной тип, чтобы можно было хранить и исходные данные, и нашу строку
    МассивТипов = Новый Массив;
    МассивТипов.Добавить(Тип("Строка")); 
    // Если в ТЗ2 состояние может быть не строкой, добавляем и его тип
    // МассивТипов.Добавить(Тип("СправочникСсылка.СостоянияСотрудников")); 
    
    ОписаниеТиповСостояние = Новый ОписаниеТипов(МассивТипов);
    ТЗРез.Колонки.Добавить("Состояние", ОписаниеТиповСостояние, "Состояние сотрудника");
КонецЕсли;

// Теперь пройдем циклом по результирующей таблице и заполним новую колонку
Для Каждого СтрокаТЗРез Из ТЗРез Цикл
    
    // Ищем в ТЗ2 строку с таким же сотрудником
    НайденнаяСтрока = ТЗ2.Найти(СтрокаТЗРез.Сотрудник, "Сотрудник"); 
    
    // Проверяем результат поиска
    Если НайденнаяСтрока <> Неопределено Тогда   
        // Если сотрудник найден, копируем его состояние
        СтрокаТЗРез.Состояние = НайденнаяСтрока.Состояние;
    Иначе 
        // Если сотрудник не найден, присваиваем строковое значение
        СтрокаТЗРез.Состояние = "Не найден";
    КонецЕсли;
    
КонецЦикла;

На первый взгляд, этот код должен работать безупречно. Он логичен и последователен. Однако именно на строке СтрокаТЗРез.Состояние = "Не найден"; может возникнуть скрытая проблема.

Подводный камень: несоответствие типов данных

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

Что произойдет в этом случае?

Когда мы попытаемся присвоить этой колонке значение "Не найден", 1С либо вызовет ошибку преобразования типов, либо (что чаще и коварнее) просто "отрежет" строку до допустимой длины. В итоге в ячейке окажется только первая буква — "Н". Это частая ошибка, которая может поставить в тупик начинающего разработчика.

Как этого избежать?

Перед заполнением данных необходимо убедиться, что тип колонки "Состояние" в таблице ТЗРез позволяет хранить длинные строки. Как показано в коде выше, лучше всего явно добавить колонку с нужным ОписаниемТипов, которое включает в себя тип Строка неограниченной длины. Если колонка уже существует, ее тип можно изменить, но это более сложная операция (требует создания новой таблицы с правильной структурой и переноса данных).

Решение 1: Оптимизация поиска в цикле с помощью индексации

Метод с циклом вполне рабочий, особенно для небольших таблиц. Но если во второй таблице (ТЗ2) содержатся тысячи или десятки тысяч строк, то на каждой итерации цикла метод Найти() будет выполнять полный перебор этой таблицы. Это может серьезно замедлить выполнение кода.

Чтобы ускорить поиск, можно предварительно проиндексировать колонку, по которой ведется поиск. Это делается очень просто.


// Индексируем колонку "Сотрудник" в ТЗ2 для ускорения поиска
ТЗ2.Индексы.Добавить("Сотрудник");

// Дальнейший код с циклом остается без изменений, 
// но теперь метод Найти() будет работать в разы быстрее.
Для Каждого СтрокаТЗРез Из ТЗРез Цикл
    НайденнаяСтрока = ТЗ2.Найти(СтрокаТЗРез.Сотрудник, "Сотрудник"); 
    // ... и так далее
КонецЦикла;

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

Решение 2: Эффективное объединение с помощью Запроса (Рекомендуемый способ)

Несмотря на то, что цикл с индексацией работает быстро, существует более элегантный, производительный и идиоматичный для 1С способ — использование запроса. Вместо того чтобы вручную перебирать строки, мы можем "попросить" платформу 1С саму сопоставить данные.

Для этого мы поместим наши таблицы значений во временные таблицы запроса и соединим их с помощью ЛЕВОГО ВНЕШНЕГО СОЕДИНЕНИЯ. Этот тип соединения берет все записи из левой таблицы (ТЗ1) и присоединяет к ним совпадающие записи из правой таблицы (ТЗ2). Если совпадения нет, поля из правой таблицы будут иметь значение NULL.

Для обработки значений NULL в запросе есть прекрасная функция ЕСТЬNULL(), которая заменяет NULL на указанное нами значение. В нашем случае — на строку "Не найден".

Посмотрим на пример кода:


// 1. Создаем объект Запрос
Запрос = Новый Запрос;

// 2. Помещаем наши таблицы во временные таблицы запроса
// Для этого используется МенеджерВременныхТаблиц
МВТ = Новый МенеджерВременныхТаблиц;
Запрос.МенеджерВременныхТаблиц = МВТ;

// Помещаем ТЗ1 во временную таблицу "ВТ_Сотрудники"
Запрос.Текст = 
"ВЫБРАТЬ
|    ТЗ1.Сотрудник
|ПОМЕСТИТЬ ВТ_Сотрудники
|ИЗ
|    &ТЗ1 КАК ТЗ1";
Запрос.УстановитьПараметр("ТЗ1", ТЗ1);
Запрос.Выполнить();

// Помещаем ТЗ2 во временную таблицу "ВТ_Состояния"
Запрос.Текст = 
"ВЫБРАТЬ
|    ТЗ2.Сотрудник,
|    ТЗ2.Состояние
|ПОМЕСТИТЬ ВТ_Состояния
|ИЗ
|    &ТЗ2 КАК ТЗ2";
Запрос.УстановитьПараметр("ТЗ2", ТЗ2);
Запрос.Выполнить();

// 3. Пишем основной запрос для объединения данных
Запрос.Текст = 
"ВЫБРАТЬ
|    ВТ_Сотрудники.Сотрудник,
|    ЕСТЬNULL(ВТ_Состояния.Состояние, ""Не найден"") КАК Состояние
|ИЗ
|    ВТ_Сотрудники КАК ВТ_Сотрудники
|        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Состояния КАК ВТ_Состояния
|        ПО ВТ_Сотрудники.Сотрудник = ВТ_Состояния.Сотрудник";

// 4. Выполняем запрос и выгружаем результат в новую таблицу
ТЗРез = Запрос.Выполнить().Выгрузить();

Преимущества этого подхода:

  1. Производительность. Запросы выполняются на уровне платформы, которая оптимизирована для работы с большими наборами данных. Это почти всегда будет быстрее ручного перебора в цикле, даже с индексами.
  2. Читаемость и лаконичность. Текст запроса декларативно описывает, что мы хотим получить, а не как мы это делаем по шагам. Такой код проще понимать и поддерживать.
  3. Надежность. Проблема с типами данных решается автоматически. Функция ЕСТЬNULL() корректно вернет строковое значение, и платформа сама создаст результирующую таблицу с колонкой подходящего типа.

В заключение, для решения задачи обогащения одной таблицы данными из другой всегда старайтесь использовать запросы. Это более профессиональный, быстрый и надежный подход. Метод с циклом допустим для небольших объемов данных или в ситуациях, где использование запроса по каким-то причинам невозможно, но всегда помните о контроле типов данных и оптимизации поиска с помощью индексов.

← На главную