При разработке отчетов и обработок в системе 1С программисты часто сталкиваются с необходимостью отбора или группировки данных по свойствам объектов, которые не хранятся в виде прямых реквизитов справочника. Ярким примером является классификация номенклатуры (классы ТОП, ABC, XYZ). Такие значения обычно рассчитываются динамически или хранятся в специальном регистре сведений дополнительных реквизитов и сведений.
В этой статье мы подробно разберем, как правильно связать данные регистра накопления (например, ПартииТоваровНаСкладах) с дополнительными свойствами из регистра сведений ЗначенияСвойствОбъектов, проанализируем типичные ошибки новичков, приводящие к падению производительности СУБД, и построим красивый, оптимизированный и быстродействующий запрос.
Представим сценарий: нам необходимо получить список документов списания и оприходования товаров с детализацией по дате, складу, номенклатуре и стоимости. При этом в выборку должна попасть только та номенклатура, которой присвоен определенный класс важности (в нашем примере — ТОП-классы А и B).
Сложность заключается в том, что в справочнике Номенклатура нет явного реквизита "ТОП". Вместо этого используется механизм дополнительных свойств. Данные хранятся в периодическом или непериодическом регистре сведений ЗначенияСвойствОбъектов, который имеет следующую структуру:
Объект — ссылка на объект конфигурации (в нашем случае — справочник Номенклатура). Имеет составной тип данных.Свойство — ссылка на План видов характеристик (ПВХ), определяющий суть свойства (например, свойство "ТОП-класс номенклатуры").Значение — конкретное значение свойства (например, элемент справочника "Значения свойств объектов" со значением "А", "B", "C" и т.д.). Также имеет составной тип.Наша цель — соединить таблицу партий товаров с этой таблицей свойств по ключевому полю номенклатуры и наложить жесткий отбор.
Для обеспечения высокой производительности запроса и исключения лишних дублей при соединении таблиц, первым делом выберем исходные данные по партиям товаров и поместим их во временную таблицу. Это позволит нам изолировать тяжелую выборку по регистрам накопления и подготовить данные для связи.
Давайте посмотрим на пример реализации первого пакета запроса:
ВЫБРАТЬ
ПартииТоваровНаСкладах.Период КАК ДатаДокумента,
ПартииТоваровНаСкладах.Склад КАК Склад,
ПартииТоваровНаСкладах.Номенклатура КАК Номенклатура,
ПартииТоваровНаСкладах.Регистратор.ИнвентаризацияТоваровНаСкладе КАК ДокументИнвентаризации,
ВЫБОР
КОГДА ПартииТоваровНаСкладах.Регистратор ССЫЛКА Документ.СписаниеТоваров
ТОГДА ПартииТоваровНаСкладах.Стоимость
ИНАЧЕ NULL
КОНЕЦ КАК СтоимостьСписания,
ВЫБОР
КОГДА ПартииТоваровНаСкладах.Регистратор ССЫЛКА Документ.ОприходованиеТоваров
ТОГДА ПартииТоваровНаСкладах.Стоимость
ИНАЧЕ NULL
КОНЕЦ КАК СтоимостьОприходования
ПОМЕСТИТЬ ВТ_ОбщиеДанныеПартий
ИЗ
РегистрНакопления.ПартииТоваровНаСкладах КАК ПартииТоваровНаСкладах
ГДЕ
ПартииТоваровНаСкладах.Период МЕЖДУ &Период1 И &Период2
И (ПартииТоваровНаСкладах.Регистратор ССЫЛКА Документ.СписаниеТоваров
ИЛИ ПартииТоваровНаСкладах.Регистратор ССЫЛКА Документ.ОприходованиеТоваров)
И НЕ ПартииТоваровНаСкладах.Регистратор.ИнвентаризацияТоваровНаСкладе = ЗНАЧЕНИЕ(Документ.ИнвентаризацияТоваровНаСкладе.ПустаяСсылка)
ИНДЕКСИРОВАТЬ ПО
Номенклатура
;
Обратите внимание на инструкцию ИНДЕКСИРОВАТЬ ПО Номенклатура в самом конце пакета. Мы принудительно создаем индекс по полю связи во временной таблице. Это критически важно, так как при последующем соединении с регистром сведений СУБД сможет мгновенно сопоставить строки, избегая полного сканирования таблиц (Table Scan).
Теперь рассмотрим, как правильно связать нашу временную таблицу ВТ_ОбщиеДанныеПартий с регистром сведений ЗначенияСвойствОбъектов. Основная ошибка начинающих разработчиков — попытка связать значение свойства напрямую со ссылкой на номенклатуру, путая измерения "Объект" и "Значение".
Правильное условие связи выглядит так: ВТ_ОбщиеДанныеПартий.Номенклатура = ЗначенияСвойствОбъектов.Объект.
Посмотрим на структуру второго запроса пакета:
ВЫБРАТЬ
ОбщиеДанные.ДатаДокумента КАК ДатаДокумента,
ОбщиеДанные.Склад КАК Склад,
ОбщиеДанные.Номенклатура КАК Номенклатура,
СвойстваОбъектов.Значение КАК ЗначениеСвойстваНоменклатуры,
СвойстваОбъектов.Свойство КАК СвойствоНоменклатуры,
ОбщиеДанные.ДокументИнвентаризации КАК ДокументИнвентаризации,
ОбщиеДанные.СтоимостьСписания КАК СтоимостьСписания,
ОбщиеДанные.СтоимостьОприходования КАК СтоимостьОприходования
ИЗ
ВТ_ОбщиеДанныеПартий КАК ОбщиеДанные
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ЗначенияСвойствОбъектов КАК СвойстваОбъектов
ПО ОбщиеДанные.Номенклатура = СвойстваОбъектов.Объект
ГДЕ
СвойстваОбъектов.Свойство = &ПараметрСвойствоТОП
И СвойстваОбъектов.Значение В (&МассивЗначенийТОП)
УПОРЯДОЧИТЬ ПО
ДатаДокумента УБЫВ
ИТОГИ
СУММА(СтоимостьСписания),
СУММА(СтоимостьОприходования)
ПО
Склад,
ЗначениеСвойстваНоменклатуры,
Номенклатура,
ДокументИнвентаризации
Использование Внутреннего соединения (ВНУТРЕННЕЕ СОЕДИНЕНИЕ) гарантирует, что в финальную выборку попадут только те позиции номенклатуры, для которых в регистре сведений действительно заполнено указанное свойство с нужным значением.
Несмотря на то, что приведенный выше запрос является рабочим, в реальных многопользовательских базах данных "1С:Предприятие" с сотнями тысяч записей он может вызывать существенные задержки и блокировки. Давайте проанализируем "узкие места" и исправим их.
В первом запросе пакета используется конструкция: ПартииТоваровНаСкладах.Регистратор.ИнвентаризацияТоваровНаСкладе. Поле Регистратор в регистрах накопления имеет составной тип данных (ссылается на десятки видов документов). Когда платформа транслирует обращение к реквизиту регистратора через точку на уровень СУБД, SQL-сервер генерирует левые соединения (LEFT JOIN) со всеми таблицами документов, входящими в этот тип. Это приводит к созданию огромного, крайне неэффективного SQL-плана запроса.
Решение: Ограничить тип регистратора с помощью функции ВЫРАЗИТЬ():
ВЫБОР
КОГДА ПартииТоваровНаСкладах.Регистратор ССЫЛКА Документ.СписаниеТоваров
ТОГДА ВЫРАЗИТЬ(ПартииТоваровНаСкладах.Регистратор КАК Документ.СписаниеТоваров).ИнвентаризацияТоваровНаСкладе
КОГДА ПартииТоваровНаСкладах.Регистратор ССЫЛКА Документ.ОприходованиеТоваров
ТОГДА ВЫРАЗИТЬ(ПартииТоваровНаСкладах.Регистратор КАК Документ.ОприходованиеТоваров).ИнвентаризацияТоваровНаСкладе
ИНАЧЕ НЕОПРЕДЕЛЕНО
КОНЕЦ КАК ДокументИнвентаризации
В коде новичков часто можно увидеть отборы вида: СвойстваОбъектов.Свойство.Код = "000000467". Это серьезная архитектурная ошибка:
Значение в регистре сведений имеет составной тип. Поиск по коду заставляет СУБД выполнять неявные соединения и сканировать таблицы, полностью игнорируя индексы.Решение: Всегда передавайте нужные элементы Планов видов характеристик и справочников в качестве внешних параметров (&ПараметрСвойствоТОП и &МассивЗначенийТОП), предварительно получая ссылки на них в коде на встроенном языке (через предопределенные элементы или настройки).
Учитывая все архитектурные требования и стандарты разработки "1С", мы можем составить оптимизированный вариант запроса, готовый к высоким нагрузкам:
ВЫБРАТЬ
ПартииТоваровНаСкладах.Период КАК ДатаДокумента,
ПартииТоваровНаСкладах.Склад КАК Склад,
ПартииТоваровНаСкладах.Номенклатура КАК Номенклатура,
ВЫБОР
КОГДА ПартииТоваровНаСкладах.Регистратор ССЫЛКА Документ.СписаниеТоваров
ТОГДА ВЫРАЗИТЬ(ПартииТоваровНаСкладах.Регистратор КАК Документ.СписаниеТоваров).ИнвентаризацияТоваровНаСкладе
КОГДА ПартииТоваровНаСкладах.Регистратор ССЫЛКА Документ.ОприходованиеТоваров
ТОГДА ВЫРАЗИТЬ(ПартииТоваровНаСкладах.Регистратор КАК Документ.ОприходованиеТоваров).ИнвентаризацияТоваровНаСкладе
ИНАЧЕ ЗНАЧЕНИЕ(Документ.ИнвентаризацияТоваровНаСкладе.ПустаяСсылка)
КОНЕЦ КАК ДокументИнвентаризации,
ВЫБОР
КОГДА ПартииТоваровНаСкладах.Регистратор ССЫЛКА Документ.СписаниеТоваров
ТОГДА ПартииТоваровНаСкладах.Стоимость
ИНАЧЕ NULL
КОНЕЦ КАК СтоимостьСписания,
ВЫБОР
КОГДА ПартииТоваровНаСкладах.Регистратор ССЫЛКА Документ.ОприходованиеТоваров
ТОГДА ПартииТоваровНаСкладах.Стоимость
ИНАЧЕ NULL
КОНЕЦ КАК СтоимостьОприходования
ПОМЕСТИТЬ ВТ_ОптимизированныеПартии
ИЗ
РегистрНакопления.ПартииТоваровНаСкладах КАК ПартииТоваровНаСкладах
ГДЕ
ПартииТоваровНаСкладах.Период МЕЖДУ &Период1 И &Период2
И (ПартииТоваровНаСкладах.Регистратор ССЫЛКА Документ.СписаниеТоваров
ИЛИ ПартииТоваровНаСкладах.Регистратор ССЫЛКА Документ.ОприходованиеТоваров)
ИНДЕКСИРОВАТЬ ПО
Номенклатура
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ОбщиеДанные.ДатаДокумента КАК ДатаДокумента,
ОбщиеДанные.Склад КАК Склад,
ОбщиеДанные.Номенклатура КАК Номенклатура,
СвойстваОбъектов.Значение КАК ЗначениеСвойстваНоменклатуры,
СвойстваОбъектов.Свойство КАК СвойствоНоменклатуры,
ОбщиеДанные.ДокументИнвентаризации КАК ДокументИнвентаризации,
ОбщиеДанные.СтоимостьСписания КАК СтоимостьСписания,
ОбщиеДанные.СтоимостьОприходования КАК СтоимостьОприходования
ИЗ
ВТ_ОптимизированныеПартии КАК ОбщиеДанные
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ЗначенияСвойствОбъектов КАК СвойстваОбъектов
ПО ОбщиеДанные.Номенклатура = СвойстваОбъектов.Объект
ГДЕ
СвойстваОбъектов.Свойство = &СвойствоТОП
И СвойстваОбъектов.Значение В (&КлассыТОП)
И НЕ ОбщиеДанные.ДокументИнвентаризации = ЗНАЧЕНИЕ(Документ.ИнвентаризацияТоваровНаСкладе.ПустаяСсылка)
УПОРЯДОЧИТЬ ПО
ДатаДокумента УБЫВ
ИТОГИ
СУММА(СтоимостьСписания),
СУММА(СтоимостьОприходования)
ПО
Склад,
ЗначениеСвойстваНоменклатуры,
Номенклатура,
ДокументИнвентаризации
Если вы работаете в современной конфигурации на управляемых формах, классификация номенклатуры по классам A, B, C чаще всего реализована в рамках стандартной подсистемы ABC/XYZ-классификации.
В этом случае вместо тяжелого универсального регистра ЗначенияСвойствОбъектов настоятельно рекомендуется использовать специализированный регистр сведений ABCXYZКлассификацияНоменклатуры. Запрос к нему выполняется в разы быстрее, так как его структура максимально проста и содержит прямые проиндексированные измерения: Номенклатура, РазделКлассификации и ресурс Класс (значение перечисления).