Как объединить связанные таблицы документов и вывести их параллельно «бок о бок» в 1С

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

В процессе разработки отчетов в системе «1С:Предприятие» программисты часто сталкиваются с задачей сопоставления данных из разных документов. Классический пример — вывод информации о связанных документах «бок о бок». Например, когда у нас есть акционные накладные (полученные по промо-акциям) и основные накладные (которые послужили основанием для начисления этих акций), и нам требуется вывести их в одну строку табличного документа: слева — данные акционного документа (номенклатура, количество), справа — данные основного документа.

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

Выясняем причину: почему простое соединение по номеру строки не работает

Давайте проанализируем стандартную ошибку, которую часто совершают начинающие разработчики. Попытка связать две таблицы по условию Акция.НомерСтроки = Основной.НомерСтроки через ПОЛНОЕ СОЕДИНЕНИЕ приводит к двум серьезным проблемам:

  1. Пропуски и потеря данных. Номера строк в табличных частях документов — это физические индексы. Если в акционном документе было 3 строки (с номерами 1, 2, 3), а в основном — 5 строк (с номерами 1, 2, 3, 4, 5), то при соединении по номеру строки последние две строки основного документа сопоставятся корректно. Но если из середины одного из документов была удалена строка, то индексы собьются, возникнут «дыры», и сопоставление нарушится.
  2. Задвоение (декартово умножение) данных. Если одному акционному документу соответствуют два или более основных документа, то левая часть (акционные товары) продублируется столько раз, сколько документов-оснований привязано к акции. В результате отчет выдаст некорректные и завышенные показатели по количеству и суммам.

Ниже мы рассмотрим три профессиональных метода, которые позволяют обойти эти ограничения и построить идеальный отчет.

---

Вариант 1. Программный вывод через объединение и метод «Присоединить»

Этот способ основан на идее, предложенной в сообщении 16 обсуждения. Вместо того чтобы пытаться связать несовпадающие строки на уровне СУБД с помощью тяжелых соединений, мы переносим логику сопоставления на сторону встроенного языка 1С. Этот подход работает быстрее всего, поскольку СУБД выполняет простую выборку без ресурсоемких операций сопоставления.

Разберем алгоритм по шагам:

  1. Мы выбираем товары акционных документов и товары основных документов в один плоский список с помощью оператора ОБЪЕДИНИТЬ ВСЕ.
  2. В запросе мы создаем синтетическое поле ТипДокумента (значение "АКЦИЯ" или "ОСНОВНОЙ") для разделения строк при обработке.
  3. Упорядочиваем результат по уникальному ключу связи документов, типу документа и физическому номеру строки.
  4. В коде обхода выборки мы накапливаем строки по каждому документу в отдельные массивы (или таблицы значений в памяти) и затем выводим их параллельно в табличный документ, используя методы Вывести() для левой части и Присоединить() для правой.

Рассмотрим подробнее пример запроса для этого метода:


ВЫБРАТЬ
    РегистрСвязи.АкционныйДокумент КАК КлючСвязи,
    "АКЦИЯ" КАК ТипДокумента,
    АкционныеТовары.НомерСтроки КАК НомерСтроки,
    АкционныеТовары.Номенклатура КАК Номенклатура,
    АкционныеТовары.Количество КАК Количество
ИЗ
    РегистрСведений.впПривязкаАкционныхДокументов КАК РегистрСвязи
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг.Товары КАК АкционныеТовары
        ПО РегистрСвязи.АкционныйДокумент = АкционныеТовары.Ссылка

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
    РегистрСвязи.АкционныйДокумент КАК КлючСвязи,
    "ОСНОВНОЙ" КАК ТипДокумента,
    ОсновныеТовары.НомерСтроки КАК НомерСтроки,
    ОсновныеТовары.Номенклатура КАК Номенклатура,
    ОсновныеТовары.Количество КАК Количество
ИЗ
    РегистрСведений.впПривязкаАкционныхДокументов КАК РегистрСвязи
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг.Товары КАК ОсновныеТовары
        ПО РегистрСвязи.ОсновнойДокумент = ОсновныеТовары.Ссылка

