Как объединить две таблицы значений в 1С и рассчитать количество подарков по отделам?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

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

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

Вариант 1. Программный обход в цикле (с оптимизацией производительности)

Для начала разберем самый простой и очевидный подход — перебор строк в цикле. Представим, что у нас есть две исходные таблицы значений: Отдел (колонки "Отдел", "ФИО") и Дети (колонки "ФИО", "КоличествоДетей"). Нам нужно получить итоговую таблицу тзПодарки, где данные будут сгруппированы по отделам, а количество подарков рассчитано как произведение количества детей на 2.

Посмотрим на классический, но крайне неоптимальный код, который часто используют начинающие разработчики:


// Создаем результирующую таблицу путем копирования структуры
тзПодарки = Отдел.Скопировать();
тзПодарки.Колонки.Добавить("КоличествоПодарков", Новый ОписаниеТипов("Число"));

Для Каждого Стр Из тзПодарки Цикл
    // ВНИМАНИЕ: Метод Скопировать() с отбором создает новую ТЗ в памяти на каждой итерации!
    ТЗ = Дети.Скопировать(Новый Структура("ФИО", Стр.ФИО));
    Стр.КоличествоПодарков = ТЗ.Итог("КоличествоДетей") * 2;
КонецЦикла;

тзПодарки.Свернуть("Отдел", "КоличествоПодарков");

Выясним причину, почему данный код использовать нельзя. Метод Скопировать() с передачей структуры отбора на каждом шаге цикла создает в оперативной памяти абсолютно новый объект ТаблицаЗначений. Если в вашей таблице будет 1000 сотрудников, система создаст и удалит 1000 объектов ТЗ. Это приведет к колоссальной и абсолютно бессмысленной нагрузке на процессор и сборщик мусора (Garbage Collector). На больших базах данных такой код вызовет серьезные зависания системы.

Оптимизация программного метода через индексирование

Чтобы избежать создания лишних объектов в памяти, мы можем использовать метод НайтиСтроки(). Этот метод не создает новые таблицы значений, а лишь возвращает массив ссылок на уже существующие строки исходной таблицы. Однако для того чтобы поиск работал мгновенно даже на десятках тысяч строк, нам обязательно нужно добавить индекс по ключевому полю поиска.

Разберем оптимизированный вариант с использованием индексов пошагово:


// Добавляем индекс по полю ФИО для ускорения поиска в разы!
Дети.Индексы.Добавить("ФИО");

тзПодарки = Отдел.Скопировать();
тзПодарки.Колонки.Добавить("КоличествоПодарков", Новый ОписаниеТипов("Число"));

Для Каждого Стр Из тзПодарки Цикл
    // НайтиСтроки возвращает массив строк, что работает значительно быстрее копирования
    НайденныеСтроки = Дети.НайтиСтроки(Новый Структура("ФИО", Стр.ФИО));
    
    СуммаДетей = 0;
    Для Каждого СтрокаДети Из НайденныеСтроки Цикл
        СуммаДетей = СуммаДетей + Число(СтрокаДети.КоличествоДетей);
    КонецЦикла;
    
    Стр.КоличествоПодарков = СуммаДетей * 2;
КонецЦикла;

тзПодарки.Свернуть("Отдел", "КоличествоПодарков");

Самый быстрый программный метод: использование Соответствия (Map)

Если вам требуется максимальное быстродействие при обработке больших объемов данных программным способом, лучшим решением будет использование объекта Соответствие (хэш-таблицы). Мы выполним всю задачу всего за два последовательных прохода (сложность алгоритма составит линейное время O(N) вместо квадратичного O(N^2) при вложенных поисках).

Проанализируем этот изящный алгоритм:


// Шаг 1. Группируем данные о детях в Соответствие (ключ - ФИО, значение - общее кол-во детей)
СоответствиеДетей = Новый Соответствие;
Для Каждого Стр Из Дети Цикл
    Колво = Число(Стр.КоличествоДетей);
    ТекущееКолво = СоответствиеДетей.Получить(Стр.ФИО);
    
    Если ТекущееКолво = Неопределено Тогда
        СоответствиеДетей.Вставить(Стр.ФИО, Колво);
    Else
        СоответствиеДетей.Вставить(Стр.ФИО, ТекущееКолво + Колво);
    КонецЕсли;
КонецЦикла;

// Шаг 2. Заполняем результирующую таблицу подарков
тзПодарки = Отдел.Скопировать();
тзПодарки.Колонки.Добавить("КоличествоПодарков", Новый ОписаниеТипов("Число"));

