Разработка запросов для работы с периодическими регистрами сведений — одна из наиболее частых задач в мире 1С. Обычно мы сталкиваемся со стандартными ситуациями, когда нам нужно получить срез последних данных по определенным измерениям. Например, узнать актуальную цену (ресурс) для конкретного товара (измерение). С такой задачей легко справляется стандартная виртуальная таблица СрезПоследних.
Однако на практике встречаются гораздо более сложные и нетривиальные структуры метаданных. В этой статье мы подробно разберем, как решить задачу, когда привычная логика среза последних переворачивается «с ног на голову»: нам необходимо получить последнее актуальное измерение по значению ресурса. А именно — определить последний актуальный автомобиль по каждому контрагенту, при условии, что автомобиль в периодическом регистре является измерением, а контрагент — ресурсом составного типа.
Давайте подробно проанализируем структуру нашего регистра сведений. Он является периодическим и имеет следующие поля:
Автомобиль (тип СправочникСсылка.Автомобили)ВидЗначения (тип ПеречислениеСсылка.ДополнительнаяИнформацияАвтомобилей)Значение (составной тип, в котором нас интересует СправочникСсылка.Контрагенты)Бизнес-требование звучит следующим образом: необходимо получить последний актуальный автомобиль по каждому контрагенту. Под «актуальным» понимается автомобиль, которым контрагент владеет в данный момент (то есть этот контрагент является последним зарегистрированным хозяином машины). Если у контрагента сейчас несколько машин, мы должны выбрать ту, которая была приобретена последней. Если контрагент когда-то владел машиной, но потом продал ее другому человеку, этот автомобиль больше не должен отображаться за старым владельцем.
Попытка решить задачу «в лоб» через соединение справочника контрагентов с виртуальной таблицей СрезПоследних по условию АвтомобилиСрезПоследних.Значение = Контрагенты.Ссылка приведет к грубым ошибкам в данных:
Чтобы решить эту задачу правильно и не сломать бизнес-логику информационной системы, нам придется реализовать двухэтапный алгоритм получения среза последних «руками» в запросе.
Для получения корректных данных нам необходимо выстроить следующую логическую цепочку:
Автомобиль, мы можем использовать виртуальную таблицу СрезПоследних. Это позволит нам отсечь всю историю продаж и получить только актуальных владельцев машин на текущий момент.Рассмотрим, как будет выглядеть оптимальный и быстрый запрос в 1С, соответствующий всем стандартам разработки.
Ниже представлен готовый текст запроса, который решает поставленную задачу с учетом оптимизации производительности СУБД:
ВЫБРАТЬ
АвтомобилиСрез.Период КАК Период,
АвтомобилиСрез.Автомобиль КАК Автомобиль,
ВЫРАЗИТЬ(АвтомобилиСрез.Значение КАК Справочник.Контрагенты) КАК Контрагент
ПОМЕСТИТЬ ВТ_ТекущиеВладельцы
ИЗ
РегистрСведений.Автомобили.СрезПоследних(&Период, ВидЗначения = ЗНАЧЕНИЕ(Перечисление.ДополнительнаяИнформацияАвтомобилей.Хозяин)) КАК АвтомобилиСрез
ГДЕ
АвтомобилиСрез.Значение ССЫЛКА Справочник.Контрагенты
ИНДЕКСИРОВАТЬ ПО
Контрагент,
Период
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ_МаксДаты.Контрагент КАК Контрагент,
МАКСИМУМ(ТекущиеВладельцы.Автомобиль) КАК Автомобиль
ИЗ
(ВЫБРАТЬ
ВТ_ТекущиеВладельцы.Контрагент КАК Контрагент,
МАКСИМУМ(ВТ_ТекущиеВладельцы.Период) КАК Период
ИЗ
ВТ_ТекущиеВладельцы КАК ВТ_ТекущиеВладельцы
СГРУППИРОВАТЬ ПО
ВТ_ТекущиеВладельцы.Контрагент) КАК ВТ_МаксДаты
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ТекущиеВладельцы КАК ТекущиеВладельцы
ПО ВТ_МаксДаты.Контрагент = ТекущиеВладельцы.Контрагент
И ВТ_МаксДаты.Период = ТекущиеВладельцы.Период
СГРУППИРОВАТЬ ПО
ВТ_МаксДаты.Контрагент
Проанализируем детально каждую секцию нашего пакетного запроса, чтобы понять, какие механизмы платформы 1С здесь задействованы и почему этот запрос будет работать быстро на больших базах данных.
В первом запросе пакета мы обращаемся к виртуальной таблице РегистрСведений.Автомобили.СрезПоследних. Это ключевой момент оптимизации. Мы передаем в параметры виртуальной таблицы условие отбора по виду значения: ВидЗначения = ЗНАЧЕНИЕ(Перечисление.ДополнительнаяИнформацияАвтомобилей.Хозяин). Это позволяет СУБД отсеять лишние записи (например, пробег или госномер, если они хранятся в этом же регистре) еще на этапе обращения к индексам виртуальной таблицы, не считывая лишние строки с диска.
Для обеспечения высокой скорости выполнения мы обязательно используем конструкцию ВЫРАЗИТЬ(АвтомобилиСрез.Значение КАК Справочник.Контрагенты). Поле Значение имеет составной тип. Если не выполнить типизацию, то SQL-сервер при формировании плана запроса выполнит неявные левые соединения (LEFT JOIN) со всеми таблицами, типы которых могут храниться в этом ресурсе. Использование ВЫРАЗИТЬ жестко указывает СУБД на то, что мы работаем только со справочником контрагентов, существенно упрощая план выполнения запроса.
Результат мы помещаем во временную таблицу ВТ_ТекущиеВладельцы и обязательно индексируем ее по полям Контрагент и Период. Индексация крайне важна, так как во втором запросе пакета мы будем производить соединение по этим полям.
Во втором запросе пакета мы решаем две задачи: находим самый свежий период владения для каждого контрагента и сопоставляем его с конкретной машиной. Для этого мы сначала делаем вложенный запрос к нашей временной таблице, группируя записи по контрагенту и вычисляя максимальный период: МАКСИМУМ(ВТ_ТекущиеВладельцы.Период).
Затем этот вложенный запрос мы соединяем с этой же временной таблицей ВТ_ТекущиеВладельцы по равенству контрагента и периода. На выходе мы получаем конкретную машину.
Важная деталь: обратите внимание на внешнюю группировку во втором запросе и поле МАКСИМУМ(ТекущиеВладельцы.Automobil). Зачем это нужно? Представим ситуацию, когда один и тот же контрагент умудрился приобрести два автомобиля абсолютно в одно и то же время (в пределах одной секунды или в пределах одного дня, если периодичность регистра настроена на день). В этом случае СУБД вернет две строки с одинаковым максимальным периодом, что приведет к дублированию строк в итоговом отчете. Использование агрегирующей функции МАКСИМУМ гарантирует, что даже в случае такой временной коллизии система вернет ровно один автомобиль (старшую ссылку по уникальному идентификатору GUID).
Решение, которое мы разобрали выше, является максимально эффективным в рамках текущей структуры метаданных. Однако сама структура регистра сведений в данном случае представляет собой известный антипаттерн проектирования, часто называемый EAV (Entity-Attribute-Value / Сущность-Атрибут-Значение).
В высоконагруженных системах использование универсальных регистров сведений, где в измерениях хранится тип информации (вид свойства), а в ресурсах — разнородные данные в составных типах, приводит к следующим проблемам:
Значение является ресурсом, а не измерением.Если в вашей системе часто возникает задача поиска автомобилей по контрагентам или получения списков актуального автопарка клиентов, настоятельно рекомендуется провести рефакторинг метаданных и создать специализированный периодический регистр сведений (например, ВладельцыАвтомобилей) со следующей структурой:
Контрагент (тип СправочникСсылка.Контрагенты)Автомобиль (тип СправочникСсылка.Автомобили)При такой структуре решение нашей задачи свелось бы к простейшему обращению к виртуальной таблице среза последних в одну строку кода:
ВЫБРАТЬ
ВладельцыАвтомобилейСрезПоследних.Контрагент КАК Контрагент,
ВладельцыАвтомобилейСрезПоследних.Автомобиль КАК Автомобиль
ИЗ
РегистрСведений.ВладельцыАвтомобилей.СрезПоследних(&Период, ) КАК ВладельцыАвтомобилейСрезПоследних
Такой запрос будет выполняться мгновенно на любом объеме данных, так как платформа 1С использует встроенные индексы СУБД, созданные по измерениям регистра сведений.