УПОРЯДОЧИТЬ ПО
    КлючСвязи,
    ТипДокумента,
    НомерСтроки

Теперь проанализируем код на встроенном языке, который будет обрабатывать этот запрос и выводить данные в отчет. Нам понадобятся области макета: область шапки строки акции (левая часть) и область шапки строки основного документа (правая часть).


// Выполняем запрос
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();

ТабДок = Новый ТабличныйДокумент;
Макет = ПолучитьМакет("МакетОтчета");

ОбластьСтрокиАкция = Макет.ПолучитьОбласть("СтрокаАкция");
ОбластьСтрокиОсновной = Макет.ПолучитьОбласть("СтрокаОсновной");
ОбластьПустаяСтрокаАкция = Макет.ПолучитьОбласть("ПустаяСтрокаАкция");
ОбластьПустаяСтрокаОсновной = Макет.ПолучитьОбласть("ПустаяСтрокаОсновной");

// Структуры для накопления данных по текущему блоку документов
ТекущийКлюч = Неопределено;
МассивАкции = Новый Массив;
МассивОсновных = Новый Массив;

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

// Не забываем вывести последний блок данных
Если ТекущийКлюч <> Неопределено Тогда
    ВывестиПараллельныеСтроки(ТабДок, МассивАкции, МассивОсновных, ОбластьСтрокиАкция, ОбластьСтрокиОсновной, ОбластьПустаяСтрокаАкция, ОбластьПустаяСтрокаОсновной);
EndIf;

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


Процедура ВывестиПараллельныеСтроки(ТабДок, МассивАкции, МассивОсновных, ОбластьАкция, ОбластьОсновной, ПустаяАкция, ПустаяОсновной)
    
    КоличествоАкции = МассивАкции.Количество();
    КоличествоОсновных = МассивОсновных.Количество();
    МаксСтрок = Макс(КоличествоАкции, КоличествоОсновных);
    
    Для Индекс = 0 По МаксСтрок - 1 Цикл
        
        // 1. Выводим левую часть (Акция)
        Если Индекс < КоличествоАкции Тогда
            ТекущиеДанные = МассивАкции[Индекс];
            ОбластьАкция.Параметры.НоменклатураАкция = ТекущиеДанные.Номенклатура;
            ОбластьАкция.Параметры.КоличествоАкция = ТекущиеДанные.Количество;
            ТабДок.Вывести(ОбластьАкция);
        Иначе
            // Если акционных строк меньше, выводим пустую заглушку для сохранения структуры таблицы
            ТабДок.Вывести(ПустаяАкция);
        КонецЕсли;
        
        // 2. Присоединяем правую часть (Основной документ)
        Если Индекс < КоличествоОсновных Тогда
            ТекущиеДанные = МассивОсновных[Индекс];
            ОбластьОсновной.Параметры.НоменклатураОсновной = ТекущиеДанные.Номенклатура;
            ОбластьОсновной.Параметры.КоличествоОсновной = ТекущиеДанные.Количество;
            ТабДок.Присоединить(ОбластьОсновной);
        Иначе
            // Пустая заглушка для правой части
            ТабДок.Присоединить(ПустаяОсновной);
        КонецЕсли;
        
    КонецЦикла;
    
КонецПроцедуры

Этот метод гарантирует абсолютную точность вывода информации. Строки никогда не затворятся, а отчет будет выглядеть аккуратно, даже если в левой части 2 строки, а в правой — 10.

---

Вариант 2. Генерация синтетического номера строки в запросе (Внутригрупповая нумерация)

Если вам обязательно нужно получить готовый результат непосредственно из SQL-запроса (например, для передачи данных во внешние системы или для вывода через стандартный универсальный отчет), программная сборка не подойдет. В этом случае нам необходимо решить задачу внутригрупповой нумерации строк средствами СУБД.

В классическом SQL для этого используется оконная функция ROW_NUMBER() OVER (PARTITION BY Document ORDER BY LineNumber). В платформе 1С стандартных оконных функций нет (функция АВТОНОМЕРЗАПИСИ() нумерует строки глобально, без сброса счетчика для каждого документа). Поэтому мы применим классический алгоритм самосоединения временных таблиц.

