Составные типы — бесплатный сыр мышеловки производительности

Как работают запросы с составными типами, почему неаккуратное использование составных типов данных может снизить производительность,  и как этого избежать.

Составные типы — бесплатный сыр мышеловки производительности

«…Еще одним важным решением в части работы с данными в «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 КОНЕЦ
КОНЕЦ

Здесь есть следующие негативные моменты:

  1. Индексы опять не имеют шансов на использование (опять сканы, опять блокировки и прочие радости)
  2. Если СоставноеПоле— не поле, а вычисляемое выражение, то оно будет подставлено в вычисления несколько раз. Это очень быстро увеличивает сложность запроса.
  3. Вычисляется выражение ПОДСТРОКА(МИНИМУМ(T.СоставноеПоле[ВИД] + T.СоставноеПоле[ССЫЛКА]),...,...) — это может спровоцировать операцию Table Spool в плане запроса, да и вообще получается достаточно затратная операция.

Следствие 10. Не используйте агрегатные функции МИНИМУМ(…) и МАКСИМУМ(…) к составным типам.

Рекомендация: если нужно получить одно значение (без группировок), то вместо

ВЫБРАТЬ
МИНИМУМ(Т.СоставноеПоле) КАК СоставноеПоле
ИЗ
Т КАК Т

можно использовать следующий запрос

ВЫБРАТЬ ПЕРВЫЕ 1
Т.СоставноеПоле КАК СоставноеПоле
ИЗ
Т КАК Т

УПОРЯДОЧИТЬ ПО
СоставноеПоле ВОЗР

В этом случае индексы могут использоваться, хотя и не всегда. Да и выражение для вычисления значительно проще.

Хотелось бы, конечно, чтобы для вычисления МИНИМУМ и МАКСИМУМ, а также для срезов последних фирма 1С в СУБД Oracle и MS SQL 2012 использовала аналитические функции типа FIRST_VALUE и LAST_VALUE, но это уже скорее ворчание.

Выражения ВЫБОР КОГДА … ТОГДА … ИНАЧЕ … КОНЕЦ

Выражение ВЫБОР следует использовать очень осторожно, если в нём используются составные типы. Основной эффект состоит в том, что такое выражение быстро усложняется и обрастает неочевидными вычислениями. Начнем с простых случаев.

Запрос:

ВЫБРАТЬ
ВЫБОР
КОГДА Т.ТипПоля = "Документ.ПриходнаяНакладная"
ТОГДА Т.СоставноеПоле
КОНЕЦ КАК Поле1
ИЗ
Т КАК Т

Этот запрос будет выполняться как

ВЫБРАТЬ
ВЫБОР
КОГДА Т.ТипПоля = "Документ.ПриходнаяНакладная"
ТОГДА Т.СоставноеПоле[Тип]
КОНЕЦ КАК Поле1[Тип],
ВЫБОР
КОГДА Т.ТипПоля = "Документ.ПриходнаяНакладная"
ТОГДА Т.СоставноеПоле[Число]
КОНЕЦ КАК Поле1[Число],
ВЫБОР
КОГДА Т.ТипПоля = "Документ.ПриходнаяНакладная"
ТОГДА Т.СоставноеПоле[Строка]
КОНЕЦ КАК Поле1[Строка],
ВЫБОР
КОГДА Т.ТипПоля = "Документ.ПриходнаяНакладная"
ТОГДА Т.СоставноеПоле[Вид]
КОНЕЦ КАК Поле1[Вид],
ВЫБОР
КОГДА Т.ТипПоля = "Документ.ПриходнаяНакладная"
ТОГДА Т.СоставноеПоле[Ссылка]
КОНЕЦ КАК Поле1[Ссылка],
ИЗ
Т КАК Т

Обратите внимание:

  1. Выражение условия КОГДАчестно размножилось по полям. Если оно будет сложнее, то это может повлиять на производительность. Особенно аккуратно надо относиться к условиям «В (…)»: такие условия могут содержать соединения.
  2. Тип никак не ограничился (и даже наоборот, мог расшириться, если бы частей КОГДА...ТОГДА в выражении было несколько)

Из второго момента явно следует, что следующая попытка оптимизации запроса только ухудшит запрос:

ВЫБРАТЬ
ВЫБОР
КОГДА Т.СоставноеПоле ССЫЛКА Документ.ПриходнаяНакладная
ТОГДА Т.СоставноеПоле
КОНЕЦ КАК Поле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. Запрос МИНИМУМ(...) и МАКСИМУМ(...) в сочетании с ВЫБОР и составными типами нежизнеспособен.

Зона риска, не зависящая от прикладного программиста

Даже если вы не создали ни одного поля составного типа, знайте, что всё равно есть риск обнаружить проблемы производительности и масштабируемости, связанные с составными типами. Кроме полей в которых явно указано «Составной тип данных» есть еще:

  1. Субконто в регистрах бухгалтерии
  2. Регистраторы регистров (сведений, накопления, бухгалтерии, расчетов), причем не забудьте и про таблицы изменений
  3. Регистры сведений с характеристиками, в том числе часто контактная информация
  4. Графы и ссылки журналов документов
  5. Таблицы последовательности документов

Вы в большинстве случаев не можете сделать так, чтобы в перечисленных случаях не было составных типов. Но можно выполнять следующие рекомендации и всегда обращать внимание на архитектурные особенности составных типов.

Как следует работать с составными типами в запросах?

Таблетка 0. Используйте составные типы в хранимых данных только по необходимости. Не добавляйте типы «про запас». Не используйте тип «любая ссылка» (и подобных) без особой на то необходимости. Если используете составной тип, то постарайтесь не смешивать ссылочные и простые типы.

Таблетка 1. При использовании RLS — исключите взаимодействие RLS с составными типами.

Таблетка 2. Работа с субконто должна быть организована «как в типовых», должны быть выполнены рекомендации с ИТС и на значения субконто не следует завязывать прикладную логику запросов.

Таблетка 3. Если используете составные типы в запросах, то не используйте их в условиях отбора или в условиях соединения, если нет уверенности, что это не повлияет на производительность. В спорных случаях следует смотреть технологический журнал или трассировку MS SQL Server, а не замеры времени на пустой базе.

Таблетка 4. Используйте ВЫРАЗИТЬ для упрощения составных типов где только возможно. Но помните, что это не панацея и это может не дать использовать индексы в запросах

Таблетка 5. Используйте явные левые и внутренние соединения для получения реквизитов составного типа. Но учитывайте, что этот прием делает нечитаемыми запросы.

Таблетка 6. Используйте ЕСТЬNULL() для получения реквизитов составного типа:

ВЫБРАТЬ
ЕСТЬNULL(ПриходнаяНакладная.Дата, РасходнаяНакладная.Дата) КАК ДатаРегистратора
ИЗ
РегистрНакопления.ОстаткиНоменклатуры КАК ОстаткиНоменклатуры
ЛЕВОЕ СОЕДИНЕНИЕ Документ.ПриходнаяНакладная КАК ПриходнаяНакладная
ПО ОстаткиНоменклатуры.Регистратор = ПриходнаяНакладная.Ссылка
ЛЕВОЕ СОЕДИНЕНИЕ Документ.РасходнаяНакладная КАК РасходнаяНакладная
ПО ОстаткиНоменклатуры.Регистратор = РасходнаяНакладная.Ссылка

