Составные типы — бесплатный сыр мышеловки производительности
"...Еще одним важным решением в части работы с данными в "1С:Предприятии" является поддержка в полях таблиц составных типов данных. Эта возможность, насколько нам известно, не имеет близких аналогов в других системах. Важно, что система не просто предоставляет возможность хранения в одном поле разнородных значений, а делает это прозрачным для разработчика способом. Прежде всего, необходимо отметить полную поддержку работы с полями составных типов "движка" базы данных и языка запросов. Для этих полей поддерживается весь набор стандартных операций (сравнение, агрегирование и т.д.)."
"Архитектура "1С:Предприятия" как продукт инженерной мысли", Cергей Нуралиев
Поводом для написания статьи стал очередной разбор такой ошибки:
Server: Msg 8632, Level 17, State 2, Line 1
Internal error: An expression services limit has been reached. Please look for potentially complex expressions in your query, and try to simplify them.
Это сообщение выводилось при попытке сформировать некоторый отчет, "сердцем" которого был пакет запросов. При помощи технологического журнала и периодически упоминаемой матери автора отчета, виновный запрос был достаточно быстро определен. Запрос умещается в экран (ну может в два), но одного взгляда на запрос достаточно: составные типы полей вместе с остроумным их использованием способны убить любую информационную систему на платформе 1С. Мой скромный опыт подсказывает мне, что составные типы в умелых руках программистов — одна из наиболее частых проблем производительности запросов. Попробуем разобраться, почему я так категорично настроен. Показывать буду на примере MS SQL Server, но и для всех других СУБД ситуация близка.
Как хранятся данные составного типа в базе данных
На первый взгляд всё достаточно просто. Поле составного типа в базе данных представляется двумя, тремя или большим количеством отдельных полей:
- Идентификатор типа (двоичные данные, 1 байт, есть почти всегда, кроме поля "Регистратор" регистров, Ссылка в таблицах журналов и подобных "всегда ссылочных" полей)
- Строка (если в поле возможно строковое значение, длина зависит от максимальной длины строки)
- Число (если в поле возможно числовое значение, длина и точность зависит от параметров типа)
- Двоичные данные длины 1 байт для хранения значения типа булево (опять же, если возможно такое значение)
- Поле типа datetime, если возможны дата и/или время.
- Вид ссылки (вид справочника, перечисления, документа и т.п.), если возможно несколько видов ссылок (двоичные данные, 4 байта)
- Ссылка, если возможно хранение хотя бы какого-то ссылочного типа (двоичные данные, 16 байт)
Радует хотя бы то, что в этот бардак запихнуть строки неограниченной длины и хранилище значений не получится :) На этом этапе заметим, что даже незаполненные поля составного значения (кроме строкового типа переменной длины) хранят значения максимального с точки зрения хранения размера, а именно:
- Идентификатор типа: 1 байт
- Строка фиксированной длины: 2 байта на символ + 2 байта
- Число: 5, 9, 13 или 17 байт (для количества цифр 1-9, 10-19, 20-28 и 29-38 соответственно)
- Булево: 1 байт
- Дата/время: 8 байт
- Вид ссылки: 4 байта
- Ссылка: 16 байт
Это приводит к заметным излишним затратам на дисковое пространство и память сервера СУБД даже не учитывая, что данные хранятся выровненными по 8-кибибайтным страницам.
Следствие 1: составной тип приводит к заметному увеличению размера поля в записи.
Ну ладно, пусть на диске база занимает не 30 ГиБ, а 35 ГиБ — кто ж эти копейки считает и заметит? Заметит как минимум MS SQL Server, когда будет строить индексы. Максимальная длина индекса 16 полей или 900 байт. Эти границы чаще всего нарушаются именно на регистрах сведений с составными полями в измерениях. Кстати, об индексах, а что вообще будет, если мы индексируем составное поле? В самом простом случае, если, например, мы индексируем составное поле справочника (без доп. упорядочивания), будет построено несколько индексов на каждый базовый тип:
- Для поиска по числовым значениям: Идентификатор типа + Число + Ссылка на элемент справочника
- Для поиска по строковым значениям: Идентификатор типа + Строка + Ссылка на элемент справочника
- Для поиска по булеву значению: Идентификатор типа + Булево + Ссылка на элемент справочника (блин, гениально "селективный" индекс)
- Для поиска по дате/времени: Идентификатор типа + Дата/время + Ссылка на элемент справочника
- Для поиска по ссылке: Идентификатор типа + Вид ссылки (если есть) + Ссылка на значение + Ссылка на справочник
До пяти индексов. К счастью, в самом частом случае, когда в составном типе возможно несколько ссылочных полей, и нет других типов, индекс всего один. Но индексы по одному полю — это еще не беда. Беда наступает, когда составные поля с несколькими индексами становятся измерениями в регистрах. Ведь платформа попытается создать индекс на каждую комбинацию базовых типов (в регистрах сведений на основную таблицу регистра, в регистрах накопления и бухгалтерии — на таблицах итогов). Всего 3 "универсальных" измерения и у вас 126 индексов (для регистра сведений 5*5*5 по составным полям + 1 по специальному внутреннему полю). Видимо из-за того, что в MS SQL Server больше 254 индексов построить нельзя, четвёртое и последующие поля в индексах просто не участвуют, но и к ошибке не приведут.
Следствие 2: Для составных полей может создаваться много индексов. Для регистров сведений с составными измерениями ОЧЕНЬ МНОГО индексов.
Индексы эти занимают место на диске, занимают время на вставке/изменении/удалении записей, требуют регулярного обслуживания. Кто-то скажет: "Да и ладно! Пусть занимают, зато выборки ускоряют!". Так ли это? Для операций поиска по одному значению — да. Во многих других случаях — никак нет.
Пример. Пусть у нас есть Справочник1
с составным полем СоставноеПоле
(может содержать число или ссылку на Документ1
) и Справочник2
с таким же составным полем СоставноеПоле
(может содержать число или ссылку на Документ1
). СоставноеПоле
индексировано в обоих справочниках. Есть запрос вида:
ВЫБРАТЬ Справочник1.Ссылка КАК Справочник1Ссылка, Справочник2.Ссылка КАК Справочник2Ссылка ИЗ Справочник.Справочник1 КАК Справочник1 ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Справочник2 КАК Справочник2 ПО Справочник1.СоставноеПоле = Справочник2.СоставноеПоле
В этом запросе индексы по составному полю использоваться не будут. Никак. Вне зависимости от статистики данных. Мы зря индексировали такое поле. Более того, индексы по такому составному полю не могут использоваться и в более простом запросе:
ВЫБРАТЬ РАЗЛИЧНЫЕ
Справочник1.СоставноеПоле КАК СоставноеПоле
ИЗ
Справочник.Справочник1 КАК Справочник1
Следствие 3. По составным полям может быть много индексов, которые никак не помогают в запросах. В общем случае индексы почти никогда не работают, если в составном поле есть несколько "базовых" типов.
Самый правильный вывод, который можно сделать из принципов хранения составных полей в 1С:Предприятии:
Следствие 4: Если необходимо использовать составные типы (например, те же субконто или характеристики), то хотя бы ограничьте тип только ссылочными полями. Это позволит избежать появления заведомо неиспользуемых индексов и совершенно провальных планов запросов.
В целом система хранения составных типов в 1С:Предприятии хороша своей архитектурной простотой, но недостаточно эффективна. Как будет видно из примеров ниже, индексы по таким полям почти никогда не используются и при этом индексы и сами данные могут занимать места гораздо больше, чем реально требуется. В принципе в современных СУБД давно есть средства, которы бы позволили работать с такими типами эффективно:
- Вычисляемые колонки доступны в MS SQL с достаточно старых версий и с навешенными на них индексами позволили бы:
- сэкономить на размере индексов,
- упростить выражения вычисления значений
- использовать индексы во многих случаях там, где они сейчас не используются и при этом существенно уменьшить их количество.
- если вычисляемые колонки вычислять из универсального поля-контейнера varbinary, то можно было бы существенно сэкономить место в основной таблице
- Фильтрованные индексы (доступны с MS SQL Server 2008) вместе с предыдущим пунктам позволили бы существенно уменьшить размер индексов и ускорить поиск.
- Разреженные столбцы (доступны с MS SQL Server 2008) можно было бы применять, если заменять неиспользуемые значения на NULL.
- Индексы с включенными полями (доступны с MS SQL Server 2005) позволили бы избежать лишних обращений к кластеризованным индексам.
- В некоторых случаях составное поле лучше было бы "нормализовать" его выделив его типы значений в отдельные таблицы, связав их с базовой таблицей по суррогатному ключу.
Причем многие из этих возможностей не влияют или не сильно влияют на текст запросов и имеют аналоги в других СУБД. Если кому-то будет интересно, пишите в личную почту, если тема будет востребована, я сделаю прототип-демонстрацию.
Использование значений составных типов в запросах
Следующий набор эффектов можно отнести к особенностям построения запросов SQL по запросам 1С.
Начнем с простых моментов. Как работает следующий запрос?
ВЫБРАТЬ
Справочник1.СоставноеПоле КАК СоставноеПоле
ИЗ
Справочник.Справочник1 КАК Справочник1
Если посмотреть технологический журнал или трассу MS SQL Server, то становится видно, что этот запрос примерно эквивалентен следующему псевдокоду (в статье часто будет использоваться "псевдокод" — то, как примерно выглядит запрос в СУБД, если бы его можно было записать на языке, похожем на язык запросов 1С):
ВЫБРАТЬ
Справочник1.СоставноеПоле.Тип КАК СоставноеПолеТип, // Двоичные данные, 1 байт
Справочник1.СоставноеПоле.Булево КАК СоставноеПолеБулево, // Двоичные данные, 1 байт
Справочник1.СоставноеПоле.Число КАК СоставноеПолеЧисло, // Число
Справочник1.СоставноеПоле.Дата КАК СоставноеПолеДата, // Дата+время 8 байт
Справочник1.СоставноеПоле.Строка КАК СоставноеПолеСтрока , // Строка — примерно 2*КоличествоСимволов
Справочник1.СоставноеПоле.ВидСсылки КАК СоставноеПолеВидСсылки, // Двоичные данные 4 байта
Справочник1.СоставноеПоле.Ссылка КАК СоставноеПолеСсылка // Двоичные данные 16 байт
ИЗ
Справочник.Справочник1 КАК Справочник1
Следствие 5. Возвращаемый из запроса составной тип заметно "тяжелее" простого.
Для запросов, возвращающих единицы записей это может быть несущественно, но для запросов, оперирующих тысячами и сотнями тысяч записей, эти лишние байты складываются в секунды загрузки сети, мегабайты занятой оперативной памяти и гигабайты дискового пространства во временной папке сервера предприятия. Из перечисленных ресурсов для 32-битных серверов 1С:Предприятия самым критичным может оказаться память, особенно, если в коде вместо обхода выборки часто используется Запрос.Выполнить().Выгрузить()
.
У вас в базе нет ни одного составного типа (кроме регистраторов, субконто и подобных неизбежностей)? Не расслабляйтесь. Составные типы замечательно появляются в запросах "сами":
ВЫБРАТЬ
Справочник1.Ссылка КАК СсылкаЛиЭто
ИЗ
Справочник.Справочник1 КАК Справочник1
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ 0
В этом запросе, тип получаемого поля СсылкаЛиЭто
расширяется до составного типа, содержащего число. Чуть менее очевидно, что тип становится составным если вместо 0 выбрать НЕОПРЕДЕЛЕНО
.
Следствие 6. 1С:Предприятие может "незаметно" и неявно начать использовать составные типы в запросах. Аккуратно относитесь к типам полей в запросах и не расширяйте их без необходимости.
Рекомендация: если вам нужно в запросе пустое значение, а вы по каким-то причинам не можете точно указать тип поля, то можно использовать NULL
, если это не нарушает логику работы запроса.
Вы всё еще думаете, что проклятие составных типов вас не коснётся? ОК, пусть у вас есть регистр накопления ОстаткиНоменклатуры
с двумя типами регистраторов: ПриходнаяНакладная
и РасходнаяНакладная
. Вы всего-навсего хотите узнать все товары, участвующие в движениях по нескольким документам. Ссылки на документы поместили в массив, написали очевидный запрос:
ВЫБРАТЬ РАЗЛИЧНЫЕ
ОстаткиНоменклатуры.Номенклатура
ИЗ
РегистрНакопления.ОстаткиНоменклатуры КАК ОстаткиНоменклатуры
ГДЕ
ОстаткиНоменклатуры.Регистратор В(&ФильтрДокументов)
В ФильтрДокументов
ссылки всего на 2 документа: 1 на ПриходнаяНакладная
и 1 на РасходнаяНакладная
. Сколько времени будет выполняться запрос? Упс. Сюрпри-и-из! Индекс по регистратору не используется! В плане запроса можно будет наблюдать сканирование кластеризованного индекса (если измерение Номенклатура
не индексировано) или сканирование индекса по измерению Номенклатура
(если такой индекс есть). В любом случае, такой запрос вместо ожидаемых долей секунды может начать работать минуты. Всё потому, что выражение ГДЕ ОстаткиНоменклатуры.Регистратор В(&ФильтрДокументов)
в данном случае преобразуется в WHERE (T1.Регистратор[Вид] + T1.Регистратор[Ссылка]
IN (ПриходнаяНакладная[Вид] + ПриходнаяНакладная[Ссылка], РасходнаяНакладная[Вид] + РасходнаяНакладная[Ссылка]))
. Конкатенация (сцепка) бинарных данных убила возможность использовать индекс. Если ссылки в массиве будут одного вида, то 1С:Предприятие выстроит запрос к СУБД так, что индекс будет использоваться.
Для небольших списков в фильтрах можно изменить такой запрос на что-нибудь вроде:
ВЫБРАТЬ РАЗЛИЧНЫЕ
ОстаткиНоменклатуры.Номенклатура
ИЗ
РегистрНакопления.ОстаткиНоменклатуры КАК ОстаткиНоменклатуры
ГДЕ
ОстаткиНоменклатуры.Регистратор = &Документ1
ИЛИ ОстаткиНоменклатуры.Регистратор = &Документ2
ИЛИ ОстаткиНоменклатуры.Регистратор = &Документ3
Я считаю, что примененное решение WHERE (T1.Регистратор_Вид + T1.Регистратор_Ссылка IN (ЧтоТо_Вид + ЧтоТо_Ссылка, ЧтоТо_Вид + ЧтоТо_Ссылка))
в 1С:Предприятии ошибочным в принципе. Есть же альтернативы, например то, как реализовано выражение (Поле1, Поле2) В (...) через EXISTS. Если уж так тяжело это реализовать, то можно наоборот — всегда хранить полную двадцатибайтную ссылку в одном поле и оперировать ею. Ну или еще какие-нибудь решения.
А теперь небольшая комбинация примеров:
ВЫБРАТЬ
Документы.Ссылка
ИЗ
(ВЫБРАТЬ
РасходнаяНакладная.Ссылка КАК Ссылка
ИЗ
Документ.РасходнаяНакладная КАК РасходнаяНакладная
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ПриходнаяНакладная.Ссылка
ИЗ
Документ.ПриходнаяНакладная КАК ПриходнаяНакладная) КАК Документы
ГДЕ
Документы.Ссылка В(&ФильтрДокументов)
Здесь получается, что Ссылка из-за объединения становится составным типом, из-за этого фильтр по списку документов (если массив содержит несколько типов) не использует индекс. Запрос следует переписать так:
ВЫБРАТЬ
РасходнаяНакладная.Ссылка КАК Ссылка
ИЗ
Документ.РасходнаяНакладная КАК РасходнаяНакладная
ГДЕ
РасходнаяНакладная.Ссылка В(&ФильтрДокументов)
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ПриходнаяНакладная.Ссылка
ИЗ
Документ.ПриходнаяНакладная КАК ПриходнаяНакладная
ГДЕ
ПриходнаяНакладная.Ссылка В(&ФильтрДокументов)
Следствие 7. Передаваемые в запрос параметры типов
Массив
,СписокЗначений
,ТаблицаЗначений
могут содержать составные типы (или значения нескольких типов), и это может ухудшить работу запроса, особенно условие "В (&Фильтр)
".
И всё-таки перечисленные проблемы — только начало проблем производительности. Главное веселье начинается при получении реквизитов составных типов. Начнем с простого:
ВЫБРАТЬ
ОстаткиНоменклатуры.Регистратор.Дата
ИЗ
РегистрНакопления.ОстаткиНоменклатуры КАК ОстаткиНоменклатуры
"Любимая" ошибка начинающих. Проблема этого запроса, что он эквивалентен (до плана запроса) следующему:
ВЫБРАТЬ
ВЫБОР
КОГДА ОстаткиНоменклатуры.Регистратор ССЫЛКА Документ.РасходнаяНакладная
ТОГДА РасходнаяНакладная.Дата
КОГДА ОстаткиНоменклатуры.Регистратор ССЫЛКА Документ.ПриходнаяНакладная
ТОГДА ПриходнаяНакладная.Дата
ИНАЧЕ NULL
КОНЕЦ КАК Дата
ИЗ
РегистрНакопления.ОстаткиНоменклатуры КАК ОстаткиНоменклатуры
ЛЕВОЕ СОЕДИНЕНИЕ Документ.РасходнаяНакладная КАК РасходнаяНакладная
ПО (РасходнаяНакладная.Ссылка = ОстаткиНоменклатуры.Регистратор)
ЛЕВОЕ СОЕДИНЕНИЕ Документ.ПриходнаяНакладная КАК ПриходнаяНакладная
ПО (ПриходнаяНакладная.Ссылка = ОстаткиНоменклатуры.Регистратор)
Это в нашем игрушечном примере с двумя регистраторами! В типовых конфигурациях количество соединений даже в простейших случаях быстро зашкаливает за сотню. Может это не так уж страшно? Соединение же по полю, которое в случае регистра индексировано, а в случае документа еще и является первичным ключом и кластеризованным индексом. Смотрим на похожий запрос:
ВЫБРАТЬ
ОстаткиНоменклатуры.Регистратор
ИЗ
РегистрНакопления.ОстаткиНоменклатуры КАК ОстаткиНоменклатуры
ГДЕ
ОстаткиНоменклатуры.Регистратор.Дата < &Дата
Этот запрос в части ГДЕ
будет эквивалентен
....
ГДЕ
ВЫБОР
КОГДА ОстаткиНоменклатуры.Регистратор ССЫЛКА Документ.РасходнаяНакладная
ТОГДА РасходнаяНакладная.Дата
КОГДА ОстаткиНоменклатуры.Регистратор ССЫЛКА Документ.ПриходнаяНакладная
ТОГДА ПриходнаяНакладная.Дата
ИНАЧЕ NULL
КОНЕЦ >= &Дата
Индексы? Забудьте. Если вы всё еще пишете подобные запросы, то можете сразу отложить 120 000 рублей и написать завещание заявление об увольнении.
Следующий виток ужаса начинается при обращении к реквизитам реквизитов. Нередко ленивые программисты пишут что-то типа Регистратор.Основание.Контрагент
или Субконто1.Владелец.Код
.
Пример. Пусть у наших документов из предыдущего примера есть еще поле Основание
, причем оно может быть составным (РасходнаяНакладная
и ПриходнаяНакладная
) и есть поле Контрагент
. Посмотрим, как будет выполняться запрос:
ВЫБРАТЬ
ОстаткиНоменклатуры.Регистратор
ИЗ
РегистрНакопления.ОстаткиНоменклатуры КАК ОстаткиНоменклатуры
ГДЕ
ОстаткиНоменклатуры.Регистратор.Основание.Контрагент = &Контрагент
А выполняться он будет вот так (в квадратных скобках указаны поля СУБД, являющиеся "частями" составного поля):
ВЫБРАТЬ
ОстаткиНоменклатуры.Регистратор[Вид],
ОстаткиНоменклатуры.Регистратор[Ссылка]
ИЗ
РегистрНакопления.ОстаткиНоменклатуры ОстаткиНоменклатуры
ЛЕВОЕ СОЕДИНЕНИЕ Документ.ПриходнаяНакладная КАК ПриходнаяНакладная
ПО
ОстаткиНоменклатуры.Регистратор[Вид] = [Документ.ПриходнаяНакладная] И
ОстаткиНоменклатуры.Регистратор[Ссылка] = ПриходнаяНакладная.Ссылка
ЛЕВОЕ СОЕДИНЕНИЕ Документ.ПриходнаяНакладная КАК ПриходнаяНакладнаяОснование_ПриходнаяНакладная
ПО
ПриходнаяНакладная.Основание[Тип] = [Ссылочный] И
ПриходнаяНакладная.Основание[Вид] = [Документ.ПриходнаяНакладная] И
ПриходнаяНакладная.Основание[Ссылка] = ПриходнаяНакладнаяОснование_ПриходнаяНакладная.Ссылка
ЛЕВОЕ СОЕДИНЕНИЕ Документ.РасходнаяНакладная КАК ПриходнаяНакладнаяОснование_РасходнаяНакладная
ПО
ПриходнаяНакладная.Основание[Тип] = [Ссылочный] И
ПриходнаяНакладная.Основание[Вид] = [Документ.РасходнаяНакладная] И
ПриходнаяНакладная.Основание[Ссылка] = ПриходнаяНакладнаяОснование_РасходнаяНакладная.Ссылка
ЛЕВОЕ СОЕДИНЕНИЕ Документ.РасходнаяНакладная КАК РасходнаяНакладная
ПО
ОстаткиНоменклатуры.Регистратор[Вид] = [Документ.РасходнаяНакладная] И
ОстаткиНоменклатуры.Регистратор[Ссылка] = РасходнаяНакладная.Ссылка
ЛЕВОЕ СОЕДИНЕНИЕ Документ.ПриходнаяНакладная КАК РасходнаяНакладнаяОснование_ПриходнаяНакладная
ПО
РасходнаяНакладная.Основание[Тип] = [Ссылочный] И
РасходнаяНакладная.Основание[Вид] = [Документ.ПриходнаяНакладная] И
РасходнаяНакладная.Основание[Ссылка] = РасходнаяНакладнаяОснование_ПриходнаяНакладная.Ссылка
ЛЕВОЕ СОЕДИНЕНИЕ Документ.РасходнаяНакладная КАК РасходнаяНакладнаяОснование_РасходнаяНакладная
ПО
РасходнаяНакладная.Основание[Тип] = [Ссылочный] И
РасходнаяНакладная.Основание[Вид] = [Документ.РасходнаяНакладная] И
РасходнаяНакладная.Основание[Ссылка] = РасходнаяНакладнаяОснование_РасходнаяНакладная.Ссылка
ГДЕ
(
ВЫБОР
КОГДА ОстаткиНоменклатуры.Регистратор[Вид] = [Документ.ПриходнаяНакладная]
ТОГДА
ВЫБОР
КОГДА
ПриходнаяНакладная.Основание[Тип] = [Ссылочный] И
ПриходнаяНакладная.Основание[Вид] = [Документ.ПриходнаяНакладная]
ТОГДА ПриходнаяНакладнаяОснование_ПриходнаяНакладная.Контрагент
КОГДА
ПриходнаяНакладная.Основание[Тип] = [Ссылочный] И
ПриходнаяНакладная.Основание[Вид] = [Документ.РасходнаяНакладная]
ТОГДА ПриходнаяНакладнаяОснование_РасходнаяНакладная.Контрагент
ИНАЧЕ NULL
КОНЕЦ
КОГДА ОстаткиНоменклатуры.Регистратор[Вид] = [Документ.РасходнаяНакладная]
ТОГДА
ВЫБОР
КОГДА
РасходнаяНакладная.Основание[Тип] = [Ссылочный] И
РасходнаяНакладная.Основание[Вид] = [Документ.ПриходнаяНакладная]
ТОГДА РасходнаяНакладнаяОснование_ПриходнаяНакладная.Контрагент
КОГДА
РасходнаяНакладная.Основание[Тип] = [Ссылочный] И
РасходнаяНакладная.Основание[Вид] = [Документ.РасходнаяНакладная]
ТОГДА РасходнаяНакладнаяОснование_РасходнаяНакладная.Контрагент
ИНАЧЕ NULL
КОНЕЦ
ИНАЧЕ NULL
КОНЕЦ = &Контрагент
)
В нашем предельно простом примере этот запрос приводит к 6 соединениям 7 таблиц, причем все 7 таблиц имеют большие шансы попасть на полное сканирование кластеризованного индекса (т.е. по сути сканирование таблицы) вместо использования подходящих индексов, даже если такие есть. Не отмахивайтесь "у меня база маленькая и так сойдёт"! Производительность такого запроса в некоторых случаях может падать как степень от количества таблиц. Что это значит на практике? На практике это значит, что даже в нашем примитивном случае запрос может замедлиться в 128 раз при увеличении количества документов вдвое или замедлиться вдвое при увеличении количества документов на 10%. В типовых конфигурациях, где в составном типе могут быть десятки и сотни простых, использование таких запросов почти всегда приведёт с существенным проблемам производительности. Еще одним негативным моментом могут стать избыточные блокировки при выполнении таких запросов в транзакциях (например, при проведении документов): мало того, что запрос выполняется всё дольше и дольше, так еще и все остальные пользователи постоянно отваливаются по таймауту блокировки.
И уж совсем печальной становится работа этого запроса, когда составной тип может содержать не только ссылки: условия и соединения становятся еще более громоздкими и быстро деградирующими.
Не обсуждая корректность или некорректность исходной задачи, попробуем понять, можно ли исправить такой запрос "малой кровью" без изменения архитектуры решения? Вполне возможно, но ценой этого станет размер и читаемость запроса:
Вариант 1.
ВЫБРАТЬ ОстаткиНоменклатуры.Регистратор ИЗ РегистрНакопления.ОстаткиНоменклатуры КАК ОстаткиНоменклатуры ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.ПриходнаяНакладная КАК ПриходнаяНакладная ПО ОстаткиНоменклатуры.Регистратор = ПриходнаяНакладная.Ссылка ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.ПриходнаяНакладная КАК ПриходнаяНакладнаяОснование_ПриходнаяНакладная ПО (ПриходнаяНакладная.Основание = ПриходнаяНакладнаяОснование_ПриходнаяНакладная.Ссылка) И (ПриходнаяНакладнаяОснование_ПриходнаяНакладная.Контрагент = &Контрагент) ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ ОстаткиНоменклатуры.Регистратор ИЗ РегистрНакопления.ОстаткиНоменклатуры КАК ОстаткиНоменклатуры ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.ПриходнаяНакладная КАК ПриходнаяНакладная ПО ОстаткиНоменклатуры.Регистратор = ПриходнаяНакладная.Ссылка ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РасходнаяНакладная КАК ПриходнаяНакладнаяОснование_РасходнаяНакладная ПО (ПриходнаяНакладная.Основание = ПриходнаяНакладнаяОснование_РасходнаяНакладная.Ссылка) И (ПриходнаяНакладнаяОснование_РасходнаяНакладная.Контрагент = &Контрагент) ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ ОстаткиНоменклатуры.Регистратор ИЗ РегистрНакопления.ОстаткиНоменклатуры КАК ОстаткиНоменклатуры ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РасходнаяНакладная КАК РасходнаяНакладная ПО ОстаткиНоменклатуры.Регистратор = РасходнаяНакладная.Ссылка ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.ПриходнаяНакладная КАК ПриходнаяНакладнаяОснование_ПриходнаяНакладная ПО (РасходнаяНакладная.Основание = ПриходнаяНакладнаяОснование_ПриходнаяНакладная.Ссылка) И (ПриходнаяНакладнаяОснование_ПриходнаяНакладная.Контрагент = &Контрагент) ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ ОстаткиНоменклатуры.Регистратор ИЗ РегистрНакопления.ОстаткиНоменклатуры КАК ОстаткиНоменклатуры ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РасходнаяНакладная КАК РасходнаяНакладная ПО ОстаткиНоменклатуры.Регистратор = РасходнаяНакладная.Ссылка ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РасходнаяНакладная КАК ПриходнаяНакладнаяОснование_РасходнаяНакладная ПО (РасходнаяНакладная.Основание = ПриходнаяНакладнаяОснование_РасходнаяНакладная.Ссылка) И (ПриходнаяНакладнаяОснование_РасходнаяНакладная.Контрагент = &Контрагент)
В этом варианте стало немного больше соединений, но все они будут гарантированно использовать подходящие индексы. В дополнение к этому планировщику запросов MS SQL Server дана чуть большая свобода в выборе планов за счет применения внутреннего соединения — оно симметрично относительно таблиц и в данном случае точнее отражает суть.
Вариант 2.
ВЫБРАТЬ ОстаткиНоменклатуры.Регистратор ИЗ РегистрНакопления.ОстаткиНоменклатуры КАК ОстаткиНоменклатуры ВНУТРЕННЕЕ СОЕДИНЕНИЕ (ВЫБРАТЬ ПриходнаяНакладная.Ссылка КАК Ссылка, ПриходнаяНакладная.Основание КАК Основание ИЗ Документ.ПриходнаяНакладная КАК ПриходнаяНакладная ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ РасходнаяНакладная.Ссылка, РасходнаяНакладная.Основание ИЗ Документ.РасходнаяНакладная КАК РасходнаяНакладная) КАК Регистраторы ПО (Регистраторы.Ссылка = ОстаткиНоменклатуры.Регистратор) ВНУТРЕННЕЕ СОЕДИНЕНИЕ (ВЫБРАТЬ ПриходнаяНакладная.Ссылка КАК Ссылка ИЗ Документ.ПриходнаяНакладная КАК ПриходнаяНакладная ГДЕ ПриходнаяНакладная.Контрагент = &Контрагент ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ РасходнаяНакладная.Ссылка ИЗ Документ.РасходнаяНакладная КАК РасходнаяНакладная ГДЕ РасходнаяНакладная.Контрагент = &Контрагент) КАК Основания ПО (Регистраторы.Основание = Основания.Ссылка)
У этого варианта, вообще говоря, планы выполнения могут оказаться менее стабильными и сильно зависеть от статистики данных. Зато в этом варианте меньше соединений и при наличии подходящих индексов (и удачного стечения звезд над планировщиком) он может оказаться быстрее.
Теперь вы понимаете, почему скромное выражение Субконто1.Владелец
в запросе по остатками регистра бухгалтерии, и тем более в части ГДЕ
этого запроса, на меня нагоняет панику?
Следствие 8. Для получения реквизита значения составного типа происходит несколько левых соединений, их может быть достаточно много и это может ухудшить работу запроса. Для получения реквизита от реквизита значения составного типа (
Поле1.Реквизит1.Реквизит2
, где хотя быПоле1
составное) используются крайне громоздкие и неэффективные конструкции.
Не всегда при использовании составного типа есть "хорошее" решение. Например, если значение составного типа выводится в отчете, то появляется дилемма:
Следствие 9. Если получать представление составного типа в запросе, то это приводит к большому количеству соединений. Если получать потом при выводе, например, отчета, то это приведёт к большому количеству запросов. Хороших новостей нет.
Использование значений составных типов в сложных выражениях в запросах
Отдельно хотелось бы показать, как реализована работа с составными типами в некоторых выражениях языка запроса:
- Агрегатные функции (минимум, максимум, количество)
- Использование в выражениях
ВЫБОР ... КОГДА ... КОНЕЦ
Буду демонстрировать примеры на такой временной таблице:
ВЫБРАТЬ "Число" КАК ТипПоля, 1 КАК СоставноеПоле ПОМЕСТИТЬ Т ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ "Строка", "1" ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ ПЕРВЫЕ 1 "Документ.ПриходнаяНакладная", ПриходнаяНакладная.Ссылка ИЗ Документ.ПриходнаяНакладная КАК ПриходнаяНакладная ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ ПЕРВЫЕ 1 "Документ.РасходнаяНакладная", РасходнаяНакладная.Ссылка ИЗ Документ.РасходнаяНакладная КАК РасходнаяНакладная
Агрегатные функции — КОЛИЧЕСТВО(...)
Эта функция является самой "безобидной". КОЛИЧЕСТВО(СоставноеПоле)
вычисляется как количество от однобайтного идентификатора типа поля.
Агрегатные функции — КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ...)
Здесь тоже всё относительно хорошо. КОЛИЧЕСТВО(РАЗЛИЧНЫЕ СоставноеПоле)
вычисляется в нашем примере как
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ВЫРАЗИТЬ (СоставноеПоле КАК НЕОПРЕДЕЛЕНО)) +
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ВЫРАЗИТЬ (СоставноеПоле КАК ЧИСЛО)) +
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ВЫРАЗИТЬ (СоставноеПоле КАК СТРОКА)) +
КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ВЫРАЗИТЬ (СоставноеПоле КАК ССЫЛКА))
Да, безусловно, я в курсе, что это выражение не может работать в запросе, но это лишь демонстрация, как примерно 1С считает это выражение.
При этом:
- Псевдовыражение
ВЫРАЗИТЬ (СоставноеПоле КАК НЕОПРЕДЕЛЕНО)
возвращаетНЕОПРЕДЕЛЕНО
либоNULL
ВЫРАЗИТЬ (СоставноеПоле КАК ССЫЛКА)
возвращает двадцатибайтное выражение соответствующееСоставноеПоле_Вид + СоставноеПоле_Ссылка
- Все слагаемые получаются как выражение
CASE WHEN THEN END
, поэтому индексы не используются.
Агрегатные функции — МИНИМУМ(...) и МАКСИМУМ(...)
Эти функции вычисляются фактически одинаково. И одинаково тяжело для запроса. Для примера возьмем функцию "МИНИМУМ":
МИНИМУМ(T.СоставноеПоле[Тип]),
ВЫБОР
КОГДА МИНИМУМ(T.СоставноеПоле[Тип]) = [ЧИСЛО]
ТОГДА МИНИМУМ(ВЫБОР КОГДА T.СоставноеПоле[Тип] = [ЧИСЛО] ТОГДА T.СоставноеПоле[ЧИСЛО] КОНЕЦ)
ИНАЧЕ ВЫБОР КОГДА МИНИМУМ(T.СоставноеПоле[Тип]) IS NOT NULL ТОГДА 0.0 КОНЕЦ
КОНЕЦ,
ВЫБОР
КОГДА МИНИМУМ(T.СоставноеПоле[Тип]) = [СТРОКА]
ТОГДА МИНИМУМ(ВЫБОР КОГДА T.СоставноеПоле[Тип] = [СТРОКА] ТОГДА T.СоставноеПоле[СТРОКА] КОНЕЦ)
ИНАЧЕ ВЫБОР КОГДА МИНИМУМ(T.СоставноеПоле[Тип]) IS NOT NULL ТОГДА '' КОНЕЦ
КОНЕЦ,
ВЫБОР
КОГДА МИНИМУМ(T.СоставноеПоле[Тип]) = [ССЫЛКА]
ТОГДА ПОДСТРОКА(МИНИМУМ(T.СоставноеПоле[ВИД] + T.СоставноеПоле[ССЫЛКА]),1,4) // Здесь ПОДСТРОКА оперирует двоичными данными
ИНАЧЕ ВЫБОР КОГДА МИНИМУМ(T.СоставноеПоле[Тип]) IS NOT NULL ТОГДА 0x00000000 КОНЕЦ
КОНЕЦ,
ВЫБОР
КОГДА МИНИМУМ(T.СоставноеПоле[Тип]) = [ССЫЛКА]
ТОГДА ПОДСТРОКА(МИНИМУМ(T.СоставноеПоле[ВИД] + T.СоставноеПоле[ССЫЛКА]),5,16) // Здесь ПОДСТРОКА оперирует двоичными данными
ИНАЧЕ ВЫБОР КОГДА МИНИМУМ(T.СоставноеПоле[Тип]) IS NOT NULL ТОГДА 0x00000000000000000000000000000000 КОНЕЦ
КОНЕЦ
Здесь есть следующие негативные моменты:
- Индексы опять не имеют шансов на использование (опять сканы, опять блокировки и прочие радости)
- Если
СоставноеПоле
— не поле, а вычисляемое выражение, то оно будет подставлено в вычисления несколько раз. Это очень быстро увеличивает сложность запроса. - Вычисляется выражение
ПОДСТРОКА(МИНИМУМ(T.СоставноеПоле[ВИД] + T.СоставноеПоле[ССЫЛКА]),...,...)
— это может спровоцировать операциюTable Spool
в плане запроса, да и вообще получается достаточно затратная операция.
Следствие 10. Не используйте агрегатные функции МИНИМУМ(...) и МАКСИМУМ(...) к составным типам.
Рекомендация: если нужно получить одно значение (без группировок), то вместо
ВЫБРАТЬ МИНИМУМ(Т.СоставноеПоле) КАК СоставноеПоле ИЗ Т КАК Т
можно использовать следующий запрос
ВЫБРАТЬ ПЕРВЫЕ 1 Т.СоставноеПоле КАК СоставноеПоле ИЗ Т КАК Т УПОРЯДОЧИТЬ ПО СоставноеПоле ВОЗР
В этом случае индексы могут использоваться, хотя и не всегда. Да и выражение для вычисления значительно проще.
Хотелось бы, конечно, чтобы для вычисления МИНИМУМ и МАКСИМУМ, а также для срезов последних фирма 1С в СУБД Oracle и MS SQL 2012 использовала аналитические функции типа FIRST_VALUE и LAST_VALUE, но это уже скорее ворчание.
Выражения ВЫБОР КОГДА ... ТОГДА ... ИНАЧЕ ... КОНЕЦ
Выражение ВЫБОР
следует использовать очень осторожно, если в нём используются составные типы. Основной эффект состоит в том, что такое выражение быстро усложняется и обрастает неочевидными вычислениями. Начнем с простых случаев.
Запрос:
ВЫБРАТЬ ВЫБОР КОГДА Т.ТипПоля = "Документ.ПриходнаяНакладная" ТОГДА Т.СоставноеПоле КОНЕЦ КАК Поле1 ИЗ Т КАК Т
Этот запрос будет выполняться как
ВЫБРАТЬ ВЫБОР КОГДА Т.ТипПоля = "Документ.ПриходнаяНакладная" ТОГДА Т.СоставноеПоле[Тип] КОНЕЦ КАК Поле1[Тип], ВЫБОР КОГДА Т.ТипПоля = "Документ.ПриходнаяНакладная" ТОГДА Т.СоставноеПоле[Число] КОНЕЦ КАК Поле1[Число], ВЫБОР КОГДА Т.ТипПоля = "Документ.ПриходнаяНакладная" ТОГДА Т.СоставноеПоле[Строка] КОНЕЦ КАК Поле1[Строка], ВЫБОР КОГДА Т.ТипПоля = "Документ.ПриходнаяНакладная" ТОГДА Т.СоставноеПоле[Вид] КОНЕЦ КАК Поле1[Вид], ВЫБОР КОГДА Т.ТипПоля = "Документ.ПриходнаяНакладная" ТОГДА Т.СоставноеПоле[Ссылка] КОНЕЦ КАК Поле1[Ссылка], ИЗ Т КАК Т
Обратите внимание:
- Выражение условия
КОГДА
честно размножилось по полям. Если оно будет сложнее, то это может повлиять на производительность. Особенно аккуратно надо относиться к условиям "В (...)": такие условия могут содержать соединения. - Тип никак не ограничился (и даже наоборот, мог расшириться, если бы частей
КОГДА...ТОГДА
в выражении было несколько)
Из второго момента явно следует, что следующая попытка оптимизации запроса только ухудшит запрос:
ВЫБРАТЬ ВЫБОР КОГДА Т.СоставноеПоле ССЫЛКА Документ.ПриходнаяНакладная ТОГДА Т.СоставноеПоле КОНЕЦ КАК Поле1 ИЗ Т КАК Т
Фактически он распадётся идентично запросу выше:
ВЫБРАТЬ ВЫБОР КОГДА Т.СоставноеПоле ССЫЛКА Документ.ПриходнаяНакладная ТОГДА Т.СоставноеПоле[Тип] КОНЕЦ КАК Поле1[Тип], ВЫБОР КОГДА Т.СоставноеПоле ССЫЛКА Документ.ПриходнаяНакладная ТОГДА Т.СоставноеПоле[Число] КОНЕЦ КАК Поле1[Число], ВЫБОР КОГДА Т.СоставноеПоле ССЫЛКА Документ.ПриходнаяНакладная ТОГДА Т.СоставноеПоле[Строка] КОНЕЦ КАК Поле1[Строка], ВЫБОР КОГДА Т.СоставноеПоле ССЫЛКА Документ.ПриходнаяНакладная ТОГДА Т.СоставноеПоле[Вид] КОНЕЦ КАК Поле1[Вид], ВЫБОР КОГДА Т.СоставноеПоле ССЫЛКА Документ.ПриходнаяНакладная ТОГДА Т.СоставноеПоле[Ссылка] КОНЕЦ КАК Поле1[Ссылка], ИЗ Т КАК Т
Если вам нужно ограничить тип составного поля, то следует пользоваться операцией ВЫРАЗИТЬ
:
ВЫБРАТЬ ВЫРАЗИТЬ (Т.СоставноеПоле КАК Документ.ПриходнаяНакладная) КАК Поле1 ИЗ Т КАК Т
При аккуратной работе ВЫБОР
не так страшен. Хуже становится, когда появляются вложенные ВЫБОР
с составными типами. В этом случае "безобидный" запрос:
ВЫБРАТЬ ВЫБОР КОГДА ВЫБОР КОГДА Т.СоставноеПоле.Основание ССЫЛКА Документ.ПриходнаяНакладная ТОГДА Т.СоставноеПоле.Основание КОНЕЦ ССЫЛКА Документ.ПриходнаяНакладная ТОГДА ВЫБОР КОГДА Т.СоставноеПоле.Основание ССЫЛКА Документ.ПриходнаяНакладная ТОГДА Т.СоставноеПоле.Основание.Дата КОНЕЦ КОНЕЦ КАК Поле1 ИЗ Т КАК Т
превращается в следующее (я даже форматировать не стал, но при "обычном" форматировании там несколько сотен строк)
ВЫБРАТЬ ВЫБОР КОГДА (ВЫБОР КОГДА (ВЫБОР КОГДА T1.СоставнойТип[Тип] = [ТипСсылка] И T1.СоставнойТип[Вид] = @P1 ТОГДА T2.Основание[Тип] КОГДА T1.СоставнойТип[Тип] = [ТипСсылка] И T1.СоставнойТип[Вид] = @P2 ТОГДА T3.Основание[Тип] ИНАЧЕ NULL КОНЕЦ = [ТипСсылка] И ВЫБОР КОГДА T1.СоставнойТип[Тип] = [ТипСсылка] И T1.СоставнойТип[Вид] = @P3 ТОГДА T2.Основание[Вид] КОГДА T1.СоставнойТип[Тип] = [ТипСсылка] И T1.СоставнойТип[Вид] = @P4 ТОГДА T3.Основание[Вид] ИНАЧЕ NULL КОНЕЦ = @P5) ТОГДА ВЫБОР КОГДА T1.СоставнойТип[Тип] = [ТипСсылка] И T1.СоставнойТип[Вид] = @P6 ТОГДА T2.Основание[Тип] КОГДА T1.СоставнойТип[Тип] = [ТипСсылка] И T1.СоставнойТип[Вид] = @P7 ТОГДА T3.Основание[Тип] ИНАЧЕ NULL КОНЕЦ КОНЕЦ = [ТипСсылка] И ВЫБОР КОГДА (ВЫБОР КОГДА T1.СоставнойТип[Тип] = [ТипСсылка] И T1.СоставнойТип[Вид] = @P8 ТОГДА T2.Основание[Тип] КОГДА T1.СоставнойТип[Тип] = [ТипСсылка] И T1.СоставнойТип[Вид] = @P9 ТОГДА T3.Основание[Тип] ИНАЧЕ NULL КОНЕЦ = [ТипСсылка] И ВЫБОР КОГДА T1.СоставнойТип[Тип] = [ТипСсылка] И T1.СоставнойТип[Вид] = @P10 ТОГДА T2.Основание[Вид] КОГДА T1.СоставнойТип[Тип] = [ТипСсылка] И T1.СоставнойТип[Вид] = @P11 ТОГДА T3.Основание[Вид] ИНАЧЕ NULL КОНЕЦ = @P12) ТОГДА ВЫБОР КОГДА T1.СоставнойТип[Тип] = [ТипСсылка] И T1.СоставнойТип[Вид] = @P13 ТОГДА T2.Основание[Вид] КОГДА T1.СоставнойТип[Тип] = [ТипСсылка] И T1.СоставнойТип[Вид] = @P14 ТОГДА T3.Основание[Вид] ИНАЧЕ NULL КОНЕЦ КОНЕЦ = @P15) ТОГДА ВЫБОР КОГДА (ВЫБОР КОГДА T1.СоставнойТип[Тип] = [ТипСсылка] И T1.СоставнойТип[Вид] = @P16 ТОГДА T2.Основание[Тип] КОГДА T1.СоставнойТип[Тип] = [ТипСсылка] И T1.СоставнойТип[Вид] = @P17 ТОГДА T3.Основание[Тип] ИНАЧЕ NULL КОНЕЦ = [ТипСсылка] И ВЫБОР КОГДА T1.СоставнойТип[Тип] = [ТипСсылка] И T1.СоставнойТип[Вид] = @P18 ТОГДА T2.Основание[Вид] КОГДА T1.СоставнойТип[Тип] = [ТипСсылка] И T1.СоставнойТип[Вид] = @P19 ТОГДА T3.Основание[Вид] ИНАЧЕ NULL КОНЕЦ = @P20) ТОГДА ВЫБОР КОГДА T1.СоставнойТип[Тип] = [ТипСсылка] И T1.СоставнойТип[Вид] = @P21 ТОГДА ВЫБОР КОГДА T2.Основание[Тип] = [ТипСсылка] И T2.Основание[Вид] = @P22 ТОГДА T4.Дата КОГДА T2.Основание[Тип] = [ТипСсылка] И T2.Основание[Вид] = @P23 ТОГДА T5.Дата ИНАЧЕ NULL КОНЕЦ КОГДА T1.СоставнойТип[Тип] = [ТипСсылка] И T1.СоставнойТип[Вид] = @P24 ТОГДА ВЫБОР КОГДА T3.Основание[Тип] = [ТипСсылка] И T3.Основание[Вид] = @P25 ТОГДА T6.Дата КОГДА T3.Основание[Тип] = [ТипСсылка] И T3.Основание[Вид] = @P26 ТОГДА T7.Дата ИНАЧЕ NULL КОНЕЦ ИНАЧЕ CAST(NULL AS DATETIME) КОНЕЦ КОНЕЦ END ИЗ Т КАК T1 ЛЕВОЕ СОЕДИНЕНИЕ Документ.ПриходнаяНакладная T2 WITH(NOLOCK) ПО T1.СоставнойТип[Тип] = [ТипСсылка] И T1.СоставнойТип[Вид] = @P27 И T1.СоставнойТип_RRRef = T2.Ссылка ЛЕВОЕ СОЕДИНЕНИЕ Документ.РасходнаяНакладная T3 WITH(NOLOCK) ПО T1.СоставнойТип[Тип] = [ТипСсылка] И T1.СоставнойТип[Вид] = @P28 И T1.СоставнойТип_RRRef = T3.Ссылка ЛЕВОЕ СОЕДИНЕНИЕ Документ.ПриходнаяНакладная T4 WITH(NOLOCK) ПО T2.Основание[Тип] = [ТипСсылка] И T2.Основание[Вид] = @P29 И T2.Основание_RRRef = T4.Ссылка ЛЕВОЕ СОЕДИНЕНИЕ Документ.РасходнаяНакладная T5 WITH(NOLOCK) ПО T2.Основание[Тип] = [ТипСсылка] И T2.Основание[Вид] = @P30 И T2.Основание_RRRef = T5.Ссылка ЛЕВОЕ СОЕДИНЕНИЕ Документ.ПриходнаяНакладная T6 WITH(NOLOCK) ПО T3.Основание[Тип] = [ТипСсылка] И T3.Основание[Вид] = @P31 И T3.Основание_RRRef = T6.Ссылка ЛЕВОЕ СОЕДИНЕНИЕ Документ.РасходнаяНакладная T7 WITH(NOLOCK) ПО T3.Основание[Тип] = [ТипСсылка] И T3.Основание[Вид] = @P32 И T3.Основание_RRRef = T7.Ссылка Параметры: @P1 varbinary(4),@P2 varbinary(4),@P3 varbinary(4),@P4 varbinary(4),@P5 varbinary(4),@P6 varbinary(4),@P7 varbinary(4),@P8 varbinary(4), @P9 varbinary(4),@P10 varbinary(4),@P11 varbinary(4),@P12 varbinary(4),@P13 varbinary(4),@P14 varbinary(4),@P15 varbinary(4),@P16 varbinary(4), @P17 varbinary(4),@P18 varbinary(4),@P19 varbinary(4),@P20 varbinary(4),@P21 varbinary(4),@P22 varbinary(4),@P23 varbinary(4),@P24 varbinary(4), @P25 varbinary(4),@P26 varbinary(4),@P27 varbinary(4),@P28 varbinary(4),@P29 varbinary(4),@P30 varbinary(4),@P31 varbinary(4),@P32 varbinary(4) Значения параметров: Тип(Документ.ПриходнаяНакладная),Тип(Документ.РасходнаяНакладная),Тип(Документ.ПриходнаяНакладная),Тип(Документ.РасходнаяНакладная), Тип(Документ.РасходнаяНакладная),Тип(Документ.ПриходнаяНакладная),Тип(Документ.РасходнаяНакладная),Тип(Документ.ПриходнаяНакладная), Тип(Документ.РасходнаяНакладная),Тип(Документ.ПриходнаяНакладная),Тип(Документ.РасходнаяНакладная),Тип(Документ.РасходнаяНакладная), Тип(Документ.ПриходнаяНакладная),Тип(Документ.РасходнаяНакладная),Тип(Документ.РасходнаяНакладная),Тип(Документ.ПриходнаяНакладная), Тип(Документ.РасходнаяНакладная),Тип(Документ.ПриходнаяНакладная),Тип(Документ.РасходнаяНакладная),Тип(Документ.РасходнаяНакладная), Тип(Документ.ПриходнаяНакладная),Тип(Документ.ПриходнаяНакладная),Тип(Документ.РасходнаяНакладная),Тип(Документ.РасходнаяНакладная), Тип(Документ.ПриходнаяНакладная),Тип(Документ.РасходнаяНакладная),Тип(Документ.ПриходнаяНакладная),Тип(Документ.РасходнаяНакладная), Тип(Документ.ПриходнаяНакладная),Тип(Документ.РасходнаяНакладная),Тип(Документ.ПриходнаяНакладная),Тип(Документ.РасходнаяНакладная)
Напоминаю, наши составные типы и содержат-то СоставноеПоле
— 4 типа, а Основание
— 2. В типовых конфигурациях, особенно КА и УПП такие запросы очень-очень быстро приведут к ошибке, процитированной в самом начале статьи.
Следствие 11. Осторожно используйте
ВЫБОР
с составными типами. Не используйте вложенныеВЫБОР
с составными типами.
И, наконец
Следствие 12. Запрос
МИНИМУМ(...)
иМАКСИМУМ(...)
в сочетании сВЫБОР
и составными типами нежизнеспособен.
Зона риска, не зависящая от прикладного программиста
Даже если вы не создали ни одного поля составного типа, знайте, что всё равно есть риск обнаружить проблемы производительности и масштабируемости, связанные с составными типами. Кроме полей в которых явно указано "Составной тип данных" есть еще:
- Субконто в регистрах бухгалтерии
- Регистраторы регистров (сведений, накопления, бухгалтерии, расчетов), причем не забудьте и про таблицы изменений
- Регистры сведений с характеристиками, в том числе часто контактная информация
- Графы и ссылки журналов документов
- Таблицы последовательности документов
Вы в большинстве случаев не можете сделать так, чтобы в перечисленных случаях не было составных типов. Но можно выполнять следующие рекомендации и всегда обращать внимание на архитектурные особенности составных типов.
Как следует работать с составными типами в запросах?
Таблетка 0. Используйте составные типы в хранимых данных только по необходимости. Не добавляйте типы "про запас". Не используйте тип "любая ссылка" (и подобных) без особой на то необходимости. Если используете составной тип, то постарайтесь не смешивать ссылочные и простые типы.
Таблетка 1. При использовании RLS — исключите взаимодействие RLS с составными типами.
Таблетка 2. Работа с субконто должна быть организована "как в типовых", должны быть выполнены рекомендации с ИТС и на значения субконто не следует завязывать прикладную логику запросов.
Таблетка 3. Если используете составные типы в запросах, то не используйте их в условиях отбора или в условиях соединения, если нет уверенности, что это не повлияет на производительность. В спорных случаях следует смотреть технологический журнал или трассировку MS SQL Server, а не замеры времени на пустой базе.
Таблетка 4. Используйте ВЫРАЗИТЬ
для упрощения составных типов где только возможно. Но помните, что это не панацея и это может не дать использовать индексы в запросах
Таблетка 5. Используйте явные левые и внутренние соединения для получения реквизитов составного типа. Но учитывайте, что этот прием делает нечитаемыми запросы.
Таблетка 6. ИспользуйтеЕСТЬNULL()
для получения реквизитов составного типа:
ВЫБРАТЬ ЕСТЬNULL(ПриходнаяНакладная.Дата, РасходнаяНакладная.Дата) КАК ДатаРегистратора ИЗ РегистрНакопления.ОстаткиНоменклатуры КАК ОстаткиНоменклатуры ЛЕВОЕ СОЕДИНЕНИЕ Документ.ПриходнаяНакладная КАК ПриходнаяНакладная ПО ОстаткиНоменклатуры.Регистратор = ПриходнаяНакладная.Ссылка ЛЕВОЕ СОЕДИНЕНИЕ Документ.РасходнаяНакладная КАК РасходнаяНакладная ПО ОстаткиНоменклатуры.Регистратор = РасходнаяНакладная.Ссылка
Таблетка 7. По возможности не давайте пользователям в отчетах отбирать по составным типам, а тем более по реквизитам значений составных типов.
Таблетка 8. Не используйте сложные выражения с составными типами. Почти единственное допустимое место в запросе для составных полей — непосредственное возвращение их значений без обработки.
Можно дать еще много рекомендаций, но главная рекомендация: стараться состаные типы не использовать, а при использовании учитывать то, как они устроены.
Выводы
Составные типы с точки зрения производительности и масштабируемости — зло. Но это мощный и гибкий инструмент, который лежит в основе самой платформы. Функционально в платформе они реализованы разумно и даже местами элегантно, но они требуют внимательного и квалифицированного использования. Надеюсь, что этой статьёй я хоть кому-нибудь смогу помочь использовать их правильно.