Разберем этот метод подробно по шагам:

  1. Помещаем строки акционных документов во временную таблицу ВТ_Акция.
  2. Помещаем строки основных документов во временную таблицу ВТ_Основной.
  3. Выполняем соединение таблиц самих с собой по условию "Ссылка равна, а физический номер строки в первой таблице >= физическому номеру во второй". Количество совпадений при группировке даст нам непрерывный синтетический порядковый номер строки (начиная с 1) в разрезе каждого документа.
  4. Соединяем пронумерованные временные таблицы через ПОЛНОЕ СОЕДИНЕНИЕ по полям КлючСвязи и новому СинтетическийНомер.

Рассмотрим пример реализации такого пакета запросов:


// ШАГ 1. Подготовка и индексация данных по акциям
ВЫБРАТЬ
    РегистрСвязи.АкционныйДокумент КАК КлючСвязи,
    АкционныеТовары.Номенклатура КАК Номенклатура,
    АкционныеТовары.Количество КАК Количество,
    АкционныеТовары.НомерСтроки КАК НомерСтрокиФизический
ПОМЕСТИТЬ ВТ_Акция_Исходная
ИЗ
    РегистрСведений.впПривязкаАкционныхДокументов КАК РегистрСвязи
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг.Товары КАК АкционныеТовары
        ПО РегистрСвязи.АкционныйДокумент = АкционныеТовары.Ссылка;

ИКС ИНДЕКСИРОВАТЬ ВТ_Акция_Исходная ПО КлючСвязи, НомерСтрокиФизический;

////////////////////////////////////////////////////////////////////////////////
// ШАГ 2. Генерация синтетического номера строки для акций
ВЫБРАТЬ
    Т1.КлючСвязи КАК КлючСвязи,
    Т1.Номенклатура КАК Номенклатура,
    Т1.Количество КАК Количество,
    КОЛИЧЕСТВО(Т2.НомерСтрокиФизический) КАК СинтетическийНомер
ПОМЕСТИТЬ ВТ_Акция_Пронумерованная
ИЗ
    ВТ_Акция_Исходная КАК Т1
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Акция_Исходная КАК Т2
        ПО Т1.КлючСвязи = Т2.КлючСвязи
        И Т1.НомерСтрокиФизический >= Т2.НомерСтрокиФизический
СГРУППИРОВАТЬ ПО
    Т1.КлючСвязи,
    Т1.Номенклатура,
    Т1.Количество;

ИКС ИНДЕКСИРОВАТЬ ВТ_Акция_Пронумерованная ПО КлючСвязи, СинтетическийНомер;

////////////////////////////////////////////////////////////////////////////////
// ШАГ 3. Подготовка и индексация данных по основным документам
ВЫБРАТЬ
    РегистрСвязи.АкционныйДокумент КАК КлючСвязи,
    ОсновныеТовары.Номенклатура КАК Номенклатура,
    ОсновныеТовары.Количество КАК Количество,
    ОсновныеТовары.НомерСтроки КАК НомерСтрокиФизический
ПОМЕСТИТЬ ВТ_Основной_Исходная
ИЗ
    РегистрСведений.впПривязкаАкционныхДокументов КАК РегистрСвязи
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг.Товары КАК ОсновныеТовары
        ПО РегистрСвязи.ОсновнойДокумент = ОсновныеТовары.Ссылка;

ИКС ИНДЕКСИРОВАТЬ ВТ_Основной_Исходная ПО КлючСвязи, НомерСтрокиФизический;

////////////////////////////////////////////////////////////////////////////////
// ШАГ 4. Генерация синтетического номера строки для основных документов
ВЫБРАТЬ
    Т1.КлючСвязи КАК КлючСвязи,
    Т1.Номенклатура КАК Номенклатура,
    Т1.Количество КАК Количество,
    КОЛИЧЕСТВО(Т2.НомерСтрокиФизический) КАК СинтетическийНомер
ПОМЕСТИТЬ ВТ_Основной_Пронумерованная
ИЗ
    ВТ_Основной_Исходная КАК Т1
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Основной_Исходная КАК Т2
        ПО Т1.КлючСвязи = Т2.КлючСвязи
        И Т1.НомерСтрокиФизический >= Т2.НомерСтрокиФизический