Таблетка 7. По возможности не давайте пользователям в отчетах отбирать по составным типам, а тем более по реквизитам значений составных типов.

Таблетка 8. Не используйте сложные выражения с составными типами. Почти единственное допустимое место в запросе для составных полей — непосредственное возвращение их значений без обработки.

Можно дать еще много рекомендаций, но главная рекомендация: стараться состаные типы не использовать, а при использовании учитывать то, как они устроены.

Выводы

Составные типы с точки зрения производительности и масштабируемости — зло. Но это мощный и гибкий инструмент, который лежит в основе самой платформы. Функционально в платформе они реализованы разумно и даже местами элегантно, но они требуют внимательного и квалифицированного использования. Надеюсь, что этой статьёй я хоть кому-нибудь смогу помочь использовать их правильно.

96 Comments

  1. ildarovich

    Очень интересно. Некоторые вещи хочется перепроверить или уточнить. Например, почему

    индексы по такому составному полю не могут использоваться и в более простом запросе:

    ВЫБРАТЬ РАЗЛИЧНЫЕ

    Справочник1.СоставноеПоле КАК СоставноеПоле

    ИЗ

    Справочник.Справочник1 КАК Справочник1

    Хочется подробностей про

    В принципе в современных СУБД давно есть средства, которы бы позволили работать с такими типами эффективно

    Как в принципе может использоваться индекс в

    Агрегатные функции — МИНИМУМ(…) и МАКСИМУМ(…)

    Пока прочитал по диагонали. Возможно, будут еще вопросы.

    Reply
  2. speshuric

    (1)

    1. Индекс не будет использоваться потому что нет нужного индекса:

    ВЫБРАТЬ РАЗЛИЧНЫЕ

    Справочник1.СоставноеПоле КАК СоставноеПоле

    ИЗ

    Справочник.Справочник1 КАК Справочник1



    будет выполняться как

    ВЫБРАТЬ РАЗЛИЧНЫЕ

    Справочник1.СоставноеПоле.Тип КАК СоставноеПолеТип,

    Справочник1.СоставноеПоле.Число КАК СоставноеПолеЧисло,

    Справочник1.СоставноеПоле.Ссылка КАК СоставноеПолеСсылка

    ИЗ

    Справочник.Справочник1 КАК Справочник1



    Отдельно есть индексы (Справочник1.СоставноеПоле.Тип, Справочник1.СоставноеПоле.Число) и (Справочник1.СоставноеПоле.Тип, Справочник1.СоставноеПоле.Ссылка). И, если бы это было поле простого типа, а не сотавного, то можно было бы расчитывать, что движок БД его подхватит, а для составного — не получается использовать.

    Reply
  3. speshuric

    (1)

    2. Про «средства, которы бы позволили работать» — там рядом ссылки есть.

    3. Мне если честно, казалось это очевидным. Попробую чуть подробнее. Самая простая аналогия для индекса — упорядоченный список статей с оглавлением. В такой структуре легко найти все значения, входящие в диапазон, а если этих значений очень много, то легко как минимум входящие в диапазон.

    Поэтому запрос

    ВЫБРАТЬ МИНИМУМ(ОстаткиНоменклатуры.Период)

    ИЗ РегистрНакопления.ОстаткиНоменклатуры КАК ОстаткиНоменклатуры

    ГДЕ ОстаткиНоменклатуры.Период>&Дата



    просто выберет первую запись из тех, которые «>&Дата».

    Reply
  4. ildarovich

    (2) А это практически проверялось? — То есть Вы смотрели план соответствующего запроса? Проводили замеры?

    (3) 2. Ссылки я посмотрел, однако не совсем ясно как это практически использовать. Вычисляемые поля — что за функция? — Что-то вроде хеш-функции? — А коллизии? Фильтрованные индексы. В качестве фильтра — тип поля? Но там разве есть возможность в одном индексе задать несколько фильтров (по числу базовых типов + ссылка)? Разреженные столбцы — тут понятно. Индексы с включенными полями — опять непонятно использование по отношению к данной проблеме.

    3. Одно время мне это тоже казалось очевидным. Но я не нашел конкретного практического подтверждения тому, что это действительно делается. Может быть, плохо смотрел.

    Reply
  5. speshuric

    (4)

    1. Да, конечно, план смотрел, хотя тут и план не нужно смотреть: нет покрывающего индекса. Но для статьи каждый пример проверялся.

    2.1. Вычисляемые поля не как хэш-функции, а как простое выделение из составного поля простого. Вычисляемые поля (которые можно было бы вычислять из одного varbinary) и их фильтрованное индексирование было бы хорошим решением. Чисто механически уменьшился бы объём индексом и увеличилось количество случаев их использования. Вечерком накидаю в SQL что я имею ввиду и приложу скрипт, чтобы было понятно. Но в любом случае это к 1С отношения не имеет. Серым шрифтом я пометил свое ворчание 🙂

    2.2. Индексы с включенными полями — именно они могли бы ускорить запросы типа того «ВЫБРАТЬ РАЗЛИЧНЫЕ» по которым сейчас индексы не используются.

    3. Сейчас приведу пример с планом.

    Reply
  6. speshuric

    (4) скрипт

    set nocount on;
    declare @start bigint, @len bigint, @seed bigint;
    set @start = 0;
    set @len = 1000000;
    set @seed = 2;
    create table #tmp
    (
    id bigint not null primary key,
    somedata nvarchar(100) not null
    );
    with recursive_builder(id) as
    (select 1 id — это просто генерация списка интов
    union all select 2*r.id + 1 from recursive_builder r where 2*r.id + 1 <= @len
    union all select 2*r.id from recursive_builder r where 2*r.id <= @len)
    insert #tmp (id, somedata) select @start + (id-1)*@seed, NEWID() from recursive_builder r;
    declare @search_from bigint;
    set @search_from = 1000000;
    set statistics profile on;
    select min(id) from #tmp where id>@search_from;
    set statistics profile off;
    drop table #tmp;
    set nocount off;

    Показать

    результат (правая часть обрезана, чтобы поместилось нормально в форум):

    ———————
    1000002
    
    Rows                 Executes             StmtText                                                                                                                                 ……
    ——————— ——————— —————————————————————————————————————————————- ……
    1                    1                    select min(id) from #tmp where id>@search_from;                                                                                          ……
    1                    1                      |—Stream Aggregate(DEFINE:([Expr1003]=MIN([tempdb].[dbo].[#tmp].[id])))                                                               ……
    1                    1                           |—Top(TOP EXPRESSION:((1)))                                                                                                      ……
    1                    1                                |—Clustered Index Seek(OBJECT:([tempdb].[dbo].[#tmp]), SEEK:([tempdb].[dbo].[#tmp].[id] > [@search_from]) ORDERED FORWARD)  ……
    
    

    Показать

    Собственно, как и ожидалось.

    Reply
  7. Jogeedae

    Спасибо за труд!

    1С действительно жертвует производительностью ради удобства разработки.

    Рекомендация делать запросы маленькими и простыми, заиграла новыми красками.

    Из статьи почерпнул для себя некоторые принципы, попробую практиковать.

    Неоптимальные запросы иногда пугают, особенно когда видишь 100% загрузки каждого из ЦП на сервере СУБД.(часто такое вижу у коллег) Поседеть можно :).

    Но всё же есть 2 момента:

    1. Оптимизация должна делаться там, где она необходима; Считаю, что не стоит утруждать себя ковыряниями над профайлером, ради 90% ускорения запроса выполняемого раз в неделю в течении 30 секунд.

    2. Встречался с проблемой составных типов только 1 раз в виде:

    Ошибка выполнения запроса: Построенный запрос к СУБД использует слишком много таблиц. Допустимо не более 256.

    Microsoft OLE DB Provider for SQL Server: Too many table names in the query. The maximum allowable is 256.

    (Это ограничение действовало на ms-sql 2000, по-моему)

    Удалось обойти с помощью проверки типа ссылки в запросе и разбиением его на составляющие + временные.

    Reply
  8. ILM

    Спасибо за статью.

    «Многие знания, многие печали» — написано в книге Экклезиаста.

    Все дело в объеме данных, если количество записей в таблице не велико и скорость выполнения запросов приемлема для пользователей, то можно и не обращать внимание на составные поля. И наоборот 🙂

    В целом статья будет полезна как новичкам, так и разработчикам с опытом.

    Reply
  9. speshuric

    (7)

    (Это ограничение действовало на ms-sql 2000, по-моему)

    Почти верно. До 2005 включительно.

    Reply
  10. speshuric

    (8) Спасибо за отзыв. Не считаю небольшой объём и количество записей поводом писать кривой код. Ведь трудозатраты зачастую одинаковы.

    Reply
  11. ILM

    (10) Главным ограничением всех процессов — является время. Можно написать супер код и опоздать.

    Reply
  12. ildarovich

    (6) Спасибо, убедили… Видимо, запомнившаяся мне рекомендация использовать SELECT ТОР 1 … ORDER BY … вместо MIN в таких случаях относилась не к MS SQL или к другим типам индексов.

    Reply
  13. Bukaska

    ТОже спасибо, поставила +

    Я ещё только учусь и всегда интересно знать, что если запрос надо строить именно так, то почему))))

    Reply
  14. Ish_2

    (0) Прочитал мельком. Нужны ли такие изыскания ?

    Черт его знает, любая тема когда -нибудь становится актуальной.

    Написано вроде бы «по-врослому». Вызывает уважение. +

    Reply
  15. speshuric
    Reply
  16. tormozit

    Спасибо за полезное исследование с качественной подачей материала.

    Reply
  17. ildarovich

    (6) Увидел в скрипте интересную рекурсивную CTE. Не могли бы ее прокомментировать: откуда-то взяли или сами придумали, почему считаете ее лучше чем простая CTE c 0 через id + 1 (на строчку короче и единицу не нужно будет отнимать). Сам использую подобные приемы, но в данном случае сомневаюсь в том, что будет быстрее. Замеры делали? За счет чего может быть выигрыш?

    Reply
  18. speshuric

    (17)По порядку

    • Эту, cte кажется сам придумал, но не со своей идеей.
    • В этой cte мне нравится, что она «медленно» уходит в рекурсию. Обычно ставят OPTION (MAXRECURSION 0) (как здесь). В моём случае глубина рекурсии растет как логарифм, а соответственно значения по умолчанию (100 вложений) хватит и на bigint.
    • Производительность сравнивал, но не помню точно. Кажется мой вариант шустрый, но если я правильно помню, всё равно декартово произведение таблиц с «заготовкой» от 0 до «много» и с кластеризованным индексом рвёт остальные варианты, особенно если правильно прописать условия для использования индексов (оно получается громоздким).

    UPD: померял производительность с «одиночной» рекурсией — она примерно одинаковая.

    Reply
  19. AlX0id

    Поставил бы пять плюсов и более, была бы возможность )

    Отличное и доступное изложение, интересный и глубокий материал — спасибо )

    Reply
  20. vet7777

    Шурик, ты проделал огромную, а главное полезную работу. Причем ты все показал с профессиональной точки зрения, все грамотно разложил по полочкам. И кое-кого можно ткнуть носом теперь сюда ). Многие ситуации интуитивно и теоретически понимались, но ты провел отличное практическое исследование и описал проблему в целом. Причем не только показал минусы, но и указал как можно было бы исправить, как разработчикам, но и даже фирме 1С. Вообщем то, на infostart event можно было даже выступить с такой темой.

    PS: пиши еще )))

    Reply
  21. tormozit

    (20) Да, я тоже считаю что такой материал был бы очень полезен на конференции.

    Reply
  22. speshuric

    (20) Я же и есть главный бенефициар статьи. 😉

    (21) Мелковата тема для доклада. Если изложить в слайдах и рассказывать голосом, то это 5-10 минут от силы. При этом вся практическая ценность в разделе «Как следует работать…».

    Reply
  23. vet7777

    Ну на конференцию едут ведь не только прорекламировать свои продукты, но и обменяться опытом. неужели на конференции нет блока 10-15 минутных докладов для обмена опытом?

    Reply
  24. apatyukov

    (23) Александр как всегда скромничает о своем опыте, методичности, глубине знаний и уровне проектов которые ведет 🙂

    Reply
  25. speshuric

    (24) Ну что ты тут наговариваешь? Опыт у меня маленький — на 3 года примерно меньше чем у тебя, по методичности и знаниям — vet7777 которому ты отвечаешь за мной вчера шлакокод переписывал, проектов я вообще не веду. Врёшь ты всё! 🙂

    Reply
  26. TrinitronOTV

    спасибо автору за предоставленную статью…., очень для меня познавательно

    Reply
  27. DoctorRoza

    Классно! 🙂 Однозначно + ! Чувствуется уровень 1С:Эксперта! Но! Хотелось бы больше примеров по оптимизации кода. Уверен, такая информация пойдет в раздел «КАК ИЗНАЧАЛЬНО ПРОГРАММИРОВАТЬ ПРАВИЛЬНО». Дополнительно, напишите статью как получить псевдокод из кода запроса 1С. Конечно, тут потребуется раскрыть и понятия SQL, пусть хоть на начальном уровне. Думаю, получится отличный цикл статей про связку СУБД + 1C. 🙂

    Reply
  28. sitec-it

    Спасибо за статью. Сил прочитать все не хватило, но возьму на заметку приемы в запросах, хотя я и так знал, что поля составного типа — проблемные в плане производительности.

    Reply
  29. Evil Beaver

    Статья объемная, работа проделана большая, спасибо. Однако, все это уже есть на ИТС и давно разжевано-пережевано. Составные типы — удобная фича, но использоваться должна с умом.

    Reply
  30. headMade

    (29) Evil Beaver,

    а в каком разделе можно найти на ИТС, или может дадите ссылку на its.1c.ru ?

    Спасибо.

    Reply
  31. Evil Beaver

    (30) headMade, честно говоря, нет времени искать. Поищите по словам «составной тип». Там где-то точно было черным-по-белому, как составной тип отображается в структуру таблиц СУБД и как нельзя делать с точки зрения производительности. В любом случае, текущая статья более чем исчерпывающая.

    Reply
  32. speshuric

    (29) Не соглашусь. На ИТС относительно небольшой объём информации. Структура хранения на ИТС есть. Индексы, которые строятся — есть. Общая рекомендация «получение данных через точку от полей составного типа» и то, как разыменуются поля значений составных типов — есть. Рекомендация не делать нессылочные субконто — где-то тоже кажется была. Есть еще замечание про ограничения индексов на составные типы (тоже в контексте субконто) и замечание про разницу NULL/Неопределено в контексте запросов и составных типов.

    Зато нет:

    • Дополнительная существенноа деградация, если «получение данных через точку от полей составного типа» используется в ГДЕ или ПО. Сколько раз я переубеждал программистов, которые утверждали что «Регистр.Регистратор.Дата>&Дата» должен использовать индексы.
    • Почти полная бесполезность индексирования составных типов, содержащих несколько базовых (фактически индекс работает только для «ГДЕ СоставноеПоле = &Параметр»)
    • Неочевидная потеря производительности на «В (&Список)»
    • Тема того, как используется «ВЫБОР КОГДА ТОГДА» и МИНИМУМ/МАКСИМУМ
    • Почти нет приемов, как обходить проблемы производительности (за исключением ВЫРАЗИТЬ, которое, кстати, не всегда работает)

    Но в целом — да, если хоть иногда приходится смотреть ТЖ, трассу и планы запросов, то мегаоткровений тут нет. И подходы 1С к тому, как преобразовать язык запросов в SQL тоже предсказуемы, понятны и объяснимы. Собственно причины написания статьи изложены в (15) — мне просто нужно было консолидировать информацию по этой проблематике.

    Reply
  33. Evil Beaver

    (32) да слов нет, Ваша статья — просто отличная. Имхо, ее можно было бы подсократить, т.к. несмотря на фундаментальность она сводится к известным тезисам:

    1. Составной тип — часто источник проблем

    2. Ссылочные типы вперемешку с примитивными — зло

    3. Получение полей от ссылочного типа через точку — зло

    4. Пункты 2+3 одновременно — аццкий сотона зохавал всех

    5. Если в запросе присутствует составное поле — нужно смотреть, как оно отобразится на реальный запрос.

    Итого, мне немного не хватило best practices в статье — как можно делать. Как нельзя, в-общем, и так было понятно. Тем не менее, прошу не воспринимать, как критику. Ни в коем случае. Это просто размышление вслух. Статья отличная, спасибо еще раз. Про потерю производительности по «СоставныеСсылки В (&СписокСсылок)» не знал, был уверен, что индекс гарантированно используется.

    Reply
  34. speshuric

    (33) в этих формулировках согласен на 146%

    Reply
  35. tango

    за ссылку отдельный +

    «1с как продукт мысли» — звучит

    В этой статье мы решили сконцентрироваться не на описании возможностей продукта «1С:Предприятия», а именно на технологических инновациях, которые в совокупности определили ряд совершенно новых технологий разработки бизнес-приложений и новых качеств самих этих приложений.
    При этом до идеала еще далеко — все имеющиеся и создаваемые технологии являются компромиссными решениями, нацеленными на улучшение показателей систем по нескольким указанным критериям.
    Reply
  36. speshuric

    (35) tango, я думаю, что не стоит придираться к хорошей маркетингово-популяризационной статье 2004 года. Тем более, что автор фактически не словом, но делом доказал, что они способны успешно развивать и улучшать продукт. Отдельное им спасибо за эволюционное, а не революционное развитие.

    Reply
  37. vlad.frost

    Отличная статья в тему про дырявые абстракции http://russian.joelonsoftware.com/Articles/LeakyAbstractions.html (если вдруг кто ещё не читал)

    Reply
  38. Yashazz

    Да, спасибо. Тоже далеко не всё знал насчёт использования (а точнее, неиспользования) индексов. Надеялся, что оно есть, а взаправду вот как. Отличная статья!

    Reply
  39. speshuric

    (37) У Спольского вообще много хороших статей. Но вот дырявые абстракции — они на самом деле лежат очень многослойно вокруг нас.

    Reply
  40. Evil Beaver

    (39) ну Спольски вообще любит покрасоваться, мол, во какую я штуку всем открыл, абстракции, оказывается, построены друг на друге 😉 Одна мысль там в этой статье мне нравится, дословно не помню, смысл такой, что несмотря на все современные инструменты и абстракции я все равно обязан знать то, что написано у Кернигана&Ричи, иначе мне, как спецу, каюк. Это прям эпиграф к Вашей статье. Составной тип клево — но нужно знать, что он есть на самом деле

    Reply
  41. vlad.frost

    (40) Позволю себе ещё одну цитату: «Вычислительная техника — это дисциплина, в которой считается, что все проблемы можно решить благодаря введению одного или нескольких уровней косвенности.» © Деннис Де Брюле

    То есть в нашем деле без абстракций вообще никуда. Абстракции удобны, иначе нам всем пришлось бы кодировать в машинных кодах на перфокартах. Просто нужно знать, что иногда они дают протечки. Профессионалы, зная как реализована та или иная абстракция, могут пользоваться ей эффективнее.

    Reply
  42. speshuric

    (40) Ну Спольскому с его особенностями простительна «любовь покрасоваться». А в 1С до фига и других дуршлагов-абстракций. Начиная со строк и арифметики:

    • Арифметика пытается усидеть на 2 стульях (точные значения и плавающая точка). В результате производительность нигде, артефакты на интеграции так и прут, и СУБД про такую арифметику ваще не в курсе. Когда я вижу что браузерs в JavaScript фигачат до 500 млн операций с числами в секунду, то вспоминая 1С прям аж… Прям аж…
    • Строки. Я не буду вспоминать эпопею с преобразованиями crlf. Я промолчу про то, что интерпретатор 1С считает буквами, а что нет (вопрос, кстати, не так тривиален 🙂 ). Давайте лучше про конкатенацию. А = А + «Ы» — что может быть проще. Пока этих выражений не 100500. Возникает глупая ситуация, когда для большого запроса к внешней системе основное время уходит на создание запроса, а не на выполнение или разбор. StringBuilder отсутствует. ТекстовыйДокумент не спасает. Зато спасает (кто бы мог подумать) ЗаписьXML:

      ТекстПостроитель = Новый ЗаписьXML;
      ТекстПостроитель.УстановитьСтроку();
      ТекстПостроитель.ЗаписатьБезОбработки(«Кусочек строки»);
      ТекстПостроитель.ЗаписатьБезОбработки(«, и еще один»);
      СтрокаРезультат = ТекстПостроитель.Закрыть();

      Я об этом писал у себя в бложике

    Реально, куда ни копни — герметичности этих абстракций нет. Ну да и ладно. Зато вентиляция 🙂

    Reply
  43. scape

    Полезная информация. Благодарю. Но зачем страх нагонять, используя обороты «ужас», «кошмар», «зло» в технической статье? Пойду застрелюсь…

    Reply
  44. higs

    А мне понравилось. Достаточно глубокая проработка и исследование, далеко не всегда доходят руки так глубоко лезть в скуль. Конечно, пока будешь приучать себя так писать запросы, производителность собственная упадет.

    Есть только некие неточно сформулированные в голове противоречия для использования рекомендованных правил из статьи и рекомендаций по работе в СКД. Например, в СКД рекомендовано отборы оставлять для выбора пользователя. Очень часто, это как раз составной тип или будет при соединении. Ну и еще там есть.

    Как я понимаю, для СКД все еще страшнее и ужаснее.

    За статью просто твердущий плюс!

    Reply
  45. zqzq

    Статья во многом пересекается с приложением книги «Проф. разработка — реализация прикладных задач в системе 8.2», там с табличками и примерами расписаны особенности и неприятности составных типов (а также вообще хранение данных). Тут в статье более сумбурно и подробно мне показалось, в книге более структурировано и кратко.

    Reply
  46. Evil Beaver

    (42) ЗаписьXML — это стрингбилдер? Аааааа ну точно же! Чорт, как все просто иногда оказывается! Мегареспект!!!!

    Про числа не понял… меня особо не докучала никогда производительность численных операций. А что за задачи?

    Reply
  47. ildarovich

    (42) Насчет

    А = А + «Ы»

    написано в статье Опять двойка Там предлагается для решения проблемы

    первоначально работать с такими длинными строками как с массивом символов (строк), а затем преобразовывать массив символов(строк) в результирующую строку с помощью простейшей рекурсивной функции

    . Интересно теперь будет сравнить по быстродействию с решением через ЗаписьXML. Кстати, проблема А = А + «Ё» существует в большинстве языков. Смотрите, например, Строки в C# и .NET / Хабрахабр.

    Reply
  48. bulpi

    Все хорошо. Единственное замечание. Когда мне ради оптимизации предлагают заменить запрос в 3 строки на запрос в 30 строк, я останавливаюсь и начинаю думать 🙂 Когда через полгода я (или не дай Бог не я!) снова полезет в этот модуль, то количество МОЕГО драгоценного времени ( а не железного компьютерного), потраченного на понимание , чего тут творится, вырастет в N раз и резко сократит общий выигрыш от оптимизации (аж до минуса). Так что я буду продолжать использовать вариант Регистратор.Основание.Контрагент=&Контрагент.

    Reply
  49. speshuric

    (45) Да, 2-й том, страница 670 🙂 Но там не разобраны моменты с ВЫБОР и МАКСИМУМ, на грабли которых мы наступили. Если честно, то я пока долистываю первый том: на работе как-то не до чтения, домой купил недавно и дома больше 50-70 страниц в день не успеваю проработать.

    (48) Пожалуйста-пожалуйста. Лишь бы это было осознанным выбором, и вы умели видеть и лечить свои же грабельки. У меня вот, например, стоимость месяца работы меньше стоимости пятиминутного простоя некоторых ИБ предприятия.

    Reply
  50. speshuric

    (47) Проверил. ЗаписьXML быстрее (для большого количества конкатенаций, конечно).

    Reply
  51. ildarovich

    (50) Спасибо! — Буду иметь ввиду. Еще перепроверил, действительно, формирование строки из миллиона «Ы» занимает при помощи функции

    Функция ДлиннаяСтрокаЧерезЗаписьXML(Длина, Чего = «») Экспорт
    Запись = Новый ЗаписьXML;
    Запись.УстановитьСтроку();
    Чего = Лев(Чего + » «, 1);
    Для ё = 0 По Длина — 1 Цикл Запись.ЗаписатьБезОбработки(Чего) КонецЦикла;
    Возврат Запись.Закрыть()
    КонецФункции

    примерно 2 секунды. Ближайший конкурент через представление массива при помощи функции

    Функция ДлиннаяСтрокаЧерезПредставлениеМассива(Длина, Чего = «») Экспорт
    Массив = Новый Массив(Длина);
    Чего = Лев(Чего + » «, 1);
    Для ё = 0 По Длина — 1 Цикл Массив[ё] = Чего КонецЦикла; //на это уходит больше половины времени, а половина от этого просто на цикл!!!
    сё = Сред(СтрЗаменить(ЗначениеВСтрокуВнутр(Массив), «»»},» + Символы.ПС + «{«»S»»,»»», «»), 53 + СтрДлина(Формат(Массив.Количество(), «ЧГ=»)));
    Возврат Лев(сё, СтрДлина(сё) — 6)
    КонецФункции

    отстает на 12 процентов с результатом 2,5 секунды.

    Reply
  52. tormozit

    (42) Про ЗаписьXML я сам не догадался. Теперь буду применять. Спасибо!

    Reply
  53. LexSeIch

    Мир этому дому! Большое спасибо за серьезную статью. Информации много и она интересная, а тем более это не компиляция доков, а практически проверенно. Большой плюс.

    Reply
  54. kiros

    Александр, огромное спасибо за статью! Теперь в голове расставилось все по полочкам. А применение на практике дало просто колоссальный результат (а кто бы мог подумать, где собака порылась). Как то раньше сильно надеялся на платформу, а оказывается… буду сильнее изучать эту тему. ОГРОМНОЕ СПАСИБО.

    Reply
  55. S2Sps

    Спасибо огромное за статью. Прочитал как на духу. Написано очень подробно и особенно радуют примеры. Всегда радовался составным типам. А теперь вернулся на землю ) и уж точно буду использовать их впредь аккуратно.

    Reply
  56. uinx
    Reply
  57. burlakov

    Статья просто шикарная. На часть вещей просто глаза открылись. Даже не думал, что от составных типов может быть так плохо, хотя и зная о проблемах с производительностью составные типы практически не использую в своих разработках.

    Reply
  58. markgenuine

    Автор молодец, узнал много интересного, написано очень грамотно, подробно и понятно даже любому пользователю.

    Спасибо.

    Reply
  59. speshuric

    (56) uinx, а какая структура регистров? И как организовано «Номенклатура.Реквизиты.Сезонность»?

    Reply
  60. speshuric
    Reply
  61. Sorm

    Хорошая грамотная статья, да только кто сначала будет разбираться в причинах резкого замедления исполнения запроса, когда «одно поле поменяли»? Скажут — «оборудование не тянет, давайте новое купим», ну или на крайний случай — 120 тыс рублей. Что интересно — когда на новом оборудовании запрос будет работать также(ну или чуть быстрее) вот тогда уже начинается «1С — г… » или там три конверта:), но чтобы профайлер открыть — «а что это»? Пример — недавно на мисте обсуждали какой-то запрос, что-то там запрос к виртуальным регистрам, с невъ… количеством джоинов по регистратору(здравствуйте, вычисляемые поля), так в профайлер в теме влезло два человека:).

    Reply
  62. mikhailovaew

    Статья феноменальная. Огромное спасибо автору за труд, систематизацию и оформление. За отстутствие снобизма, за готовность отвечать на комментарии.

    Завидую вашим коллегам, как прекрасно, когда есть у кого перенять опыт! А то сидишь единственным программистом на фикси, и в свой же говнокод и ткнуть некому…

    А теперь вопрос нуба: как неправильный пример приводится запрос «Регистр.Регистратор.Дата>&Дата». Имеется в виду, что отбор по дате должен идти через Период соответствующего регистра? А если нужно именно сделать отбор по каким-то свойствам регистратора, то как это правильно оформить в запросе? Возможно, потребность такого отбора сигнализирует о неправильной архитектуре регистра, но допустим, что регистр менять мы не будем, а отбор в запросе реализовать надо.

    Reply
  63. speshuric

    (62) mikhailovaew:

    1. Отбор замедлится по любым реквизитам поля составного типа. Дата или не дата — тут не важно

    2. Архитектура, она обычно не в каменных скрижалях высечена и кривую архитектуру нужно менять. Пусть не за один день и не останавливая другие задачи, но менять придётся. Кроме изменения регистра есть еще как минимум прием — создание нового регистра.

    3. Как костыльный вариант (один из многих):

    ВЫБРАТЬ что-то
    ИЗ РегистрНакопления.МойРегистр КАК МойРегистр
    ГДЕ 1 В
    (ВЫБРАТЬ 1 ИЗ Документ.ДокументРегистратор1 КАК Док ГДЕ Док.Ссылка = МойРегистр.Регистратор И Док.Дата>&Дата
    ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 1 ИЗ Документ.ДокументРегистратор2 КАК Док ГДЕ Док.Ссылка = МойРегистр.Регистратор И Док.Дата>&Дата
    ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 1 ИЗ Документ.ДокументРегистратор3 КАК Док ГДЕ Док.Ссылка = МойРегистр.Регистратор И Док.Дата>&Дата
    ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 1 ИЗ Документ.ДокументРегистратор4 КАК Док ГДЕ Док.Ссылка = МойРегистр.Регистратор И Док.Дата>&Дата
    ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 1 ИЗ Документ.ДокументРегистратор5 КАК Док ГДЕ Док.Ссылка = МойРегистр.Регистратор И Док.Дата>&Дата
    ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 1 ИЗ Документ.ДокументРегистратор6 КАК Док ГДЕ Док.Ссылка = МойРегистр.Регистратор И Док.Дата>&Дата
    ОБЪЕДИНИТЬ ВСЕ …….
    ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 1 ИЗ Документ.ДокументРегистраторN КАК Док ГДЕ Док.Ссылка = МойРегистр.Регистратор И Док.Дата>&Дата)
    

    Показать

    Но потом править эту «прелесть» — проще застрелиться сразу. А когда таких запросов 500 в конфигурации?

    Reply
  64. mikhailovaew

    (64) согласна, запрос душераздирающий.

    спасибо за ответ )

    Reply
  65. slazzy

    (63) здравствуйте. Статья на самом деле классная. В общем очень радует эта тенденция, наконец появляются серьезные технические статьи.

    Подскажите, где и вообще что можно читать в подобной тематике для начинающего программиста, который таки хочет стать хорошим программистом? ) есть ли вообще подобная литература/статьи, или единственный выход это исследовать самому?(ну помимо статей, которые есть на инфостарте)

    Reply
  66. speshuric

    (65) Вон, выше «проф. разработку» советуют — в ней вагон информации. Ну и в принципе — ИТС и книги 1С. Остальное — как-то так, самостоятельно.

    Reply
  67. Ish_2

    (60) Ты написал :

    «3.2. во временной таблице ПрайсЛист — нужен индекс по Номенклатура»

    В запросе также используется индексирование временной таблицы «ФильтрПодразделения»

    Вопрос : Зачем ?

    Оптимизатор запроса сам создаст индекс.

    Причем в моих опытах на временных таблицах значительного размера явное индексирование приводило

    к некоторому замедлению выполнения запроса как в файловом варианте так и в клиент-серверном.

    Reply
  68. speshuric

    (67) Ish_2,

    1. Оптимизатор индекс не строит. Никогда. В лучшем случае добавит операцию «Sort».

    2. Но и операция Sort это ни разу не индекс на самом деле. Где-то сортированная выборка лучше обхода индексов (особенно для небольших таблиц), где-то хуже. Важно, что для каждого соединения таблиц скорее всего придётся серверу сортировать отдельной операцией с отдельными затратами.

    3. Сервер совсем не всегда правильно догадывается о том, нужно сортировать или не нужно. Индекс на ВТ — «прозрачный намёк» (я намёки жены примерно также понимаю: когда мусорный пакет она мне даёт в руки, тогда я его и выношу).

    4. И, да, индексирование не бесплатное. Но если утверждается, что у запроса план кривой и запрос долго выполняется, то для относительно мелкой ВТ можно и проиндексировать исходя из последующих сортировок/отборов/соединений — хуже скорее всего не станет.

    5. За файловый ничего говорить не буду. Я в нём никто, и не представляю как там ВТ сделаны.

    Reply
  69. Ish_2

    (68) «Оптимизатор индекс не строит. Никогда. В лучшем случае добавит операцию «Sort». »



    Отлично. Рассматриваем клиент-сервер.

    1. Я сделал запросом временную таблицу ВРЕМ1 из двух-трех полей с уникальным полем «Ключ» ( скажем, 1 000 000 записей ). Индекс не строю.

    Аналогичным запросом получаю таблицу ВРЕМ2. Индекс для ВРЕМ2 также не строю.

    Затем делаю соединение двух таблиц по полю «КЛЮЧ» и вывожу результат. Засекаю время TIME1.

    2. Повторяю п1. Но для каждой из временных таблиц ВРЕМ1 и ВРЕМ2 указываю «ИНДЕКСИРОВАТЬ по КЛЮЧ».

    Засекаю время TIME2.

    Повторить эксперимент легко. Поэтому я утверждаю TIME1< TIME2 как минимум в два раза.

    Если ты подтвердишь этот факт , то тогда вопросы :

    1. почему явное индексирование временных таблиц приводит к резкому замедлению ?

    2. кто же индексирует ВРЕМ1 и ВРЕМ2 перед соединением ?

    Reply
  70. Ish_2

    На всякий случай реальный текст запроса для п.1.

    В нем закомментирована опция «Индексировать по » .

    ВРЕМ1 и ВРЕМ2 отсортированы по разным полям.

    ВЫБРАТЬ ПЕРВЫЕ 500000
    ОП_ФинансовыеТранзакцииОпера.Ссылка,
    ОП_ФинансовыеТранзакцииОпера.НомерБрони,
    ОП_ФинансовыеТранзакцииОпера.КодУслуги
    ПОМЕСТИТЬ Врем1
    ИЗ
    Документ.ОП_ФинансовыеТранзакцииОпера КАК ОП_ФинансовыеТранзакцииОпера
    
    УПОРЯДОЧИТЬ ПО
    ОП_ФинансовыеТранзакцииОпера.НомерБрони
    // Индексировать по ОП_ФинансовыеТранзакцииОпера.Ссылка
    ;
    
    ////////////////////////////////////////////////////////////­////////////////////
    ВЫБРАТЬ ПЕРВЫЕ 500000
    ОП_ФинансовыеТранзакцииОпера.Ссылка,
    ОП_ФинансовыеТранзакцииОпера.НомерБрони,
    ОП_ФинансовыеТранзакцииОпера.КодУслуги
    ПОМЕСТИТЬ Врем2
    ИЗ
    Документ.ОП_ФинансовыеТранзакцииОпера КАК ОП_ФинансовыеТранзакцииОпера
    
    УПОРЯДОЧИТЬ ПО
    ОП_ФинансовыеТранзакцииОпера.КодУслуги
    //Индексировать по ОП_ФинансовыеТранзакцииОпера.Ссылка
    ;
    
    ////////////////////////////////////////////////////////////­////////////////////
    ВЫБРАТЬ
    Врем1.Ссылка,
    Врем1.НомерБрони,
    Врем1.КодУслуги,
    Врем2.Ссылка КАК Ссылка1,
    Врем1.НомерБрони как НомерБрони1,
    Врем1.КодУслуги  как КодУслуги1
    ИЗ
    Врем1 КАК Врем1
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ Врем2 КАК Врем2
    ПО Врем1.Ссылка = Врем2.Ссылка

    Показать

    Reply
  71. speshuric
    Reply
  72. Ish_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.

    Так ?

    Reply
  73. speshuric

    (72) Ish_2,

    1. Согласен ли ты с тем , что для конкретных примеров (60) индексировать таблицы «ФильтрНоменклатура»

    и «ФильтрПодразделения» и в (70) таблицы ВРЕМ1 и ВРЕМ2 НЕ нужно (причем без всяких оговорок) ?


    Нет. Могу показать, но на выходных. Если «запрос тормозит» из-за некорректного плана, то индексирование ВТ вряд ли ухудшит его. В данном случае по (60) я вообще сразу написал, что без доп. данных всё это будет гаданием на кофейной гуще.

    И еще раз обращу внимание. Сравнивать просто время — не вполне корректно.

    2. К сожалению ( а может быть и к счастью), никогда не открывал профалер.

    Ну профайлер я использовал только потому что терять время на запуск ТЖ (минута же!) было лень и показывает профайлер в целом больше, поэтому я его чаще использую. Время, запросы, планы можно было и из ТЖ взять. Профайлером, кстати, пользоваться очень просто. Могу научить 🙂

    Тогда совершенно непонятен механизм соединения двух таблиц в MSSQL без использования индексов.

    Настройка запроса — отличное место для начала. На русском. Про джойны, например, раздел «Дополнительные понятия настройки запросов». Если кратко: есть 3 вида джойнов (hash, merge, nested loops), в зависимости от ситуации оптимизатор использует каждый из них.

    Если надо в большом индексированном (упорядоченном) списке найти мало записей, то nested loops («вложенные циклы»).

    Если надо соединить 2 одинаково упорядоченных набора, то merge.

    В большинстве других случаев это будет hash join, которых тоже есть несколько видов. Hash join целиком или частями, в памяти или не совсем в памяти строит хэш-таблицу одного из наборов, а вторым набором идёт по хешам.

    Вариант Берем первую запись ВРЕМ1 , тупо сканируем таблицу ВРЕМ2 по значению ВРЕМ1.Ссылка, находим , выводим.

    И так далее для второй , третьей записи .. из ВРЕМ1.
    будет использоваться для очень маленьких временных таблиц или если оптимизатор профакапится и примет решение использовать nested loops там, где нужен был hash join.

    Reply
  74. Ish_2

    (73) Почему на выходных ? Не пойму .

    Ты работаешь что ли ?

    Оттолкнемся от факта. Ты и я убедились в том , что для конкретного примера (70) индексация ВТ не нужна.

    Отсюда я делаю вывод , что индексация ВТ по ключевому полю НЕ нужна при условии, что в дальнейшем ВТ используется однократно ( фильтр или соединение).

    Если ты с этим не согласен, то приведи контрпример (текст запроса) , чтобы я проверил и убедился

    в обоснованности твоего несогласия.

    За «Настройку запроса» спасибо. Как далеко ушла техника .. пока я не открывал профайлер.

    Добавлено :

    Текст в (70) исправил «по-твоему» : последний запрос поместил в таблицу ВРЕМ3.

    И тогда TIME1 ( время без индексации) меньше TIME2 ( время с индексацией) в ~ 3 раза.

    Reply
  75. Al-X

    Спасибо автору, статья СУПЕР !!! Эт скока же запросов мне надо просмотреть ?!!! Самое обидное, буквально вчера я в отчете использовал составные типы через точку в условии 🙁 .

    Мда… думаю я еще долго буду перечитывать статью и оптимизировать свои запросы.

    Еще раз спасибо за статью !!!

    Reply
  76. speshuric

    (74) Ish_2, Посмотрел. В 8.1.15 и 8.2.16 индексы создаются отдельной командой CREATE INDEX, примерно так

    SELECT TOP 0 CAST(0 AS NUMERIC(38,8)) _Q_001_F_000,
    CAST(0 AS NUMERIC(38,8)) _Q_001_F_001
    INTO #tt1
    
    INSERT INTO #tt1
    (_Q_001_F_000, _Q_001_F_001)
    SELECT
    ….
    
    CREATE INDEX TmpInd1 ON #tt1 (
    _Q_001_F_000)

    Показать

    Отсюда и 2 раза. В 8.3.3 и вроде в последних 8.2 — заранее создаётся индекс (и вставка тупит лишь минимально).

    Reply
  77. Ish_2

    (76) Вот и выяснили расхождения в результатах.

    Тем не менее , если представишь контрпример (74) будет совсем всё ясно.

    P.S. Ну , на выходных , конечно.

    Reply
  78. speshuric

    (74) Да уж, посмотрел, как ВТ работают в 8.1.15 и 8.2.16, там действительно надо постараться для того, чтобы индексы заработали.

    Итого:

    1. Индексы создаются отдельной командой. После вставки. Это достаточно существенные затраты (для таблицы в 16 млн записей — создание индексов в 4-6 раз дороже самой вставки)

    2. В 8.2.16 создаются некластеризованные индексы. С одной стороны это хорошо, создание индексов не требует перелопачивания страниц данных, с другой — если индекс содержит не все строки таблицы, то почти гарантированно будет достаточно дорогой RID Lookup. То есть в индекс ВТ нужно запихивать ВСЕ колонки, которые потом могут быть использованы в запросах.

    В общем, на этих версиях однократное использование индексированных таблиц, наверное, заметной практической ценности не имеет. Пример, когда индекс всё-таки нужен для однократного использования я привожу ниже.

    На свежих версиях этой проблемы вроде нет.

    Запрос, который у меня и для однократного использования индекса показывает нужность индекса:

    ВЫБРАТЬ 0 КАК Знч, 2 КАК База ПОМЕСТИТЬ Уровень0
    ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 1, 2;
    ВЫБРАТЬ
    Таб1.Знч + Таб1.База * Таб2.Знч КАК Знч,
    Таб1.База * Таб1.База КАК База
    ПОМЕСТИТЬ Уровень1
    ИЗ Уровень0 КАК Таб1, Уровень0 КАК Таб2;
    ВЫБРАТЬ
    Таб1.Знч + Таб1.База * Таб2.Знч КАК Знч,
    Таб1.База * Таб1.База КАК База
    ПОМЕСТИТЬ Уровень2
    ИЗ Уровень1 КАК Таб1, Уровень1 КАК Таб2;
    ВЫБРАТЬ
    Таб1.Знч + Таб1.База * Таб2.Знч КАК Знч,
    Таб1.База * Таб1.База КАК База
    ПОМЕСТИТЬ Уровень3
    ИЗ Уровень2 КАК Таб1, Уровень2 КАК Таб2;
    ВЫБРАТЬ
    Таб1.Знч + Таб1.База * Таб2.Знч КАК Знч,
    Таб1.База * Таб1.База КАК База
    ПОМЕСТИТЬ Уровень4
    ИЗ Уровень3 КАК Таб1, Уровень3 КАК Таб2;
    ВЫБРАТЬ
    Таб1.Знч + Таб1.База * Таб2.Знч КАК Знч,
    Таб1.База * Таб1.База КАК База
    ПОМЕСТИТЬ Уровень5
    ИЗ Уровень4 КАК Таб1, Уровень3 КАК Таб2
    ИНДЕКСИРОВАТЬ ПО Знч, База //вот этот индекс
    ;
    ВЫБРАТЬ Таб1.Знч КАК Знч, Таб1.Знч * 2 КАК Знч2
    ПОМЕСТИТЬ Уровень31
    ИЗ Уровень3 КАК Таб1;
    ВЫБРАТЬ
    Таб1.Знч,
    Таб1.База,
    Таб2.Знч КАК Знч1,
    Таб2.Знч2 КАК Знч2
    ИЗ
    Уровень5 КАК Таб1
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ Уровень31 КАК Таб2
    ПО (Таб1.Знч МЕЖДУ Таб2.Знч И Таб2.Знч2)

    Показать

    Суммарно с индексом у меня делалось около 65 сек, а без индексов — 88

    Reply
  79. Ish_2

    (78) Релиз 8.2.18.61. Твой запрос (78) скопирован в КонсольЗапросов.

    Время без индексирования 75 сек. ( сред.время 2 и 3 запуска)

    Время с индексированием 170 сек ( сред.время 2 и 3 запуска).

    Здесь всё понятно .

    И мы понимаем , что время без индексирования меньше всего лишь из-за кривости релиза 8.2.

    Но нас-то интересовал принцип ,

    т.е. нужно было в принципе добиться чтобы время с индексированием было меньше.

    И ты этого добился при использовании «некривого» 8.3.

    Дело тут , надо думать, в сложности выражения соединения ( у тебя «Таб1.Знч МЕЖДУ Таб2.Знч И Таб2.Знч2»)

    Здесь получаем , что преимущества индексированной таблицы становятся очевидными.

    В (70) же использовалось «слишком» простое условие «Врем1.Ссылка = Врем2.Ссылка».

    Ок. Спасибо !

    Reply
  80. speshuric

    (79) тест запускал на чем? В смысле: какая версия скуля и что там с памятью и дисками? Типа могу подогнать, теперь уже понятно как. Там вопрос не в сложности выражения, а в том, что без индексов «МЕЖДУ Таб2.Знч И Таб2.Знч2» приводит к жестокому соединению с обходом по декартову произведению, а с индексами лишь выгребания небольших диапазонов. Остаётся лишь подобрать размеры таблиц. Как несложно заметить, тут индексированная таблица достаточно жирная.

    Reply
  81. Ish_2

    (80)

    1. Сам я не очень-то .. Завтра спрошу у наших сисадминов.

    2. «Там вопрос не в сложности выражения, а в том, что без индексов «МЕЖДУ Таб2.Знч И Таб2.Знч2″ приводит к жестокому соединению с обходом по декартову произведению, а с индексами лишь выгребания небольших диапазонов.»

    Ну, да. Это и имелось ввиду.

    В случае усложнения ( добавления новых условий) резко увеличивается объем пробега для этой самой «hash join».

    Reply
  82. Ish_2

    «А осадочек остался…».

    Дело в том , что я ни секунды не сомневался оптимизатор запроса сам строит необходимые индексы ( пусть даже некие «временные» , «виртуальные»)- если нужно ,после анализа условий соединения или фильтраций.

    Фразу «не индексируйте временные таблицы при однократном последующем использовании» я помню «со школы».

    Ведь еще при царе Горохе старые СУБД именно так создавали недостающие индексы — насколько помню.

    В последнем твоем примере необходимость создания недостающего индекса также очевидна.

    И тут на тебе : MSSQL этого не делает.

    Неужто трудно проанализировать выражение соединения и создать индекс ?

    А ORacle и прочие ?

    Reply
  83. Ish_2

    (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

    Reply
  84. hogik

    (82)

    «»не индексируйте временные таблицы при однократном последующем использовании» я помню «со школы».»(с)

    Игорь (Ish_2).

    Помнить мало, надо еще понимать. 😉

    Эта фраза относится к другому случаю — если «однократно» применяется запрос выборки записей из таблицы. Т.к. при индексировании выполняется полный просмотр всех записей таблицы. Плюс, затраты на построение самого индекса. А потом еще запрос будет просматривать N-ое количество записей. Т.е. быстрее будет работать сразу запрос без (вместо) предварительного индексирования.

    Вроде, это так очевидно? Даже, я это смог понять… 😉

    Reply
  85. Ish_2

    (84) Дело прошлое , догадки — дело скользкое. Кто там что понял ,а что — нет..

    Пока ясно одно , MSSQL сам не создает недостающие индексы.

    Вчера возмущенный этим фактом я написал в MS. Жду вот.

    Думаю , к осени поправят.

    Как считаете ?

    Reply
  86. hogik

    (85)

    «… к осени поправят … ?»(с)

    Игорь (Ish_2).

    Нет. Но, включат в планы для «MS SQL 2021».

    Reply
  87. MrFlanker

    Отличная статья, прочитал на одном дыхании

    Reply
  88. Bukaska

    Отличная статья! Много интересного…

    Reply
  89. agrustny

    Это замечательная великолепная статья, я Вас недавно рекламировал!

    Reply
  90. Збянтэжаны Саўка

    ух-ты какая шикарная статья!!!

    я в шоке — она меня просто потрясла!!!

    спасибо!

    Reply
  91. Ndochp

    (49) Вот ты не пожалел своего времени, так как оно дешевле простоя предприятия и переписал по всей конфе в 100500 местах на «оптимальное» решение с ручным объединением всего и вся.

    А потом кто-то сделал движение корректировкой записи регистров, про которую ты забыл. Или у этого регистра добавился регистратор. Какова вероятность, что ты поправишь не в 100500 местах, а в 100499, и предприятие не останавливаясь продолжит работать с неверными данными?

    И не больше ли это убытков принесет?

    Reply
  92. It-developer

    На 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 байт} (на реквизит) и какое-то количество байт на всю строку

    Reply
  93. speshuric

    (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 (как минимум) индекс будет ВСЕГДА (но это действительно не важно для вашего исследования).

    Плюс по структуре файловой базы можно еще почитать этого товарища.

    И я не понял причем тут составные поля, если вы приводите пример простых полей — статья в основном про особенности именно составных полей. И в моей статье в первом абзаце написано

    Показывать буду на примере MS SQL Server

    В других СУБД структура схожа, но не до байтов, разумеется.

    В общем, я не понял что исследовалось. Строки в 1С с двухбайтными буквами — это вряд ли где-то отличается. Дополнительные поля не учтены. Страницы не учтены. Дополнительные расходы из этих данных считать нельзя.

    Reply
  94. It-developer

    (93) Спасибо!

    Я не учёл _IDRRef (16 байт в SQL), _Marked (1 байт в SQL), _Version (8 байт в SQL) 🙁

    _IsMetadata (1 байт в SQL) там нет, но есть пустое _PredefenedID

    В общем 16+1+8 = 25 байт.

    Т.е. в итоге 2 байта на символ строки 🙂

    Моя цель исследования была понять сколько можно запихнуть данных в файловую версию в справочник 🙂

    В итоге недосчитался и поэтому написал на форуме 🙂

    Понятно, что все в основном пользуются SQL, но бывают разные исключения.

    К составным типам да, мои тесты отношения не имеют

    Вообще мне Ваша статья понравилась. Я ранее ее использовал в работе

    Reply
  95. speshuric

    (94) It-developer,

    Ну тогда всегда пожалуйста. Жаль, конечно, что для 1С нет аналога такой статьи, по которой можно достаточно точно определить сайзинг.

    Reply
  96. speshuric

    (96) Sergey.Noskov,

    Ну дай дураку рекомендацию, он и руки порежет и рекомендацию извратит.

    Reply

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *