Как вывести две таблицы параллельно (рядом) в отчете СКД 1С без умножения строк?

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

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

Рассмотрим типовую ситуацию: нам необходимо показать список назначенных пациенту медицинских услуг и список фактически выполненных услуг. При этом назначенные услуги могут быть представлены в базе данных как текстовые строки (например, в документе назначения), а выполненные – как элементы справочника «Номенклатура». Связь между ними может быть непрямой или вовсе отсутствовать на уровне отдельных строк, но важен общий контекст (пациент). Мы хотим получить отчет, где для каждого пациента слева будут его назначения, а справа – выполнения, при этом строки должны идти по порядку, а если одной из сторон больше, то пустые значения должны заполняться.

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

Подход 1: Нумерация строк в запросе и полное соединение (SQL-подход)

Один из эффективных способов отобразить данные параллельно без умножения – это присвоить порядковый номер каждой строке внутри группы (например, для каждого пациента) в обеих таблицах, а затем выполнить полное соединение по этой группе и номеру. Этот метод позволяет выровнять строки и вывести их по порядку.

Разберем по шагам, как это реализовать в запросе СКД:

  1. Создаем временные таблицы для Назначений и Выполнений. Представим, что у нас есть две исходные таблицы – ВТ_Назначения и ВТ_Выполнения.
  2. Присваиваем каждой записи общий порядковый номер. Используем функцию АВТОНОМЕРЗАПИСИ() для получения уникального номера для каждой записи в каждой временной таблице. Это нужно для следующего шага – нумерации внутри группы.
  3. Вычисляем номер записи внутри группы (пациента). Это ключевой момент. Для этого мы выполняем левое соединение таблицы самой с собой. Соединение происходит по полю «Пациент» (т.е. внутри одной группы) и условию, что общий номер текущей записи больше или равен общему номеру соединяемой записи. Затем мы используем агрегатную функцию СУММА(1) для подсчета количества строк, удовлетворяющих условию, что фактически даст нам порядковый номер внутри группы.
  4. Выполняем полное соединение нумерованных таблиц. После того как обе таблицы (назначения и выполнения) будут иметь порядковый номер внутри группы «Пациент», мы можем выполнить ПОЛНОЕ СОЕДИНЕНИЕ по полям «Пациент» и «Номер». Это гарантирует, что строки будут выведены параллельно, и если у одной из сторон больше записей, то недостающие будут заполнены NULL, которые мы затем заменим на «---» или другие читаемые значения с помощью ЕСТЬNULL().

Посмотрим на пример запроса, который демонстрирует эту логику:


ВЫБРАТЬ
    "Пациент1" КАК Пациент,
    "Назначение24" КАК Назначение
ПОМЕСТИТЬ ВТ_Назначения
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
    "Пациент1",
    "Назначение42"
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
    "Пациент2",
    "Назначение65"
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
    "Пациент2",
    "Назначение87"
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
    "Пациент2",
    "Назначение91"
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ВТ_Назначения.Пациент,
    ВТ_Назначения.Назначение,
    АВТОНОМЕРЗАПИСИ() КАК ОбщийНомер
ПОМЕСТИТЬ ВТ_Назначения_ОбщаяНумерация
ИЗ
    ВТ_Назначения КАК ВТ_Назначения
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ВТ_Назначения_ОбщаяНумерация.Пациент,
    ВТ_Назначения_ОбщаяНумерация.Назначение,
    СУММА(1) КАК Номер
ПОМЕСТИТЬ ВТ_Назначения_НомерВнутриГруппы
ИЗ
    ВТ_Назначения_ОбщаяНумерация КАК ВТ_Назначения_ОбщаяНумерация
        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Назначения_ОбщаяНумерация КАК ВТ_Назначения_ОбщаяНумерация1
        ПО ВТ_Назначения_ОбщаяНумерация.Пациент = ВТ_Назначения_ОбщаяНумерация1.Пациент
            И ВТ_Назначения_ОбщаяНумерация.ОбщийНомер >= ВТ_Назначения_ОбщаяНумерация1.ОбщийНомер
СГРУППИРОВАТЬ ПО
    ВТ_Назначения_ОбщаяНумерация.Пациент,
    ВТ_Назначения_ОбщаяНумерация.Назначение
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    "Пациент1" КАК Пациент,
    "Выполнение67" КАК Выполнение
ПОМЕСТИТЬ ВТ_Выполнения
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
    "Пациент2",
    "Выполнение52"
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
    "Пациент2",
    "Выполнение84"
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
    "Пациент3",
    "Выполнение35"
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
    "Пациент3",
    "Выполнение01"
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ВТ_Выполнения.Пациент,
    ВТ_Выполнения.Выполнение,
    АВТОНОМЕРЗАПИСИ() КАК ОбщийНомер
ПОМЕСТИТЬ ВТ_Выполнения_ОбщаяНумерация
ИЗ
    ВТ_Выполнения КАК ВТ_Выполнения
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ВТ_Выполнения_ОбщаяНумерация.Пациент,
    ВТ_Выполнения_ОбщаяНумерация.Выполнение,
    СУММА(1) КАК Номер
