Составные типы — бесплатный сыр мышеловки производительности
«…Еще одним важным решением в части работы с данными в «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. Не используйте сложные выражения с составными типами. Почти единственное допустимое место в запросе для составных полей — непосредственное возвращение их значений без обработки.
Можно дать еще много рекомендаций, но главная рекомендация: стараться состаные типы не использовать, а при использовании учитывать то, как они устроены.
Выводы
Составные типы с точки зрения производительности и масштабируемости — зло. Но это мощный и гибкий инструмент, который лежит в основе самой платформы. Функционально в платформе они реализованы разумно и даже местами элегантно, но они требуют внимательного и квалифицированного использования. Надеюсь, что этой статьёй я хоть кому-нибудь смогу помочь использовать их правильно.
Очень интересно. Некоторые вещи хочется перепроверить или уточнить. Например, почему
ВЫБРАТЬ РАЗЛИЧНЫЕ
Справочник1.СоставноеПоле КАК СоставноеПоле
ИЗ
Справочник.Справочник1 КАК Справочник1
Хочется подробностей про
Как в принципе может использоваться индекс в
Пока прочитал по диагонали. Возможно, будут еще вопросы.
(1)
1. Индекс не будет использоваться потому что нет нужного индекса:
ВЫБРАТЬ РАЗЛИЧНЫЕ
Справочник1.СоставноеПоле КАК СоставноеПоле
ИЗ
Справочник.Справочник1 КАК Справочник1
будет выполняться как
ВЫБРАТЬ РАЗЛИЧНЫЕ
Справочник1.СоставноеПоле.Тип КАК СоставноеПолеТип,
Справочник1.СоставноеПоле.Число КАК СоставноеПолеЧисло,
Справочник1.СоставноеПоле.Ссылка КАК СоставноеПолеСсылка
ИЗ
Справочник.Справочник1 КАК Справочник1
Отдельно есть индексы (Справочник1.СоставноеПоле.Тип, Справочник1.СоставноеПоле.Число) и (Справочник1.СоставноеПоле.Тип, Справочник1.СоставноеПоле.Ссылка). И, если бы это было поле простого типа, а не сотавного, то можно было бы расчитывать, что движок БД его подхватит, а для составного — не получается использовать.
(1)
2. Про «средства, которы бы позволили работать» — там рядом ссылки есть.
3. Мне если честно, казалось это очевидным. Попробую чуть подробнее. Самая простая аналогия для индекса — упорядоченный список статей с оглавлением. В такой структуре легко найти все значения, входящие в диапазон, а если этих значений очень много, то легко как минимум входящие в диапазон.
Поэтому запрос
ВЫБРАТЬ МИНИМУМ(ОстаткиНоменклатуры.Период)
ИЗ РегистрНакопления.ОстаткиНоменклатуры КАК ОстаткиНоменклатуры
ГДЕ ОстаткиНоменклатуры.Период>&Дата
просто выберет первую запись из тех, которые «>&Дата».
(2) А это практически проверялось? — То есть Вы смотрели план соответствующего запроса? Проводили замеры?
(3) 2. Ссылки я посмотрел, однако не совсем ясно как это практически использовать. Вычисляемые поля — что за функция? — Что-то вроде хеш-функции? — А коллизии? Фильтрованные индексы. В качестве фильтра — тип поля? Но там разве есть возможность в одном индексе задать несколько фильтров (по числу базовых типов + ссылка)? Разреженные столбцы — тут понятно. Индексы с включенными полями — опять непонятно использование по отношению к данной проблеме.
3. Одно время мне это тоже казалось очевидным. Но я не нашел конкретного практического подтверждения тому, что это действительно делается. Может быть, плохо смотрел.
(4)
1. Да, конечно, план смотрел, хотя тут и план не нужно смотреть: нет покрывающего индекса. Но для статьи каждый пример проверялся.
2.1. Вычисляемые поля не как хэш-функции, а как простое выделение из составного поля простого. Вычисляемые поля (которые можно было бы вычислять из одного varbinary) и их фильтрованное индексирование было бы хорошим решением. Чисто механически уменьшился бы объём индексом и увеличилось количество случаев их использования. Вечерком накидаю в SQL что я имею ввиду и приложу скрипт, чтобы было понятно. Но в любом случае это к 1С отношения не имеет. Серым шрифтом я пометил свое ворчание 🙂
2.2. Индексы с включенными полями — именно они могли бы ускорить запросы типа того «ВЫБРАТЬ РАЗЛИЧНЫЕ» по которым сейчас индексы не используются.
3. Сейчас приведу пример с планом.
(4) скрипт
Показать
результат (правая часть обрезана, чтобы поместилось нормально в форум):
Показать
Собственно, как и ожидалось.
Спасибо за труд!
1С действительно жертвует производительностью ради удобства разработки.
Рекомендация делать запросы маленькими и простыми, заиграла новыми красками.
Из статьи почерпнул для себя некоторые принципы, попробую практиковать.
Неоптимальные запросы иногда пугают, особенно когда видишь 100% загрузки каждого из ЦП на сервере СУБД.(часто такое вижу у коллег) Поседеть можно :).
Но всё же есть 2 момента:
1. Оптимизация должна делаться там, где она необходима; Считаю, что не стоит утруждать себя ковыряниями над профайлером, ради 90% ускорения запроса выполняемого раз в неделю в течении 30 секунд.
2. Встречался с проблемой составных типов только 1 раз в виде:
Microsoft OLE DB Provider for SQL Server: Too many table names in the query. The maximum allowable is 256.
(Это ограничение действовало на ms-sql 2000, по-моему)
Удалось обойти с помощью проверки типа ссылки в запросе и разбиением его на составляющие + временные.
Спасибо за статью.
«Многие знания, многие печали» — написано в книге Экклезиаста.
Все дело в объеме данных, если количество записей в таблице не велико и скорость выполнения запросов приемлема для пользователей, то можно и не обращать внимание на составные поля. И наоборот 🙂
В целом статья будет полезна как новичкам, так и разработчикам с опытом.
(7)
(Это ограничение действовало на ms-sql 2000, по-моему)
Почти верно. До 2005 включительно.
(8) Спасибо за отзыв. Не считаю небольшой объём и количество записей поводом писать кривой код. Ведь трудозатраты зачастую одинаковы.
(10) Главным ограничением всех процессов — является время. Можно написать супер код и опоздать.
(6) Спасибо, убедили… Видимо, запомнившаяся мне рекомендация использовать SELECT ТОР 1 … ORDER BY … вместо MIN в таких случаях относилась не к MS SQL или к другим типам индексов.
ТОже спасибо, поставила +
Я ещё только учусь и всегда интересно знать, что если запрос надо строить именно так, то почему))))
(0) Прочитал мельком. Нужны ли такие изыскания ?
Черт его знает, любая тема когда -нибудь становится актуальной.
Написано вроде бы «по-врослому». Вызывает уважение. +
Спасибо за полезное исследование с качественной подачей материала.
(6) Увидел в скрипте интересную рекурсивную CTE. Не могли бы ее прокомментировать: откуда-то взяли или сами придумали, почему считаете ее лучше чем простая CTE c 0 через id + 1 (на строчку короче и единицу не нужно будет отнимать). Сам использую подобные приемы, но в данном случае сомневаюсь в том, что будет быстрее. Замеры делали? За счет чего может быть выигрыш?
(17)По порядку
UPD: померял производительность с «одиночной» рекурсией — она примерно одинаковая.
Поставил бы пять плюсов и более, была бы возможность )
Отличное и доступное изложение, интересный и глубокий материал — спасибо )
Шурик, ты проделал огромную, а главное полезную работу. Причем ты все показал с профессиональной точки зрения, все грамотно разложил по полочкам. И кое-кого можно ткнуть носом теперь сюда ). Многие ситуации интуитивно и теоретически понимались, но ты провел отличное практическое исследование и описал проблему в целом. Причем не только показал минусы, но и указал как можно было бы исправить, как разработчикам, но и даже фирме 1С. Вообщем то, на infostart event можно было даже выступить с такой темой.
PS: пиши еще )))
(20) Да, я тоже считаю что такой материал был бы очень полезен на конференции.
(20) Я же и есть главный бенефициар статьи. 😉
(21) Мелковата тема для доклада. Если изложить в слайдах и рассказывать голосом, то это 5-10 минут от силы. При этом вся практическая ценность в разделе «Как следует работать…».
Ну на конференцию едут ведь не только прорекламировать свои продукты, но и обменяться опытом. неужели на конференции нет блока 10-15 минутных докладов для обмена опытом?
(23) Александр как всегда скромничает о своем опыте, методичности, глубине знаний и уровне проектов которые ведет 🙂
(24) Ну что ты тут наговариваешь? Опыт у меня маленький — на 3 года примерно меньше чем у тебя, по методичности и знаниям — vet7777 которому ты отвечаешь за мной вчера шлакокод переписывал, проектов я вообще не веду. Врёшь ты всё! 🙂
спасибо автору за предоставленную статью…., очень для меня познавательно
Классно! 🙂 Однозначно + ! Чувствуется уровень 1С:Эксперта! Но! Хотелось бы больше примеров по оптимизации кода. Уверен, такая информация пойдет в раздел «КАК ИЗНАЧАЛЬНО ПРОГРАММИРОВАТЬ ПРАВИЛЬНО». Дополнительно, напишите статью как получить псевдокод из кода запроса 1С. Конечно, тут потребуется раскрыть и понятия SQL, пусть хоть на начальном уровне. Думаю, получится отличный цикл статей про связку СУБД + 1C. 🙂
Спасибо за статью. Сил прочитать все не хватило, но возьму на заметку приемы в запросах, хотя я и так знал, что поля составного типа — проблемные в плане производительности.
Статья объемная, работа проделана большая, спасибо. Однако, все это уже есть на ИТС и давно разжевано-пережевано. Составные типы — удобная фича, но использоваться должна с умом.
(29) Evil Beaver,
its.1c.ru ?
а в каком разделе можно найти на ИТС, или может дадите ссылку на
Спасибо.
(30) headMade, честно говоря, нет времени искать. Поищите по словам «составной тип». Там где-то точно было черным-по-белому, как составной тип отображается в структуру таблиц СУБД и как нельзя делать с точки зрения производительности. В любом случае, текущая статья более чем исчерпывающая.
(29) Не соглашусь. На ИТС относительно небольшой объём информации. Структура хранения на ИТС есть. Индексы, которые строятся — есть. Общая рекомендация «получение данных через точку от полей составного типа» и то, как разыменуются поля значений составных типов — есть. Рекомендация не делать нессылочные субконто — где-то тоже кажется была. Есть еще замечание про ограничения индексов на составные типы (тоже в контексте субконто) и замечание про разницу NULL/Неопределено в контексте запросов и составных типов.
Зато нет:
Но в целом — да, если хоть иногда приходится смотреть ТЖ, трассу и планы запросов, то мегаоткровений тут нет. И подходы 1С к тому, как преобразовать язык запросов в SQL тоже предсказуемы, понятны и объяснимы. Собственно причины написания статьи изложены в (15) — мне просто нужно было консолидировать информацию по этой проблематике.
(32) да слов нет, Ваша статья — просто отличная. Имхо, ее можно было бы подсократить, т.к. несмотря на фундаментальность она сводится к известным тезисам:
1. Составной тип — часто источник проблем
2. Ссылочные типы вперемешку с примитивными — зло
3. Получение полей от ссылочного типа через точку — зло
4. Пункты 2+3 одновременно — аццкий сотона зохавал всех
5. Если в запросе присутствует составное поле — нужно смотреть, как оно отобразится на реальный запрос.
Итого, мне немного не хватило best practices в статье — как можно делать. Как нельзя, в-общем, и так было понятно. Тем не менее, прошу не воспринимать, как критику. Ни в коем случае. Это просто размышление вслух. Статья отличная, спасибо еще раз. Про потерю производительности по «СоставныеСсылки В (&СписокСсылок)» не знал, был уверен, что индекс гарантированно используется.
(33) в этих формулировках согласен на 146%
за ссылку отдельный +
«1с как продукт мысли» — звучит
(35) tango, я думаю, что не стоит придираться к хорошей маркетингово-популяризационной статье 2004 года. Тем более, что автор фактически не словом, но делом доказал, что они способны успешно развивать и улучшать продукт. Отдельное им спасибо за эволюционное, а не революционное развитие.
Отличная статья в тему про дырявые абстракцииhttp://russian.joelonsoftware.com/Articles/LeakyAbstractions.html (если вдруг кто ещё не читал)
Да, спасибо. Тоже далеко не всё знал насчёт использования (а точнее, неиспользования) индексов. Надеялся, что оно есть, а взаправду вот как. Отличная статья!
(37) У Спольского вообще много хороших статей. Но вот дырявые абстракции — они на самом деле лежат очень многослойно вокруг нас.
(39) ну Спольски вообще любит покрасоваться, мол, во какую я штуку всем открыл, абстракции, оказывается, построены друг на друге 😉 Одна мысль там в этой статье мне нравится, дословно не помню, смысл такой, что несмотря на все современные инструменты и абстракции я все равно обязан знать то, что написано у Кернигана&Ричи, иначе мне, как спецу, каюк. Это прям эпиграф к Вашей статье. Составной тип клево — но нужно знать, что он есть на самом деле
(40) Позволю себе ещё одну цитату: «Вычислительная техника — это дисциплина, в которой считается, что все проблемы можно решить благодаря введению одного или нескольких уровней косвенности.» © Деннис Де Брюле
То есть в нашем деле без абстракций вообще никуда. Абстракции удобны, иначе нам всем пришлось бы кодировать в машинных кодах на перфокартах. Просто нужно знать, что иногда они дают протечки. Профессионалы, зная как реализована та или иная абстракция, могут пользоваться ей эффективнее.
(40) Ну Спольскому с его особенностями простительна «любовь покрасоваться». А в 1С до фига и других дуршлагов-абстракций. Начиная со строк и арифметики:
Я об этом писал у себяв бложике
Реально, куда ни копни — герметичности этих абстракций нет. Ну да и ладно. Зато вентиляция 🙂
Полезная информация. Благодарю. Но зачем страх нагонять, используя обороты «ужас», «кошмар», «зло» в технической статье? Пойду застрелюсь…
А мне понравилось. Достаточно глубокая проработка и исследование, далеко не всегда доходят руки так глубоко лезть в скуль. Конечно, пока будешь приучать себя так писать запросы, производителность собственная упадет.
Есть только некие неточно сформулированные в голове противоречия для использования рекомендованных правил из статьи и рекомендаций по работе в СКД. Например, в СКД рекомендовано отборы оставлять для выбора пользователя. Очень часто, это как раз составной тип или будет при соединении. Ну и еще там есть.
Как я понимаю, для СКД все еще страшнее и ужаснее.
За статью просто твердущий плюс!
Статья во многом пересекается с приложением книги «Проф. разработка — реализация прикладных задач в системе 8.2», там с табличками и примерами расписаны особенности и неприятности составных типов (а также вообще хранение данных). Тут в статье более сумбурно и подробно мне показалось, в книге более структурировано и кратко.
(42) ЗаписьXML — это стрингбилдер? Аааааа ну точно же! Чорт, как все просто иногда оказывается! Мегареспект!!!!
Про числа не понял… меня особо не докучала никогда производительность численных операций. А что за задачи?
(42) Насчет
написано в статьеОпять двойка Там предлагается для решения проблемы
. Интересно теперь будет сравнить по быстродействию с решением через ЗаписьXML. Кстати, проблема А = А + «Ё» существует в большинстве языков. Смотрите, например,Строки в C# и .NET / Хабрахабр .
Все хорошо. Единственное замечание. Когда мне ради оптимизации предлагают заменить запрос в 3 строки на запрос в 30 строк, я останавливаюсь и начинаю думать 🙂 Когда через полгода я (или не дай Бог не я!) снова полезет в этот модуль, то количество МОЕГО драгоценного времени ( а не железного компьютерного), потраченного на понимание , чего тут творится, вырастет в N раз и резко сократит общий выигрыш от оптимизации (аж до минуса). Так что я буду продолжать использовать вариант Регистратор.Основание.Контрагент=&Контрагент.
(45) Да, 2-й том, страница 670 🙂 Но там не разобраны моменты с ВЫБОР и МАКСИМУМ, на грабли которых мы наступили. Если честно, то я пока долистываю первый том: на работе как-то не до чтения, домой купил недавно и дома больше 50-70 страниц в день не успеваю проработать.
(48) Пожалуйста-пожалуйста. Лишь бы это было осознанным выбором, и вы умели видеть и лечить свои же грабельки. У меня вот, например, стоимость месяца работы меньше стоимости пятиминутного простоя некоторых ИБ предприятия.
(47) Проверил. ЗаписьXML быстрее (для большого количества конкатенаций, конечно).
(50) Спасибо! — Буду иметь ввиду. Еще перепроверил, действительно, формирование строки из миллиона «Ы» занимает при помощи функции
примерно 2 секунды. Ближайший конкурент через представление массива при помощи функции
отстает на 12 процентов с результатом 2,5 секунды.
(42) Про ЗаписьXML я сам не догадался. Теперь буду применять. Спасибо!
Мир этому дому! Большое спасибо за серьезную статью. Информации много и она интересная, а тем более это не компиляция доков, а практически проверенно. Большой плюс.
Александр, огромное спасибо за статью! Теперь в голове расставилось все по полочкам. А применение на практике дало просто колоссальный результат (а кто бы мог подумать, где собака порылась). Как то раньше сильно надеялся на платформу, а оказывается… буду сильнее изучать эту тему. ОГРОМНОЕ СПАСИБО.
Спасибо огромное за статью. Прочитал как на духу. Написано очень подробно и особенно радуют примеры. Всегда радовался составным типам. А теперь вернулся на землю ) и уж точно буду использовать их впредь аккуратно.
Статья просто шикарная. На часть вещей просто глаза открылись. Даже не думал, что от составных типов может быть так плохо, хотя и зная о проблемах с производительностью составные типы практически не использую в своих разработках.
Автор молодец, узнал много интересного, написано очень грамотно, подробно и понятно даже любому пользователю.
Спасибо.
(56) uinx, а какая структура регистров? И как организовано «Номенклатура.Реквизиты.Сезонность»?
Хорошая грамотная статья, да только кто сначала будет разбираться в причинах резкого замедления исполнения запроса, когда «одно поле поменяли»? Скажут — «оборудование не тянет, давайте новое купим», ну или на крайний случай — 120 тыс рублей. Что интересно — когда на новом оборудовании запрос будет работать также(ну или чуть быстрее) вот тогда уже начинается «1С — г… » или там три конверта:), но чтобы профайлер открыть — «а что это»? Пример — недавно на мисте обсуждали какой-то запрос, что-то там запрос к виртуальным регистрам, с невъ… количеством джоинов по регистратору(здравствуйте, вычисляемые поля), так в профайлер в теме влезло два человека:).
Статья феноменальная. Огромное спасибо автору за труд, систематизацию и оформление. За отстутствие снобизма, за готовность отвечать на комментарии.
Завидую вашим коллегам, как прекрасно, когда есть у кого перенять опыт! А то сидишь единственным программистом на фикси, и в свой же говнокод и ткнуть некому…
А теперь вопрос нуба: как неправильный пример приводится запрос «Регистр.Регистратор.Дата>&Дата». Имеется в виду, что отбор по дате должен идти через Период соответствующего регистра? А если нужно именно сделать отбор по каким-то свойствам регистратора, то как это правильно оформить в запросе? Возможно, потребность такого отбора сигнализирует о неправильной архитектуре регистра, но допустим, что регистр менять мы не будем, а отбор в запросе реализовать надо.
(62) mikhailovaew:
1. Отбор замедлится по любым реквизитам поля составного типа. Дата или не дата — тут не важно
2. Архитектура, она обычно не в каменных скрижалях высечена и кривую архитектуру нужно менять. Пусть не за один день и не останавливая другие задачи, но менять придётся. Кроме изменения регистра есть еще как минимум прием — создание нового регистра.
3. Как костыльный вариант (один из многих):
Показать
Но потом править эту «прелесть» — проще застрелиться сразу. А когда таких запросов 500 в конфигурации?
(64) согласна, запрос душераздирающий.
спасибо за ответ )
(63) здравствуйте. Статья на самом деле классная. В общем очень радует эта тенденция, наконец появляются серьезные технические статьи.
Подскажите, где и вообще что можно читать в подобной тематике для начинающего программиста, который таки хочет стать хорошим программистом? ) есть ли вообще подобная литература/статьи, или единственный выход это исследовать самому?(ну помимо статей, которые есть на инфостарте)
(65) Вон, выше «проф. разработку» советуют — в ней вагон информации. Ну и в принципе — ИТС и книги 1С. Остальное — как-то так, самостоятельно.
(60) Ты написал :
«3.2. во временной таблице ПрайсЛист — нужен индекс по Номенклатура»
В запросе также используется индексирование временной таблицы «ФильтрПодразделения»
Вопрос : Зачем ?
Оптимизатор запроса сам создаст индекс.
Причем в моих опытах на временных таблицах значительного размера явное индексирование приводило
к некоторому замедлению выполнения запроса как в файловом варианте так и в клиент-серверном.
(67) Ish_2,
1. Оптимизатор индекс не строит. Никогда. В лучшем случае добавит операцию «Sort».
2. Но и операция Sort это ни разу не индекс на самом деле. Где-то сортированная выборка лучше обхода индексов (особенно для небольших таблиц), где-то хуже. Важно, что для каждого соединения таблиц скорее всего придётся серверу сортировать отдельной операцией с отдельными затратами.
3. Сервер совсем не всегда правильно догадывается о том, нужно сортировать или не нужно. Индекс на ВТ — «прозрачный намёк» (я намёки жены примерно также понимаю: когда мусорный пакет она мне даёт в руки, тогда я его и выношу).
4. И, да, индексирование не бесплатное. Но если утверждается, что у запроса план кривой и запрос долго выполняется, то для относительно мелкой ВТ можно и проиндексировать исходя из последующих сортировок/отборов/соединений — хуже скорее всего не станет.
5. За файловый ничего говорить не буду. Я в нём никто, и не представляю как там ВТ сделаны.
(68) «Оптимизатор индекс не строит. Никогда. В лучшем случае добавит операцию «Sort». »
Отлично. Рассматриваем клиент-сервер.
1. Я сделал запросом временную таблицу ВРЕМ1 из двух-трех полей с уникальным полем «Ключ» ( скажем, 1 000 000 записей ). Индекс не строю.
Аналогичным запросом получаю таблицу ВРЕМ2. Индекс для ВРЕМ2 также не строю.
Затем делаю соединение двух таблиц по полю «КЛЮЧ» и вывожу результат. Засекаю время TIME1.
2. Повторяю п1. Но для каждой из временных таблиц ВРЕМ1 и ВРЕМ2 указываю «ИНДЕКСИРОВАТЬ по КЛЮЧ».
Засекаю время TIME2.
Повторить эксперимент легко. Поэтому я утверждаю TIME1< TIME2 как минимум в два раза.
Если ты подтвердишь этот факт , то тогда вопросы :
1. почему явное индексирование временных таблиц приводит к резкому замедлению ?
2. кто же индексирует ВРЕМ1 и ВРЕМ2 перед соединением ?
На всякий случай реальный текст запроса для п.1.
В нем закомментирована опция «Индексировать по » .
ВРЕМ1 и ВРЕМ2 отсортированы по разным полям.
Показать
(71) По порядку. Рассматриваем запрос из (70).
В твоем эксперименте с 8.3. при отсутствии индексации ВТ ты получил выигрыш во времени очень
небольшой 15-20%. У меня же выигрыш по времени 100% ( т.е в 2 раза).
Причем , разумеется , у меня запуск для (70) проводился многократно , эксперимент проводился в разных организациях в разные годы на 8.2. Тут нужен кто-то третий кто бы нас рассудил.
Но мы сейчас оттолкнемся от результатов твоего эксперимента.
1. Согласен ли ты с тем , что для конкретных примеров (60) индексировать таблицы «ФильтрНоменклатура»
и «ФильтрПодразделения» и в (70) таблицы ВРЕМ1 и ВРЕМ2 НЕ нужно (причем без всяких оговорок) ?
2. К сожалению ( а может быть и к счастью), никогда не открывал профалер.
Тем не менее : кто же индексирует ВРЕМ1 и ВРЕМ2 в (70) перед соединением , если они не проиндексированы ?
Ты пишешь , что НИКТО.
Тогда совершенно непонятен механизм соединения двух таблиц в MSSQL без использования
индексов. Это как ?
Скажем, получение первой записи результирующей таблицы соединения происходит таким образом :
Берем первую запись ВРЕМ1 , тупо сканируем таблицу ВРЕМ2 по значению ВРЕМ1.Ссылка, находим , выводим.
И так далее для второй , третьей записи .. из ВРЕМ1.
Так ?
(72) Ish_2,
Настройка запроса — отличное место для начала. На русском. Про джойны, например, раздел «Дополнительные понятия настройки запросов». Если кратко: есть 3 вида джойнов (hash, merge, nested loops), в зависимости от ситуации оптимизатор использует каждый из них.
1. Согласен ли ты с тем , что для конкретных примеров (60) индексировать таблицы «ФильтрНоменклатура»
и «ФильтрПодразделения» и в (70) таблицы ВРЕМ1 и ВРЕМ2 НЕ нужно (причем без всяких оговорок) ?
Нет. Могу показать, но на выходных. Если «запрос тормозит» из-за некорректного плана, то индексирование ВТ вряд ли ухудшит его. В данном случае по (60) я вообще сразу написал, что без доп. данных всё это будет гаданием на кофейной гуще.
И еще раз обращу внимание. Сравнивать просто время — не вполне корректно.
2. К сожалению ( а может быть и к счастью), никогда не открывал профалер.
Ну профайлер я использовал только потому что терять время на запуск ТЖ (минута же!) было лень и показывает профайлер в целом больше, поэтому я его чаще использую. Время, запросы, планы можно было и из ТЖ взять. Профайлером, кстати, пользоваться очень просто. Могу научить 🙂
Тогда совершенно непонятен механизм соединения двух таблиц в MSSQL без использования индексов.
Если надо в большом индексированном (упорядоченном) списке найти мало записей, то nested loops («вложенные циклы»).
Если надо соединить 2 одинаково упорядоченных набора, то merge.
В большинстве других случаев это будет hash join, которых тоже есть несколько видов. Hash join целиком или частями, в памяти или не совсем в памяти строит хэш-таблицу одного из наборов, а вторым набором идёт по хешам.
Вариант Берем первую запись ВРЕМ1 , тупо сканируем таблицу ВРЕМ2 по значению ВРЕМ1.Ссылка, находим , выводим.
И так далее для второй , третьей записи .. из ВРЕМ1. будет использоваться для очень маленьких временных таблиц или если оптимизатор профакапится и примет решение использовать nested loops там, где нужен был hash join.
(73) Почему на выходных ? Не пойму .
Ты работаешь что ли ?
Оттолкнемся от факта. Ты и я убедились в том , что для конкретного примера (70) индексация ВТ не нужна.
Отсюда я делаю вывод , что индексация ВТ по ключевому полю НЕ нужна при условии, что в дальнейшем ВТ используется однократно ( фильтр или соединение).
Если ты с этим не согласен, то приведи контрпример (текст запроса) , чтобы я проверил и убедился
в обоснованности твоего несогласия.
За «Настройку запроса» спасибо. Как далеко ушла техника .. пока я не открывал профайлер.
Добавлено :
Текст в (70) исправил «по-твоему» : последний запрос поместил в таблицу ВРЕМ3.
И тогда TIME1 ( время без индексации) меньше TIME2 ( время с индексацией) в ~ 3 раза.
Спасибо автору, статья СУПЕР !!! Эт скока же запросов мне надо просмотреть ?!!! Самое обидное, буквально вчера я в отчете использовал составные типы через точку в условии 🙁 .
Мда… думаю я еще долго буду перечитывать статью и оптимизировать свои запросы.
Еще раз спасибо за статью !!!
(74) Ish_2, Посмотрел. В 8.1.15 и 8.2.16 индексы создаются отдельной командой CREATE INDEX, примерно так
Показать
Отсюда и 2 раза. В 8.3.3 и вроде в последних 8.2 — заранее создаётся индекс (и вставка тупит лишь минимально).
(76) Вот и выяснили расхождения в результатах.
Тем не менее , если представишь контрпример (74) будет совсем всё ясно.
P.S. Ну , на выходных , конечно.
(74) Да уж, посмотрел, как ВТ работают в 8.1.15 и 8.2.16, там действительно надо постараться для того, чтобы индексы заработали.
Итого:
1. Индексы создаются отдельной командой. После вставки. Это достаточно существенные затраты (для таблицы в 16 млн записей — создание индексов в 4-6 раз дороже самой вставки)
2. В 8.2.16 создаются некластеризованные индексы. С одной стороны это хорошо, создание индексов не требует перелопачивания страниц данных, с другой — если индекс содержит не все строки таблицы, то почти гарантированно будет достаточно дорогой RID Lookup. То есть в индекс ВТ нужно запихивать ВСЕ колонки, которые потом могут быть использованы в запросах.
В общем, на этих версиях однократное использование индексированных таблиц, наверное, заметной практической ценности не имеет. Пример, когда индекс всё-таки нужен для однократного использования я привожу ниже.
На свежих версиях этой проблемы вроде нет.
Запрос, который у меня и для однократного использования индекса показывает нужность индекса:
Показать
Суммарно с индексом у меня делалось около 65 сек, а без индексов — 88
(78) Релиз 8.2.18.61. Твой запрос (78) скопирован в КонсольЗапросов.
Время без индексирования 75 сек. ( сред.время 2 и 3 запуска)
Время с индексированием 170 сек ( сред.время 2 и 3 запуска).
Здесь всё понятно .
И мы понимаем , что время без индексирования меньше всего лишь из-за кривости релиза 8.2.
Но нас-то интересовал принцип ,
т.е. нужно было в принципе добиться чтобы время с индексированием было меньше.
И ты этого добился при использовании «некривого» 8.3.
Дело тут , надо думать, в сложности выражения соединения ( у тебя «Таб1.Знч МЕЖДУ Таб2.Знч И Таб2.Знч2»)
Здесь получаем , что преимущества индексированной таблицы становятся очевидными.
В (70) же использовалось «слишком» простое условие «Врем1.Ссылка = Врем2.Ссылка».
Ок. Спасибо !
(79) тест запускал на чем? В смысле: какая версия скуля и что там с памятью и дисками? Типа могу подогнать, теперь уже понятно как. Там вопрос не в сложности выражения, а в том, что без индексов «МЕЖДУ Таб2.Знч И Таб2.Знч2» приводит к жестокому соединению с обходом по декартову произведению, а с индексами лишь выгребания небольших диапазонов. Остаётся лишь подобрать размеры таблиц. Как несложно заметить, тут индексированная таблица достаточно жирная.
(80)
1. Сам я не очень-то .. Завтра спрошу у наших сисадминов.
2. «Там вопрос не в сложности выражения, а в том, что без индексов «МЕЖДУ Таб2.Знч И Таб2.Знч2″ приводит к жестокому соединению с обходом по декартову произведению, а с индексами лишь выгребания небольших диапазонов.»
Ну, да. Это и имелось ввиду.
В случае усложнения ( добавления новых условий) резко увеличивается объем пробега для этой самой «hash join».
«А осадочек остался…».
Дело в том , что я ни секунды не сомневался оптимизатор запроса сам строит необходимые индексы ( пусть даже некие «временные» , «виртуальные»)- если нужно ,после анализа условий соединения или фильтраций.
Фразу «не индексируйте временные таблицы при однократном последующем использовании» я помню «со школы».
Ведь еще при царе Горохе старые СУБД именно так создавали недостающие индексы — насколько помню.
В последнем твоем примере необходимость создания недостающего индекса также очевидна.
И тут на тебе : MSSQL этого не делает.
Неужто трудно проанализировать выражение соединения и создать индекс ?
А ORacle и прочие ?
(80)»тест запускал на чем? В смысле: какая версия скуля и что там с памятью и дисками? »
Кластер серверов MSSQL
ОС Windows Server 2008 R2 Enterprise
Процессор X5675 3.07 GHz (2 шт)
ОЗУ 32 Гб
СХД EMC ( я спросил про диски , а мне сказали , что СХД — это система хранения данных)
Microsoft SQL Server Standard Edition (64-bit) 10.50.1617.0
(82)
«»не индексируйте временные таблицы при однократном последующем использовании» я помню «со школы».»(с)
Игорь (Ish_2).
Помнить мало, надо еще понимать. 😉
Эта фраза относится к другому случаю — если «однократно» применяется запрос выборки записей из таблицы. Т.к. при индексировании выполняется полный просмотр всех записей таблицы. Плюс, затраты на построение самого индекса. А потом еще запрос будет просматривать N-ое количество записей. Т.е. быстрее будет работать сразу запрос без (вместо) предварительного индексирования.
Вроде, это так очевидно? Даже, я это смог понять… 😉
(84) Дело прошлое , догадки — дело скользкое. Кто там что понял ,а что — нет..
Пока ясно одно , MSSQL сам не создает недостающие индексы.
Вчера возмущенный этим фактом я написал в MS. Жду вот.
Думаю , к осени поправят.
Как считаете ?
(85)
«… к осени поправят … ?»(с)
Игорь (Ish_2).
Нет. Но, включат в планы для «MS SQL 2021».
Отличная статья, прочитал на одном дыхании
Отличная статья! Много интересного…
Это
замечательнаявеликолепная статья, я Вас недавно рекламировал!ух-ты какая шикарная статья!!!
я в шоке — она меня просто потрясла!!!
спасибо!
(49) Вот ты не пожалел своего времени, так как оно дешевле простоя предприятия и переписал по всей конфе в 100500 местах на «оптимальное» решение с ручным объединением всего и вся.
А потом кто-то сделал движение корректировкой записи регистров, про которую ты забыл. Или у этого регистра добавился регистратор. Какова вероятность, что ты поправишь не в 100500 местах, а в 100499, и предприятие не останавливаясь продолжит работать с неверными данными?
И не больше ли это убытков принесет?
На 8.3.5.1383 на файловой версии проверял размеры строк.
Заполнял таблицы под завязку, т.е. 4,2 Гб на внутренние таблицы.
Состав данных — 2 поля:
Поле1 (9 символов),
Поле2 (40 символов)
везде убрал индексирование (хотя к нашей таблице это не имеет значения) и полнотекстовый поиск.
Смотрел внутренние таблицы Tool_1CD.exe
В итоге результаты:
1) Справочник с реквизитами — Поле1, Поле2. Обе строки — переменной длины 4,265,631,632 байт — размер таблицы, 28,063,365 — кол-во элементов в справочнике, т.е. 152 байта на 1 элемент
По принципу, описываемому автором для строк
40x+у+9x+y=152 => 49x + 2y = 152
X = 2, y =(152-49*2)/2=27
X = 3, y =(152-49*3)/2=2.5
Думаю, правильно будет X = 3, y = 3 (?)
——
2) Для строк фиксированной длины результаты приблизительно те же. 4,265,631,600 байт — размер таблицы 28,437,543 — кол-во элементов, 150 Байт на 1 элемент
похоже тоже на x = 3, y = 0-3
——
3) Справочник с табличной частью — Поле1, Поле2. Строки переменной длины. 4,265,630,586 — размер таблицы, 33,854,210 — элементов, 126 Байт на 1 элемент
результаты уже другие!
49х+2y=126
X = 2, y = (126-49*2) /2=14
X = 3, y = (126-49*3)/2 = -10.5
Не понятно в общем, но похоже на 2 байта+y. И, скорее всего, нужно какое-то количество байт на всю строку
—-
Мои результаты:
на реквизиты с типом Строка приходится 3 байта на символ + 3,
на реквизиты в табличных частях с типом Строка, я не уверен в результатах, но, скорее всего,
это {2 байта на символ + y байт} (на реквизит) и какое-то количество байт на всю строку
(92) It-developer,
статье блоками по 4 КБ. Т.е. не может на странице храниться часть записи. Так что всё что мы знаем достоверно, что в 4 КБ влезает 27 записей (28 не влезает, 26 — остаётся лишнее). Т.е. запись может быть 146-152 байта.
Эммм. Извините, очень странное исследование.
Во-первых, данные хранятся, если верить
Во-вторых, есть служебные поля. Минимум для справочника _IDRRef (16 байт в SQL), _Marked (1 байт в SQL), _IsMetadata (1 байт в SQL), _Version (8 байт в SQL). Уточнить можно через ПолучитьСтруктуруХраненияБазыДанных. Если есть Код, Наименование, Группы — то это еще поля. Т.е. даже если вы вы код, наименование занулили и запретили иерархию, то 26 байт тут минимум дополнительных. Обязательно посмотрите для тестируемых таблиц ПолучитьСтруктуруХраненияБазыДанных — это же сразу покажет, какие там еще поля есть.
В-третьих, как именно хранится одна запись в файловой БД я не в курсе, если она хранится в типичном для 1С формате с фигурными скобками, запятыми, кавычками и т.п, то на это тоже нужно место (не менее 10 байт). Но тут я дуб, потому что файловые базы не использую (если честно, то и 1С я открываю сейчас раз-два в месяц, наверное).
В-четвёртых, по _IDRRef (как минимум) индекс будет ВСЕГДА (но это действительно не важно для вашего исследования).
Плюс по структуре файловой базы можно еще почитатьэтого товарища .
И я не понял причем тут составные поля, если вы приводите пример простых полей — статья в основном про особенности именно составных полей. И в моей статье в первом абзаце написано
В других СУБД структура схожа, но не до байтов, разумеется.
В общем, я не понял что исследовалось. Строки в 1С с двухбайтными буквами — это вряд ли где-то отличается. Дополнительные поля не учтены. Страницы не учтены. Дополнительные расходы из этих данных считать нельзя.
(93) Спасибо!
Я не учёл _IDRRef (16 байт в SQL), _Marked (1 байт в SQL), _Version (8 байт в SQL) 🙁
_IsMetadata (1 байт в SQL) там нет, но есть пустое _PredefenedID
В общем 16+1+8 = 25 байт.
Т.е. в итоге 2 байта на символ строки 🙂
Моя цель исследования была понять сколько можно запихнуть данных в файловую версию в справочник 🙂
В итоге недосчитался и поэтому написал на форуме 🙂
Понятно, что все в основном пользуются SQL, но бывают разные исключения.
К составным типам да, мои тесты отношения не имеют
Вообще мне Ваша статья понравилась. Я ранее ее использовал в работе
(94) It-developer,
такой статьи, по которой можно достаточно точно определить сайзинг.
Ну тогда всегда пожалуйста. Жаль, конечно, что для 1С нет аналога
(96) Sergey.Noskov,
Ну дай дураку рекомендацию, он и руки порежет и рекомендацию извратит.