СГРУППИРОВАТЬ ПО
    Т1.КлючСвязи,
    Т1.Номенклатура,
    Т1.Количество;

ИКС ИНДЕКСИРОВАТЬ ВТ_Основной_Пронумерованная ПО КлючСвязи, СинтетическийНомер;

////////////////////////////////////////////////////////////////////////////////
// ШАГ 5. Итоговое полное соединение двух пронумерованных таблиц
ВЫБРАТЬ
    ЕСТЬNULL(ТаблицаАкции.КлючСвязи, ТаблицаОсновные.КлючСвязи) КАК ДокументСвязи,
    ЕСТЬNULL(ТаблицаАкции.СинтетическийНомер, ТаблицаОсновные.СинтетическийНомер) КАК СтрокаПоПорядку,
    ТаблицаАкции.Номенклатура КАК АкцияНоменклатура,
    ТаблицаАкции.Количество КАК АкцияКоличество,
    ТаблицаОсновные.Номенклатура КАК ОсновнойНоменклатура,
    ТаблицаОсновные.Количество КАК ОсновнойКоличество
ИЗ
    ВТ_Акция_Пронумерованная КАК ТаблицаАкции
    ПОЛНОЕ СОЕДИНЕНИЕ ВТ_Основной_Пронумерованная КАК ТаблицаОсновные
        ПО ТаблицаАкции.КлючСвязи = ТаблицаОсновные.КлючСвязи
        И ТаблицаАкции.СинтетическийНомер = ТаблицаОсновные.СинтетическийНомер
УПОРЯДОЧИТЬ ПО
    ДокументСвязи,
    СтрокаПоПорядку

Важное примечание по оптимизации: Обратите внимание на использование оператора ИКС ИНДЕКСИРОВАТЬ в запросе. Применение самосоединений для генерации номеров строк создает серьезную нагрузку на СУБД (сложность алгоритма O(N^2) в рамках одного документа). Без создания индексов по полям связи и физическим номерам строк на больших базах данных этот запрос может выполняться крайне медленно. Индексация позволяет СУБД использовать быстрые алгоритмы поиска и значительно снижает нагрузку на дисковую подсистему сервера.

---

Вариант 3. Использование связей наборов данных в СКД

Система компоновки данных (СКД) предоставляет нам встроенные механизмы сопоставления данных, которые работают очень эффективно. Мы можем полностью отказаться от сложных запросов со слияниями и переложить задачу сопоставления на саму СКД.

Для реализации этого метода выполним следующие шаги в конструкторе схемы компоновки данных:

  1. Создаем Набор данных - Запрос 1 ("НаборДанныхАкция"). Этот запрос выбирает данные только по акционным документам и их табличным частям. Обязательно выбираем поле связи АкционныйДокумент.
  2. Создаем Набор данных - Запрос 2 ("НаборДанныхОсновной"). Этот запрос выбирает данные по документам-основаниям. Поле связи также должно присутствовать в выборке.
  3. Настраиваем вкладку "Связи наборов данных".
    • Источник связи: НаборДанныхАкция.
    • Приемник связи: НаборДанныхОсновной.
    • Условие связи: АкционныйДокумент = АкционныйДокумент.
  4. Настраиваем структуру отчета в виде таблицы.
    • В строки таблицы добавляем группировку по полю АкционныйДокумент.
    • В колонки таблицы добавляем вычисляемое поле ТипИнформации (которое в схеме принимает значения "Акция" или "Основной").
    • В область "Детальные записи" или в ресурсы помещаем поля Номенклатура и Количество.

Платформа автоматически сгенерирует оптимальные временные таблицы на уровне СУБД, свяжет их по указанным ключам и выведет информацию параллельно в колонках отчета, избежав при этом дублирования и зависания системы.

---

Подведем итоги: какой метод выбрать?

Мы подробно проанализировали три кардинально отличающихся подхода к решению задачи параллельного вывода связанных данных в 1С. Чтобы вам было проще сделать выбор для вашего проекта, сопоставим их особенности:

← На главную