ПОМЕСТИТЬ ВТ_Выполнения_НомерВнутриГруппы
ИЗ
    ВТ_Выполнения_ОбщаяНумерация КАК ВТ_Выполнения_ОбщаяНумерация
        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Выполнения_ОбщаяНумерация КАК ВТ_Выполнения_ОбщаяНумерация1
        ПО ВТ_Выполнения_ОбщаяНумерация.Пациент = ВТ_Выполнения_ОбщаяНумерация1.Пациент
            И ВТ_Выполнения_ОбщаяНумерация.ОбщийНомер >= ВТ_Выполнения_ОбщаяНумерация1.ОбщийНомер
СГРУППИРОВАТЬ ПО
    ВТ_Выполнения_ОбщаяНумерация.Пациент,
    ВТ_Выполнения_ОбщаяНумерация.Выполнение
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ЕСТЬNULL(ВТ_Назначения_НомерВнутриГруппы.Пациент, ВТ_Выполнения_НомерВнутриГруппы.Пациент) КАК Пациент,
    ЕСТЬNULL(ВТ_Назначения_НомерВнутриГруппы.Номер, ВТ_Выполнения_НомерВнутриГруппы.Номер) КАК Номер,
    ЕСТЬNULL(ВТ_Назначения_НомерВнутриГруппы.Назначение, "---") КАК Назначение,
    ЕСТЬNULL(ВТ_Выполнения_НомерВнутриГруппы.Выполнение, "---") КАК Выполнение
ИЗ
    ВТ_Назначения_НомерВнутриГруппы КАК ВТ_Назначения_НомерВнутриГруппы
        ПОЛНОЕ СОЕДИНЕНИЕ ВТ_Выполнения_НомерВнутриГруппы КАК ВТ_Выполнения_НомерВнутриГруппы
        ПО ВТ_Назначения_НомерВнутриГруппы.Пациент = ВТ_Выполнения_НомерВнутриГруппы.Пациент
            И ВТ_Назначения_НомерВнутриГруппы.Номер = ВТ_Выполнения_НомерВнутриГруппы.Номер
УПОРЯДОЧИТЬ ПО
    Пациент,
    Номер

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

Подход 2: Использование оконных функций (для платформы 1С 8.3.12 и выше)

Для версий платформы 1С 8.3.12 и старше появилась возможность использовать оконные функции в запросах. Это значительно упрощает и ускоряет нумерацию строк внутри групп. Функция ROW_NUMBER() является идеальным инструментом для нашей задачи.

Как мы используем ROW_NUMBER():

  1. Нумеруем строки в каждой временной таблице. Вместо сложного самосоединения, мы можем использовать ROW_NUMBER() с ключевым словом ПОРЯДОК ПО для указания порядка нумерации и неявной группировки. Например, для нумерации назначений по пациентам:

ВЫБРАТЬ
    ВТ_Назначения.Пациент,
    ВТ_Назначения.Назначение,
    ROW_NUMBER() ПОРЯДОК ПО ВТ_Назначения.Пациент, ВТ_Назначения.Назначение КАК НомерВГруппе
ИЗ
    ВТ_Назначения КАК ВТ_Назначения

Здесь ПОРЯДОК ПО ВТ_Назначения.Пациент, ВТ_Назначения.Назначение означает, что записи будут нумероваться внутри каждой уникальной комбинации «Пациент», «Назначение» в указанном порядке. Если нам нужна нумерация только по пациенту, то следует использовать РАЗДЕЛИТЬ ПО ВТ_Назначения.Пациент ПОРЯДОК ПО ВТ_Назначения.Назначение для более явного указания группировки и порядка внутри нее.

  1. Выполняем полное соединение. После нумерации обеих таблиц (Назначений и Выполнений) с использованием ROW_NUMBER(), мы, как и в предыдущем подходе, выполняем ПОЛНОЕ СОЕДИНЕНИЕ по полям «Пациент» и полученному номеру внутри группы. Этот подход значительно производительнее для больших объемов данных.

Подход 3: Несколько наборов данных и пользовательский макет СКД

СКД предоставляет мощные инструменты для настройки вывода отчета через макеты. Мы можем использовать несколько наборов данных и настроить пользовательский макет для вывода информации параллельно.

