При разработке интерфейсов на платформе 1С:Предприятие 8.3 программисты часто сталкиваются с необходимостью вывода комплексных данных в списочные формы. Типичный сценарий: в справочнике ведется история каких-либо изменений (например, тарифных ставок, цен, статусов) прямо внутри табличной части. Задача состоит в том, чтобы вывести в форму списка сам справочник, но при этом показать только самую последнюю (актуальную) запись из его табличной части.
На первый взгляд задача кажется простой, но при реализации запроса для динамического списка разработчики регулярно совершают две серьезные ошибки: либо из списка пропадают элементы, у которых табличная часть вообще не заполнена, либо элементы с несколькими записями в табличной части начинают дублироваться. Давайте подробно разберем, почему это происходит, проанализируем ошибки в решениях и построим идеальный, оптимизированный запрос для динамического списка.
Для понимания сути проблемы рассмотрим стандартные механизмы связывания таблиц в СУБД:
Попытки использовать ключевое слово РАЗЛИЧНЫЕ в данном случае не помогают, так как значения тарифов и дат в строках различаются, и СУБД справедливо считает эти строки уникальными.
В ходе обсуждения автор темы предложил вариант запроса, который якобы решил его проблему. Давайте внимательно посмотрим на этот код:
ВЫБРАТЬ
ВТСсылки.Ссылка КАК Ссылка,
ВТСсылки.Ссылка.Код,
ВТСсылки.Ссылка.Наименование,
ВТСсылки.Ссылка.СтанцияНазначения,
ВТСсылки.Ссылка.СтанцияОтправления,
ВТСсылки.Ссылка.ВидПодвижногоСостава,
ВТСсылки.Ссылка.Подрядчик,
ВТТЧ.Дата,
ВТТЧ.ТарифнаяСтавка,
ВТТЧ.ВключенПорожнийВозврат
ИЗ
(ВЫБРАТЬ РАЗЛИЧНЫЕ
СобиЖДТарифныеСтавки.Ссылка КАК Ссылка
ИЗ
Справочник.СобиЖДТарифныеСтавки КАК СобиЖДТарифныеСтавки) КАК ВТСсылки
ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ
СобиЖДТарифныеСтавкиТабличнаяЧасть1.Ссылка КАК Ссылка,
МАКСИМУМ(СобиЖДТарифныеСтавкиТабличнаяЧасть1.Дата) КАК Дата
ИЗ
Справочник.СобиЖДТарифныеСтавки.ТабличнаяЧасть1 КАК СобиЖДТарифныеСтавкиТабличнаяЧасть1
СГРУППИРОВАТЬ ПО
СобиЖДТарифныеСтавкиТабличнаяЧасть1.Ссылка) КАК ВТДаты
ПО (ВТСсылки.Ссылка = ВТДаты.Ссылка)
ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ
СобиЖДТарифныеСтавкиТабличнаяЧасть1.Ссылка КАК Ссылка,
СобиЖДТарифныеСтавкиТабличнаяЧасть1.Дата КАК Дата,
СобиЖДТарифныеСтавкиТабличнаяЧасть1.ТарифнаяСтавка КАК ТарифнаяСтавка,
СобиЖДТарифныеСтавкиТабличнаяЧасть1.ВключенПорожнийВозврат КАК ВключенПорожнийВозврат
ИЗ
Справочник.СобиЖДТарифныеСтавки.ТабличнаяЧасть1 КАК СобиЖДТарифныеСтавкиТабличнаяЧасть1) КАК ВТТЧ
ПО (ВТСсылки.Ссылка = ВТТЧ.Ссылка)
Несмотря на то, что автор посчитал задачу решенной, данный запрос содержит две критические ошибки (логическую и архитектурную), которые обязательно проявят себя в реальной базе данных ERP-системы.
Ошибка первая: Логическая (Дублирование строк никуда не исчезло). Посмотрите на условие связи последней таблицы: ПО (ВТСсылки.Ссылка = ВТТЧ.Ссылка). Автор полностью проигнорировал таблицу ВТДаты, в которой вычислялась максимальная дата тарифа. Он связал исходную таблицу со всей табличной частью только по полю Ссылка. Это означает, что если в табличной части элемента будет 5 строк с разными датами, запрос вернет все 5 строк, и элемент продублируется 5 раз. Вычисление МАКСИМУМ(Дата) во временной таблице оказалось абсолютно бесполезным, так как оно никак не ограничивает выборку из ВТТЧ.
Ошибка вторая: Падение производительности из-за разыменования полей «через точку». Обратите внимание на строки выборки: ВТСсылки.Ссылка.Код, ВТСсылки.Ссылка.Наименование и так далее. Обращение к реквизитам через точку от поля Ссылка во вложенном запросе заставляет платформу 1С генерировать неявные левые соединения (LEFT JOIN) с таблицей справочника для каждого такого поля на уровне СУБД. В результате простейший запрос превращается в тяжеловесную конструкцию с десятком скрытых соединений. На больших объемах данных динамический список начнет зависать при прокрутке и поиске.
Миф о временных таблицах: Автор также упомянул, что пакетные запросы с временными таблицами не работают в динамических списках. Это устаревшее заблуждение. Начиная с версии платформы 1С:Предприятие 8.3.8, динамические списки полностью поддерживают пакетные запросы. Главное требование — последний запрос пакета должен быть выбирающим (основным), а временные таблицы должны быть корректно проиндексированы.
Давайте построим корректный запрос, который лишен указанных недостатков. Мы выберем сам справочник в качестве основной таблицы (что исключит необходимость разыменования полей через точку), найдем максимальные даты изменений для каждого элемента и точечно присоединим строки табличной части, соответствующие этим датам.
Проанализируем оптимальный вариант запроса:
ВЫБРАТЬ
Тарифы.Ссылка КАК Ссылка,
Тарифы.ПометкаУдаления КАК ПометкаУдаления,
Тарифы.Код КАК Код,
Тарифы.Наименование КАК Наименование,
Тарифы.СтанцияНазначения КАК СтанцияНазначения,
Тарифы.СтанцияОтправления КАК СтанцияОтправления,
Тарифы.ВидПодвижногоСостава КАК ВидПодвижногоСостава,
Тарифы.Подрядчик КАК Подрядчик,
ЕСТЬNULL(ВТ_ПоследниеДаты.Дата, ДАТАВРЕМЯ(1, 1, 1)) КАК Дата,
ЕСТЬNULL(ВТ_ДеталиТарифов.ТарифнаяСтавка, 0) КАК ТарифнаяСтавка,
ЕСТЬNULL(ВТ_ДеталиТарифов.ВключенПорожнийВозврат, ЛОЖЬ) КАК ВключенПорожнийВозврат
ИЗ
Справочник.СобиЖДТарифныеСтавки КАК Тарифы
// Шаг 1: Получаем последнюю дату тарифа для каждого элемента справочника
ЛЕВОЕ СОЕДИНЕНИЕ (
ВЫБРАТЬ
ТЧ_Даты.Ссылка КАК Ссылка,
МАКСИМУМ(ТЧ_Даты.Дата) КАК Дата
ИЗ
Справочник.СобиЖДТарифныеСтавки.ТабличнаяЧасть1 КАК ТЧ_Даты
СГРУППИРОВАТЬ ПО
ТЧ_Даты.Ссылка
) КАК ВТ_ПоследниеДаты
ПО Тарифы.Ссылка = ВТ_ПоследниеДаты.Ссылка
// Шаг 2: Присоединяем реквизиты ТЧ, соответствующие найденной максимальной дате
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.СобиЖДТарифныеСтавки.ТабличнаяЧасть1 КАК ВТ_ДеталиТарифов
ПО Тарифы.Ссылка = ВТ_ДеталиТарифов.Ссылка
И ВТ_ПоследниеДаты.Дата = ВТ_ДеталиТарифов.Date
Давайте разберем по шагам, почему этот вариант работает быстро и решает все поставленные задачи:
ВЫБРАТЬ мы обращаемся к полям справочника напрямую через псевдоним основной таблицы Тарифы.Код, Тарифы.Наименование. Это исключает лишние скрытые соединения СУБД и обеспечивает максимальное быстродействие.СобиЖДТарифныеСтавки. К нему левым соединением подключаются данные табличной части. Это гарантирует, что абсолютно все элементы справочника попадут в итоговый список, даже если их табличная часть пуста.МАКСИМУМ.ПО Тарифы.Ссылка = ВТ_ДеталиТарифов.Ссылка И ВТ_ПоследниеДаты.Дата = ВТ_ДеталиТарифов.Дата. Именно это условие отсекает все старые записи из табличной части и гарантирует, что для каждого элемента справочника будет выбрана ровно одна актуальная строка.ЕСТЬNULL позволяет корректно вывести пустые значения (дату начала времен, нулевую ставку или булево ЛОЖЬ) для тех элементов, у которых табличная часть не заполнена.В реальной практике может возникнуть ситуация, когда пользователь умудрился ввести в табличную часть две разные строки с абсолютно одинаковой датой и временем. В этом случае наше второе левое соединение найдет две строки с максимальной датой и элемент снова продублируется в списке.
Чтобы застраховаться от этой редкой, но возможной ошибки, рекомендуется использовать для связи не дату, а максимальный номер строки на эту дату. Посмотрим, как будет выглядеть усовершенствованное соединение:
ВЫБРАТЬ
Тарифы.Ссылка КАК Ссылка,
Тарифы.Код КАК Код,
Тарифы.Наименование КАК Наименование,
ЕСТЬNULL(ВТ_ДеталиТарифов.ТарифнаяСтавка, 0) КАК ТарифнаяСтавка
ИЗ
Справочник.СобиЖДТарифныеСтавки КАК Тарифы
// Шаг 1: Находим максимальную дату
ЛЕВОЕ СОЕДИНЕНИЕ (
ВЫБРАТЬ
ТЧ_Даты.Ссылка КАК Ссылка,
МАКСИМУМ(ТЧ_Даты.Дата) КАК Дата
ИЗ
Справочник.СобиЖДТарифныеСтавки.ТабличнаяЧасть1 КАК ТЧ_Даты
СГРУППИРОВАТЬ ПО
ТЧ_Даты.Ссылка
) КАК ВТ_ПоследниеДаты
ПО Тарифы.Ссылка = ВТ_ПоследниеДаты.Ссылка
// Шаг 2: Находим максимальный номер строки на эту дату (на случай совпадения дат)
ЛЕВОЕ СОЕДИНЕНИЕ (
ВЫБРАТЬ
ТЧ_Номера.Ссылка КАК Ссылка,
ТЧ_Номера.Дата КАК Дата,
МАКСИМУМ(ТЧ_Номера.НомерСтроки) КАК НомерСтроки
ИЗ
Справочник.СобиЖДТарифныеСтавки.ТабличнаяЧасть1 КАК ТЧ_Номера
СГРУППИРОВАТЬ ПО
ТЧ_Номера.Ссылка,
ТЧ_Номера.Дата
) КАК ВТ_МаксСтроки
ПО Тарифы.Ссылка = ВТ_МаксСтроки.Ссылка И ВТ_ПоследниеДаты.Дата = ВТ_МаксСтроки.Дата
// Шаг 3: Получаем саму ставку по ссылке, дате и номеру строки
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.СобиЖДТарифныеСтавки.ТабличнаяЧасть1 КАК ВТ_ДеталиТарифов
ПО Тарифы.Ссылка = ВТ_ДеталиТарифов.Ссылка
И ВТ_МаксСтроки.Дата = ВТ_ДеталиТарифов.Дата
И ВТ_МаксСтроки.НомерСтроки = ВТ_ДеталиТарифов.НомерСтроки
Такой подход обеспечивает 100% защиту от дублирования строк в динамическом списке при любых обстоятельствах.
Даже если данный запрос полностью решает текущую техническую проблему, с точки зрения архитектуры систем класса ERP хранение истории изменений в табличных частях справочников является плохой практикой. Давайте проанализируем основные риски:
Рекомендация: Правильным архитектурным решением для хранения периодических данных (таких как тарифные ставки, цены и курсы валют) является использование Периодического регистра сведений. У него есть встроенный виртуальный механизм СрезПоследних, который оптимизирован на уровне СУБД и избавляет разработчиков от необходимости писать сложные вложенные запросы для динамических списков.