Для Каждого Стр Из тзПодарки Цикл
    КолвоДетей = СоответствиеДетей.Получить(Стр.ФИО);
    // Если у сотрудника нет детей, метод Получить() вернет Неопределено
    КолвоДетей = ?(КолвоДетей = Неопределено, 0, КолвоДетей);
    Стр.КоличествоПодарков = КолвоДетей * 2;
КонецЦикла;

// Шаг 3. Сворачиваем итоговую таблицу по отделам
тзПодарки.Свернуть("Отдел", "КоличествоПодарков");

Данный метод работает в десятки раз быстрее любых поисков по таблицам значений, так как поиск ключа в объекте Соответствие выполняется платформой на уровне платформенного С++ кода практически мгновенно.

Вариант 2. Объединение таблиц значений через Запрос

Встроенный язык запросов 1С — мощнейший инструмент для обработки и группировки данных. Мы можем передать наши таблицы значений в качестве параметров запроса, временно поместить их в базу данных, связать с помощью конструкции ЛЕВОЕ СОЕДИНЕНИЕ и сгруппировать по отделам.

Давайте посмотрим, как выглядит правильный запрос для решения нашей задачи:


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

Запрос.УстановитьПараметр("Отдел", Отдел);
Запрос.УстановитьПараметр("Дети", Дети);

тзПодарки = Запрос.Выполнить().Выгрузить();

Важный нюанс при левом соединении: Обратите внимание на использование функции ЕСТЬNULL в тексте запроса. Если у сотрудника нет детей (т.е. он отсутствует во второй таблице значений), результатом левого соединения в колонке КоличествоДетей будет значение NULL. Математические операции с NULL всегда возвращают NULL, поэтому обязательно оборачивайте числовые суммируемые поля в функцию ЕСТЬNULL(..., 0).

Критически важный нюанс: Обязательная типизация колонок ТЗ для запроса

Если вы попытаетесь передать в запрос программно созданную таблицу значений, у которой колонки добавлены простым методом Колонки.Добавить("ИмяКолонки") без указания типов, вы неизбежно столкнетесь с ошибкой платформы: "Тип не может быть выбран в запросе".

Это происходит потому, что СУБД (будь то MS SQL Server или PostgreSQL) при создании временной таблицы в своей памяти должна жестко знать физические типы данных полей. Если типы колонок ТЗ не определены (имеют тип Неопределено), платформа 1С не сможет сопоставить им SQL-типы данных.

Многие начинающие разработчики пытаются типизировать колонки упрощенным способом, например: Новый ОписаниеТипов("Строка",, 200). Однако этот синтаксис не поддерживается конструктором напрямую и вызовет ошибку компиляции. Чтобы задать колонке ТЗ ограниченную длину строки или числовой формат для использования в запросах, нужно явно использовать объекты квалификаторов.

Рассмотрим правильный синтаксис типизации колонок с использованием квалификаторов строки и числа:


// 1. Создаем квалификатор строки и задаем длину 200 символов
КвалификаторыСтроки = Новый КвалификаторыСтроки(200);
ОписаниеСтрока200 = Новый ОписаниеТипов("Строка", , КвалификаторыСтроки);

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

// 2. Создаем квалификатор числа (длина 10, точность 0 - для целых чисел)
КвалификаторыЧисла = Новый КвалификаторыЧисла(10, 0);
ОписаниеЧисла = Новый ОписаниеТипов("Число", КвалификаторыЧисла);

Дети = Новый ТаблицаЗначений;
Дети.Колонки.Добавить("ФИО", ОписаниеСтрока200);
Дети.Колонки.Добавить("КоличествоДетей", ОписаниеЧисла);

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

Почему прямой запрос к реальным данным базы всегда лучше таблиц значений?

Передача таблиц значений в запрос в качестве параметров — это рабочий инструмент, но его следует использовать преимущественно тогда, когда таблицы значений формируются на клиенте пользователем вручную или загружаются из внешних источников (например, при парсинге файлов Excel, XML или JSON).

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

  1. Избыточное потребление оперативной памяти: Сервер приложений 1С вынужден удерживать в памяти потенциально огромные массивы строк таблиц значений.
  2. Трафик между сервером приложений и СУБД: Передача временных таблиц туда и обратно создает лишнюю сетевую нагрузку и сериализацию данных.
  3. Невозможность оптимального использования индексов СУБД: СУБД оптимизирует запросы к физическим таблицам базы данных на основе статистик и готовых индексов. Для временных таблиц, созданных "на лету" из переданных ТЗ, такие оптимизации малоэффективны.

Поэтому профессиональным архитектурным решением будет написание одного прямого запроса к реальным таблицам базы данных (например, к регистру сведений кадровой истории сотрудников и регистру состава семьи). База данных сама произведет левое соединение, отбор и группировку на уровне дисков и индексов, вернув на сервер 1С сразу готовый и сжатый результат.

← На главную