Разберем этапы реализации:

  1. Создаем два отдельных набора данных. В схеме компоновки данных создаем два набора данных: один для «Назначений», другой для «Выполнений». Каждый набор данных будет получать свои данные из соответствующих источников.
  2. Настраиваем общую группировку. На уровне СКД создаем общую группировку, например, по «Пациенту». Это обеспечит, что данные для одного пациента будут выводиться вместе.
  3. Создаем пользовательский макет группировки или детальных записей. Для группировки «Пациент» или для детальных записей создаем свой макет. В этом макете мы можем расположить элементы как нам угодно.
  4. Размещаем две табличные области в макете. В пользовательском макете мы создаем две отдельные табличные области (или области-списки) рядом.
    • Первую область мы привязываем к полям из набора данных «Назначения».
    • Вторую область привязываем к полям из набора данных «Выполнения».
    При этом важно понимать, что СКД будет выводить эти области как независимые списки внутри каждой группировки. Если количество строк в списках разное, они будут выводиться «один под другим» в своей области, но сами области будут расположены рядом. Для строгого попарного сопоставления этот метод может потребовать дополнительной доработки (например, в самом запросе, как описано в первых двух подходах, чтобы каждая строка имела уникальный идентификатор для сопоставления). Однако, если задача – просто показать два списка рядом, это отличный вариант.
  5. Настраиваем поля и параметры. Мы можем настроить поля для каждой области, их заголовки, форматирование и так далее.

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

Подход 4: Программная обработка табличного документа в процедуре ПриКомпоновкеРезультата

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

Мы будем работать с событием ПриКомпоновкеРезультата схемы компоновки данных. В этом событии мы получаем доступ к уже сформированному, но еще не выведенному табличному документу.

Рассмотрим шаги:

  1. Отключаем стандартное формирование результата СКД. В процедуре ПриКомпоновкеРезультата устанавливаем параметр СтандартнаяОбработка = Ложь. Это предотвратит автоматический вывод отчета СКД.
  2. Выполняем два отдельных запроса. Внутри процедуры мы создаем и выполняем два отдельных запроса: один для получения данных о назначениях, другой – о выполнениях. Результаты получаем в таблицы значений (например, ТаблицаНазначений и ТаблицаВыполнений).
  3. Инициализируем табличный документ. Создаем новый табличный документ или используем ДокументРезультат, переданный в процедуру.
  4. Настраиваем макеты для строк. Определяем, как будут выглядеть заголовки и детальные строки отчета. Мы можем программно создать области для заголовков столбцов «Назначено» и «Выполнено».
  5. Заполняем табличный документ вручную.
    • Перебираем пациентов из объединенного списка или из одного из запросов.
    • Для каждого пациента получаем все его назначения и выполнения.
    • В цикле построчно заполняем табличный документ, размещая данные из ТаблицаНазначений в одной части строки и данные из ТаблицаВыполнений в другой.
    • При этом мы должны учитывать, что количество назначений и выполнений для одного пациента может быть разным. Мы можем использовать два указателя или индекса для каждой таблицы, чтобы синхронно выводить строки. Если одна из таблиц закончилась, продолжаем выводить данные из другой, заполняя недостающие ячейки пробелами или дефисами.
  6. Выводим сформированный документ. После заполнения всех данных, мы можем вывести ДокументРезультат.

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

Подход 5: Использование левого соединения, если есть логическая связь

Вернемся к обсуждению из форума. Иногда причина "умножения" строк или сложности в сопоставлении кроется в неполном понимании логической связи между данными. Если существует хотя бы косвенная связь между "назначением" и "выполнением" (например, по типу услуги, дате, документу-основанию или некому уникальному идентификатору, который можно сопоставить), то вместо полного соединения по номеру может быть достаточно левого или правого соединения.

Проанализируем ситуацию:

  1. Существует ли прямой или косвенный идентификатор? Если "назначение" в виде строки может быть сопоставлено с "номенклатурой" выполнения (например, через текстовое описание, код услуги или какую-то общую характеристику), то мы можем использовать это для соединения.
  2. Что хотим увидеть?
    • Если нам важно видеть все назначения и только те выполнения, которые им соответствуют, то мы используем Левое Соединение (Назначения ЛЕВОЕ СОЕДИНЕНИЕ Выполнения ПО <УсловиеСвязи>). Невыполненные назначения будут иметь NULL в колонках выполнения.
    • Если нам важно видеть все выполнения и только те назначения, которые им соответствуют, то это будет Правое Соединение.
    • Если нам нужны только связанные друг с другом записи, то это Внутреннее Соединение.

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

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

Выводы

Мы рассмотрели несколько методов вывода двух таблиц параллельно в отчетах СКД 1С, каждый из которых имеет свои преимущества и недостатки:

  1. Нумерация строк в запросе и полное соединение (SQL): Эффективен для выравнивания строк по порядку, но может быть медленным для больших объемов данных при использовании самосоединения.
  2. Оконные функции (ROW_NUMBER()): Наиболее производительный и элегантный способ нумерации строк внутри групп, если ваша платформа 1С поддерживает их (версия 8.3.12 и выше).
  3. Несколько наборов данных и пользовательский макет СКД: Удобен, если нам нужно просто отобразить два независимых списка рядом для общей сущности, без строгого попарного сопоставления.
  4. Программная обработка табличного документа (`ПриКомпоновкеРезультата`): Предоставляет максимальный контроль, но требует значительных усилий по кодированию и может быть менее производительным.
  5. Левое/Правое соединение: Если есть адекватная логическая связь между данными, это самый простой и прямолинейный подход.

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

← На главную