Приветствуем! При разработке отчетов и доработке конфигураций на платформе 1С:Предприятие 8 одна из частых задач — вывести список контрагентов и «прицепить» к каждому ровно одно контактное лицо. В конфигурациях, использующих современные архитектурные решения (таких как 1С:Управление нашей фирмой (УНФ), 1С:ERP или 1С:Комплексная автоматизация), связь между контрагентами и контактами реализована не через простое подчинение справочников, а по принципу «многие-ко-многим». Для этого применяется специальный регистр сведений.
В этой статье мы подробно разберем, почему стандартные простые соединения не работают, проанализируем архитектуру хранения данных в УНФ, выявим типичные ошибки разработчиков и составим максимально эффективный, оптимизированный пакетный запрос, который гарантированно вернет по одной записи на каждого контрагента без дублей и зависаний базы данных.
Давайте разберем, почему задача вызывает трудности. В классических старых конфигурациях контактные лица были жестко подчинены контрагенту (справочник КонтактныеЛица имел владельцем справочник Контрагенты). В УНФ разработчики 1С применили более гибкую схему:
Контакты (или КонтактныеЛица).СвязиКонтрагентКонтакт.Порядок (числовое поле сортировки) и признак деактивации СвязьНедействительна.Именно из-за структуры «многие-ко-многим» прямое левое соединение справочника контрагентов с регистром сведений приводит к «размножению» строк в результате запроса: если у контрагента привязано 5 контактов, в отчете мы получим 5 одинаковых строк контрагента.
Часто начинающие разработчики пытаются решить задачу «в лоб», используя конструкцию ПЕРВЫЕ 1 во вложенном запросе левого соединения. Давайте посмотрим на этот ошибочный пример:
ВЫБРАТЬ
Контрагенты.Ссылка КАК Ссылка,
СвязиКонтактов.Контакт КАК Контакт
ИЗ
Справочник.Контрагенты КАК Контрагенты
ЛЕВОЕ СОЕДИНЕНИЕ (
ВЫБРАТЬ ПЕРВЫЕ 1
СрезПоследних.Контакт КАК Контакт,
СрезПоследних.Контрагент КАК Контрагент
ИЗ
РегистрСведений.СвязиКонтрагентКонтакт.СрезПоследних КАК СрезПоследних
) КАК СвязиКонтактов
ПО Контрагенты.Ссылка = СвязиКонтактов.Контрагент
Почему этот запрос работает некорректно? Дело в том, что СУБД выполнит вложенный запрос автономно и выберет ровно одно контактное лицо на всю базу данных. Затем это единственное контактное лицо попытается соединиться со всеми контрагентами. В итоге мы получим заполненный контакт только у одного контрагента во всей таблице, а у остальных поле останется пустым.
Конструкция ПЕРВЫЕ работает глобально для всей выборки подзапроса, а не «в разрезе» группировки по контрагентам. Чтобы решить задачу правильно, нам необходимо применить механизм пакетирования запроса с агрегатными функциями.
Наиболее надежный, быстрый и соответствующий стандартам разработки 1С метод — это использование временных таблиц. Мы разобьем задачу на шаги, чтобы гарантировать максимальную скорость работы СУБД и полное отсутствие дублей.
Рассмотрим по шагам логику нашего будущего пакета запросов:
Порядок. Это позволит нам понять, какой контакт был добавлен первым или назначен самым приоритетным.МИНИМУМ(Контакт). Это ключевой шаг для стабильности отчета!Контрагенты с подготовленной чистой временной таблицей связей.Давайте реализуем эту схему в коде:
// ШАГ 1. Получаем срез последних и отсекаем недействующие связи
ВЫБРАТЬ
Связи.Контрагент КАК Контрагент,
Связи.Контакт КАК Контакт,
Связи.Порядок КАК Порядок
ПОМЕСТИТЬ ВТ_СвязиАктивные
ИЗ
РегистрСведений.СвязиКонтрагентКонтакт.СрезПоследних КАК Связи
ГДЕ
НЕ Связи.СвязьНедействительна
;
////////////////////////////////////////////////////////////////////////////////
// ШАГ 2. Находим минимальный порядок связи для каждого контрагента
ВЫБРАТЬ
АктивныеСвязи.Контрагент КАК Контрагент,
МИНИМУМ(АктивныеСвязи.Порядок) КАК МинимальныйПорядок
ПОМЕСТИТЬ ВТ_МинимальныйПорядок
ИЗ
ВТ_СвязиАктивные КАК АктивныеСвязи
СГРУППИРОВАТЬ ПО
АктивныеСвязи.Контрагент
;
////////////////////////////////////////////////////////////////////////////////
// ШАГ 3. Получаем строго одну связь на контрагента (исключаем дубли по МИНИМУМ(Контакт))
ВЫБРАТЬ
АктивныеСвязи.Контрагент КАК Контрагент,
МИНИМУМ(АктивныеСвязи.Контакт) КАК Контакт
ПОМЕСТИТЬ ВТ_ИтоговыеСвязи
ИЗ
ВТ_СвязиАктивные КАК АктивныеСвязи
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_МинимальныйПорядок КАК МинПорядок
ПО АктивныеСвязи.Контрагент = МинПорядок.Контрагент
И АктивныеСвязи.Порядок = МинПорядок.МинимальныйПорядок
СГРУППИРОВАТЬ ПО
АктивныеСвязи.Контрагент
;
////////////////////////////////////////////////////////////////////////////////
// ШАГ 4. Соединяем справочник Контрагенты с уникальными контактами
ВЫБРАТЬ
Контрагенты.Ссылка КАК Контрагент,
Контрагенты.Код КАК Код,
Контрагенты.Наименование КАК Наименование,
ЕСТЬNULL(ИтоговыеСвязи.Контакт, ЗНАЧЕНИЕ(Справочник.Контакты.ПустаяСсылка)) КАК ПервоеКонтактноеЛицо
ИЗ
Справочник.Контрагенты КАК Контрагенты
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ИтоговыеСвязи КАК ИтоговыеСвязи
ПО Контрагенты.Ссылка = ИтоговыеСвязи.Контрагент
ГДЕ
НЕ Контрагенты.ЭтоГруппа
Проанализируем преимущества этого подхода. Использование временных таблиц позволяет оптимизатору СУБД (MS SQL Server, PostgreSQL) построить оптимальный план запроса. Индексы будут использоваться максимально эффективно, а исключение дублей на третьем шаге гарантирует, что количество строк на выходе будет в точности соответствовать количеству контрагентов в базе данных.
Иногда на форумах или обучающих ресурсах можно встретить предложение использовать так называемый коррелированный запрос. Смысл его в том, что вложенный запрос обращается к полям внешнего запроса.
Давайте посмотрим, как выглядит такая конструкция в синтаксисе 1С:
ВЫБРАТЬ
Контрагенты.Ссылка КАК Контрагент,
(ВЫБРАТЬ ПЕРВЫЕ 1
Связи.Контакт
ИЗ
РегистрСведений.СвязиКонтрагентКонтакт.СрезПоследних КАК Связи
ГДЕ
Связи.Контрагент = Контрагенты.Ссылка
И НЕ Связи.СвязьНедействительна
УПОРЯДОЧИТЬ ПО
Связи.Порядок Возр) КАК КонтактноеЛицо
ИЗ
Справочник.Контрагенты КАК Контрагенты
Важное предостережение по производительности! Несмотря на внешнюю компактность кода, этот подход обладает существенным минусом — крайне низкой скоростью работы на больших базах данных. При выполнении такого запроса СУДБ вынуждена выполнять вложенную выборку построчно (использовать алгоритм Nested Loops). Если в вашей базе данных более 10 000 контрагентов, построение такого отчета может вызвать перегрузку процессора сервера и завершиться ошибкой по таймауту выполнения.
Кроме того, в платформе 1С отсутствуют аналитические оконные функции (такие как ROW_NUMBER() OVER в классическом SQL), поэтому эмуляция выбора первой строки через корреляционные подзапросы в 1С всегда является крайней мерой, не рекомендуемой для высоконагруженных систем.
Не забываем также о бизнес-логике самой конфигурации. Перед тем как писать сложные запросы к регистрам связей, проанализируем структуру справочника Контрагенты. В актуальных версиях УНФ разработчики предусмотрели стандартный реквизит ОсновноеКонтактноеЛицо непосредственно в самом справочнике.
Если в вашей базе этот реквизит заполняется пользователями при интерактивной работе, то запрос упрощается до элементарного:
ВЫБРАТЬ
Контрагенты.Ссылка КАК Контрагент,
Контрагенты.ОсновноеКонтактноеЛицо КАК ПервоеКонтактноеЛицо
ИЗ
Справочник.Контрагенты КАК Контрагенты
ГДЕ
НЕ Контрагенты.ЭтоГруппа
Этот способ — самый быстродействующий из всех возможных, поскольку данные извлекаются напрямую из одной таблицы справочника без каких-либо соединений. Рекомендуем использовать его, если бизнес-процессы компании обязывают менеджеров явно назначать основного контакта в карточке контрагента.
Если вы решаете задачу программно (например, в модуле объекта при проведении документа или заполнении реквизитов на форме), писать ручные запросы к регистрам связей вовсе не обязательно. Конфигурация УНФ содержит механизмы Библиотеки стандартных подсистем (БСП) и специализированные модули CRM.
Мы можем получить основное контактное лицо с помощью одной строки встроенного языка, вызвав экспортную функцию общего модуля:
// Получаем ссылку на основное контактное лицо контрагента
КонтактноеЛицо = ПартнерыИКонтрагенты.ПолучитьКонтактноеЛицоПартнераКонтрагентаПоУмолчанию(ВыбранныйКонтрагент);
Использование функций БСП гарантирует работоспособность вашего программного кода даже в случае последующих глобальных обновлений конфигурации от фирмы 1С, если структура хранения связей в будущих релизах претерпит изменения.
Для решения задачи по выводу контрагента с его первым контактным лицом мы имеем три основных пути:
Применяйте пакетные запросы с предварительной агрегацией данных, избегайте коррелированных подзапросов на больших таблицах, и ваши отчеты всегда будут работать быстро и корректно!