IE 2016

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

Опубликовал speshuric в раздел Программирование - Практика программирования

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

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

"...Еще одним важным решением в части работы с данными в "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. Не используйте сложные выражения с составными типами. Почти единственное допустимое место в запросе для составных полей — непосредственное возвращение их значений без обработки.

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

Выводы

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

См. также

Лучшие комментарии

42. speshuric 14.05.2013 18:05
(40) Ну Спольскому с его особенностями простительна "любовь покрасоваться". А в 1С до фига и других дуршлагов-абстракций. Начиная со строк и арифметики:
  • Арифметика пытается усидеть на 2 стульях (точные значения и плавающая точка). В результате производительность нигде, артефакты на интеграции так и прут, и СУБД про такую арифметику ваще не в курсе. Когда я вижу что браузерs в JavaScript фигачат до 500 млн операций с числами в секунду, то вспоминая 1С прям аж... Прям аж...
  • Строки. Я не буду вспоминать эпопею с преобразованиями crlf. Я промолчу про то, что интерпретатор 1С считает буквами, а что нет (вопрос, кстати, не так тривиален :) ). Давайте лучше про конкатенацию. А = А + "Ы" - что может быть проще. Пока этих выражений не 100500. Возникает глупая ситуация, когда для большого запроса к внешней системе основное время уходит на создание запроса, а не на выполнение или разбор. StringBuilder отсутствует. ТекстовыйДокумент не спасает. Зато спасает (кто бы мог подумать) ЗаписьXML:
    ТекстПостроитель = Новый ЗаписьXML;
    ТекстПостроитель.УстановитьСтроку();
    ТекстПостроитель.ЗаписатьБезОбработки("Кусочек строки");
    ТекстПостроитель.ЗаписатьБезОбработки(", и еще один");
    СтрокаРезультат = ТекстПостроитель.Закрыть();
    ...Показать Скрыть

    Я об этом писал у себя в бложике
Реально, куда ни копни - герметичности этих абстракций нет. Ну да и ладно. Зато вентиляция :)
Ответили: (46) (47) (52)
# Ответить
15. speshuric 12.05.2013 10:36
(14) Мне посчастливилось работать в коллективе хороших специалистов, в котором не нужно коллегам объяснять что такое индекс в базе данных, какие операции он ускоряет, на какие не влияет, а какие замедляет, не нужно рассказывать что такое транзакция и что такое транзакционные блокировки, все в общем-то знают что такое план запроса (хотя некоторым и нужно помочь читать его). Поэтому "тупых" ошибок возникает не много, а если и возникают, то они моментально диагностируются и стремительно устраняются.
А раз простых ошибок нет, то на первый план выходят менее тривиальные и архитектурные ошибки. Это и неэффективность некоторых архитектурных шаблонов, это и ограничения текущих версий платформы (должен признать, что 1С постепеннно устраняет откровенно неэффективные решения, особенно если их несложно исправить), ну и в частности неэффективная работа с составными типами. Это чаще архитектурная ошибка - без составных типов часто можно обойтись или свести их использование к "удачным" шаблонам. Неприятно то, что такие ошибки имеют тенденцию накапливаться: во-первых обнаруживаются не сразу, и к этому моемнту исправлять их уже нужно с накопленными данными и зависимой логикой, во-вторых "работает же" - из-за этого у задачи исправления такой ошибки с точки зрения бизнеса достаточно низкий приоритет, в-третьих когда-то это могло быть хорошим архитектурным решением (например на предыдущей версии платформы), а сейчас это стало убогим тормозом.
Эта статья написана, чтобы показать, что создание или расширение составного типа - важное и спорное техническое архитектурное решение, хоть и делается "одной галочкой", и какие грабли есть у этого решения. Лаконичная рекомендация в статье уважаемого Константина Рупасова (статья "Типичные причины неоптимальной работы запросов и методы оптимизации", см. "получение данных через точку от полей составного типа") слишком многое оставляет за бортом. Если говорить о том, нужна ли эта тема другим, то нужна. Вот только то, что перед глазами в незакрытых вкладках браузера:

1. В принципе неплохая статья Полезные сведения о языке запросов 1С 8.2, но в ней в пункте 5, чтобы избавиться от соединений, рекомендуется написать:
ВЫБРАТЬ РАЗЛИЧНЫЕ
ВЫРАЗИТЬ(ОстаткиТоваров.Регистратор КАК Документ.ПоступлениеТоваров).Номер КАК НОМЕРПОСТУПЛЕНИЯ,
ВЫРАЗИТЬ(ОстаткиТоваров.Регистратор КАК Документ.ПоступлениеТоваров).Дата КАК ДАТАПОСТУПЛЕНИЯ
ИЗ
РегистрНакопления.ОстаткиТоваров КАК ОстаткиТоваров
ГДЕ
(ВЫРАЗИТЬ(ОстаткиТоваров.Регистратор КАК Документ.ПоступлениеТоваров) ЕСТЬ НЕ NULL)

Одно лечим, другое калечим. Соединения заменили на полное сканирование, даже если статистика позволяет использовать индекс. Стоило использовать ОстаткиТоваров.Регистратор ССЫЛКА Документ.ПоступлениеТоваров. Кстати, там в комментариях об этом написано.

2. На мисте тема недавно была. Тоже идёт работа с составным типом.
Ответили: (32)
# Ответить
37. vlad.frost 14.05.2013 12:17
Отличная статья в тему про дырявые абстракции http://russian.joelonsoftware.com/Articles/LeakyAbstractions.html (если вдруг кто ещё не читал)
Ответили: (39)
# Ответить

Комментарии

1. ildarovich 11.05.2013 12:24
Очень интересно. Некоторые вещи хочется перепроверить или уточнить. Например, почему
индексы по такому составному полю не могут использоваться и в более простом запросе:
ВЫБРАТЬ РАЗЛИЧНЫЕ
Справочник1.СоставноеПоле КАК СоставноеПоле
ИЗ
Справочник.Справочник1 КАК Справочник1
Хочется подробностей про
В принципе в современных СУБД давно есть средства, которы бы позволили работать с такими типами эффективно
Как в принципе может использоваться индекс в
Агрегатные функции — МИНИМУМ(...) и МАКСИМУМ(...)
Пока прочитал по диагонали. Возможно, будут еще вопросы.
Ответили: (2) (3) (6)
# Ответить
2. speshuric 11.05.2013 14:42
(1)
1. Индекс не будет использоваться потому что нет нужного индекса:
ВЫБРАТЬ РАЗЛИЧНЫЕ
Справочник1.СоставноеПоле КАК СоставноеПоле
ИЗ
Справочник.Справочник1 КАК Справочник1

будет выполняться как
ВЫБРАТЬ РАЗЛИЧНЫЕ
Справочник1.СоставноеПоле.Тип КАК СоставноеПолеТип,
Справочник1.СоставноеПоле.Число КАК СоставноеПолеЧисло,
Справочник1.СоставноеПоле.Ссылка КАК СоставноеПолеСсылка
ИЗ
Справочник.Справочник1 КАК Справочник1

Отдельно есть индексы (Справочник1.СоставноеПоле.Тип, Справочник1.СоставноеПоле.Число) и (Справочник1.СоставноеПоле.Тип, Справочник1.СоставноеПоле.Ссылка). И, если бы это было поле простого типа, а не сотавного, то можно было бы расчитывать, что движок БД его подхватит, а для составного - не получается использовать.
Ответили: (4)
# Ответить
3. speshuric 11.05.2013 14:53
(1)
2. Про "средства, которы бы позволили работать" - там рядом ссылки есть.
3. Мне если честно, казалось это очевидным. Попробую чуть подробнее. Самая простая аналогия для индекса - упорядоченный список статей с оглавлением. В такой структуре легко найти все значения, входящие в диапазон, а если этих значений очень много, то легко как минимум входящие в диапазон.
Поэтому запрос
ВЫБРАТЬ МИНИМУМ(ОстаткиНоменклатуры.Период)
ИЗ РегистрНакопления.ОстаткиНоменклатуры КАК ОстаткиНоменклатуры
ГДЕ ОстаткиНоменклатуры.Период>&Дата

просто выберет первую запись из тех, которые ">&Дата".
Ответили: (4)
# Ответить
4. ildarovich 11.05.2013 15:52
(2) А это практически проверялось? - То есть Вы смотрели план соответствующего запроса? Проводили замеры?
(3) 2. Ссылки я посмотрел, однако не совсем ясно как это практически использовать. Вычисляемые поля - что за функция? - Что-то вроде хеш-функции? - А коллизии? Фильтрованные индексы. В качестве фильтра - тип поля? Но там разве есть возможность в одном индексе задать несколько фильтров (по числу базовых типов + ссылка)? Разреженные столбцы - тут понятно. Индексы с включенными полями - опять непонятно использование по отношению к данной проблеме.
3. Одно время мне это тоже казалось очевидным. Но я не нашел конкретного практического подтверждения тому, что это действительно делается. Может быть, плохо смотрел.
Ответили: (5) (6)
# Ответить
5. speshuric 11.05.2013 16:08
(4)
1. Да, конечно, план смотрел, хотя тут и план не нужно смотреть: нет покрывающего индекса. Но для статьи каждый пример проверялся.
2.1. Вычисляемые поля не как хэш-функции, а как простое выделение из составного поля простого. Вычисляемые поля (которые можно было бы вычислять из одного varbinary) и их фильтрованное индексирование было бы хорошим решением. Чисто механически уменьшился бы объём индексом и увеличилось количество случаев их использования. Вечерком накидаю в SQL что я имею ввиду и приложу скрипт, чтобы было понятно. Но в любом случае это к 1С отношения не имеет. Серым шрифтом я пометил свое ворчание :)
2.2. Индексы с включенными полями - именно они могли бы ускорить запросы типа того "ВЫБРАТЬ РАЗЛИЧНЫЕ" по которым сейчас индексы не используются.
3. Сейчас приведу пример с планом.
# Ответить
6. speshuric 11.05.2013 17:20
(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)  ......

...Показать Скрыть

Собственно, как и ожидалось.
Ответили: (12) (17)
+ 1 [ ildarovich; ]
# Ответить
7. Jogeedae 11.05.2013 19:53
Спасибо за труд!
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, по-моему)
Удалось обойти с помощью проверки типа ссылки в запросе и разбиением его на составляющие + временные.
Ответили: (9)
# Ответить
8. ILM 11.05.2013 20:05
Спасибо за статью.
"Многие знания, многие печали" - написано в книге Экклезиаста.
Все дело в объеме данных, если количество записей в таблице не велико и скорость выполнения запросов приемлема для пользователей, то можно и не обращать внимание на составные поля. И наоборот :-)
В целом статья будет полезна как новичкам, так и разработчикам с опытом.
Ответили: (10)
# Ответить
9. speshuric 11.05.2013 20:06
(7)
(Это ограничение действовало на ms-sql 2000, по-моему)
Почти верно. До 2005 включительно.
# Ответить
10. speshuric 11.05.2013 20:09
(8) Спасибо за отзыв. Не считаю небольшой объём и количество записей поводом писать кривой код. Ведь трудозатраты зачастую одинаковы.
Ответили: (11)
+ 3 [ krv2k; hulio; kiros; ]
# Ответить
11. ILM 11.05.2013 20:35
(10) Главным ограничением всех процессов - является время. Можно написать супер код и опоздать.
+ 2 [ Ovrfox; the1; ]
# Ответить
12. ildarovich 11.05.2013 21:15
(6) Спасибо, убедили... Видимо, запомнившаяся мне рекомендация использовать SELECT ТОР 1 ... ORDER BY ... вместо MIN в таких случаях относилась не к MS SQL или к другим типам индексов.
# Ответить
13. Bukaska 11.05.2013 22:45
ТОже спасибо, поставила +
Я ещё только учусь и всегда интересно знать, что если запрос надо строить именно так, то почему))))
# Ответить
14. Ish_2 12.05.2013 08:14
(0) Прочитал мельком. Нужны ли такие изыскания ?
Черт его знает, любая тема когда -нибудь становится актуальной.
Написано вроде бы "по-врослому". Вызывает уважение. +
Ответили: (15)
# Ответить
15. speshuric 12.05.2013 10:36
(14) Мне посчастливилось работать в коллективе хороших специалистов, в котором не нужно коллегам объяснять что такое индекс в базе данных, какие операции он ускоряет, на какие не влияет, а какие замедляет, не нужно рассказывать что такое транзакция и что такое транзакционные блокировки, все в общем-то знают что такое план запроса (хотя некоторым и нужно помочь читать его). Поэтому "тупых" ошибок возникает не много, а если и возникают, то они моментально диагностируются и стремительно устраняются.
А раз простых ошибок нет, то на первый план выходят менее тривиальные и архитектурные ошибки. Это и неэффективность некоторых архитектурных шаблонов, это и ограничения текущих версий платформы (должен признать, что 1С постепеннно устраняет откровенно неэффективные решения, особенно если их несложно исправить), ну и в частности неэффективная работа с составными типами. Это чаще архитектурная ошибка - без составных типов часто можно обойтись или свести их использование к "удачным" шаблонам. Неприятно то, что такие ошибки имеют тенденцию накапливаться: во-первых обнаруживаются не сразу, и к этому моемнту исправлять их уже нужно с накопленными данными и зависимой логикой, во-вторых "работает же" - из-за этого у задачи исправления такой ошибки с точки зрения бизнеса достаточно низкий приоритет, в-третьих когда-то это могло быть хорошим архитектурным решением (например на предыдущей версии платформы), а сейчас это стало убогим тормозом.
Эта статья написана, чтобы показать, что создание или расширение составного типа - важное и спорное техническое архитектурное решение, хоть и делается "одной галочкой", и какие грабли есть у этого решения. Лаконичная рекомендация в статье уважаемого Константина Рупасова (статья "Типичные причины неоптимальной работы запросов и методы оптимизации", см. "получение данных через точку от полей составного типа") слишком многое оставляет за бортом. Если говорить о том, нужна ли эта тема другим, то нужна. Вот только то, что перед глазами в незакрытых вкладках браузера:

1. В принципе неплохая статья Полезные сведения о языке запросов 1С 8.2, но в ней в пункте 5, чтобы избавиться от соединений, рекомендуется написать:
ВЫБРАТЬ РАЗЛИЧНЫЕ
ВЫРАЗИТЬ(ОстаткиТоваров.Регистратор КАК Документ.ПоступлениеТоваров).Номер КАК НОМЕРПОСТУПЛЕНИЯ,
ВЫРАЗИТЬ(ОстаткиТоваров.Регистратор КАК Документ.ПоступлениеТоваров).Дата КАК ДАТАПОСТУПЛЕНИЯ
ИЗ
РегистрНакопления.ОстаткиТоваров КАК ОстаткиТоваров
ГДЕ
(ВЫРАЗИТЬ(ОстаткиТоваров.Регистратор КАК Документ.ПоступлениеТоваров) ЕСТЬ НЕ NULL)

Одно лечим, другое калечим. Соединения заменили на полное сканирование, даже если статистика позволяет использовать индекс. Стоило использовать ОстаткиТоваров.Регистратор ССЫЛКА Документ.ПоступлениеТоваров. Кстати, там в комментариях об этом написано.

2. На мисте тема недавно была. Тоже идёт работа с составным типом.
Ответили: (32)
# Ответить
16. tormozit 12.05.2013 11:00
Спасибо за полезное исследование с качественной подачей материала.
+ 1 [ hulio; ]
# Ответить
17. ildarovich 12.05.2013 12:10
(6) Увидел в скрипте интересную рекурсивную CTE. Не могли бы ее прокомментировать: откуда-то взяли или сами придумали, почему считаете ее лучше чем простая CTE c 0 через id + 1 (на строчку короче и единицу не нужно будет отнимать). Сам использую подобные приемы, но в данном случае сомневаюсь в том, что будет быстрее. Замеры делали? За счет чего может быть выигрыш?
Ответили: (18)
# Ответить
18. speshuric 12.05.2013 12:34
(17)По порядку
  • Эту, cte кажется сам придумал, но не со своей идеей.
  • В этой cte мне нравится, что она "медленно" уходит в рекурсию. Обычно ставят OPTION (MAXRECURSION 0) (как здесь). В моём случае глубина рекурсии растет как логарифм, а соответственно значения по умолчанию (100 вложений) хватит и на bigint.
  • Производительность сравнивал, но не помню точно. Кажется мой вариант шустрый, но если я правильно помню, всё равно декартово произведение таблиц с "заготовкой" от 0 до "много" и с кластеризованным индексом рвёт остальные варианты, особенно если правильно прописать условия для использования индексов (оно получается громоздким).

UPD: померял производительность с "одиночной" рекурсией - она примерно одинаковая.
+ 1 [ ildarovich; ]
# Ответить
19. AlX0id 12.05.2013 12:48
Поставил бы пять плюсов и более, была бы возможность )
Отличное и доступное изложение, интересный и глубокий материал - спасибо )
# Ответить
20. vet7777 12.05.2013 14:15
Шурик, ты проделал огромную, а главное полезную работу. Причем ты все показал с профессиональной точки зрения, все грамотно разложил по полочкам. И кое-кого можно ткнуть носом теперь сюда ). Многие ситуации интуитивно и теоретически понимались, но ты провел отличное практическое исследование и описал проблему в целом. Причем не только показал минусы, но и указал как можно было бы исправить, как разработчикам, но и даже фирме 1С. Вообщем то, на infostart event можно было даже выступить с такой темой.
PS: пиши еще )))
Ответили: (22)
+ 3 [ lamelioss; bulpi; spock; ]
# Ответить
21. tormozit 12.05.2013 14:37
(20) Да, я тоже считаю что такой материал был бы очень полезен на конференции.
Ответили: (22)
+ 1 [ alk; ]
# Ответить
22. speshuric 12.05.2013 15:09
(20) Я же и есть главный бенефициар статьи. ;)
(21) Мелковата тема для доклада. Если изложить в слайдах и рассказывать голосом, то это 5-10 минут от силы. При этом вся практическая ценность в разделе "Как следует работать...".
# Ответить
23. vet7777 12.05.2013 15:13
Ну на конференцию едут ведь не только прорекламировать свои продукты, но и обменяться опытом. неужели на конференции нет блока 10-15 минутных докладов для обмена опытом?
Ответили: (24)
# Ответить
24. apatyukov 12.05.2013 16:07
(23) Александр как всегда скромничает о своем опыте, методичности, глубине знаний и уровне проектов которые ведет :)
Ответили: (25)
+ 1 [ alk; ]
# Ответить
25. speshuric 12.05.2013 16:49
(24) Ну что ты тут наговариваешь? Опыт у меня маленький - на 3 года примерно меньше чем у тебя, по методичности и знаниям - vet7777 которому ты отвечаешь за мной вчера шлакокод переписывал, проектов я вообще не веду. Врёшь ты всё! :)
# Ответить
26. TrinitronOTV 13.05.2013 06:46
спасибо автору за предоставленную статью...., очень для меня познавательно
# Ответить
27. DoctorRoza 13.05.2013 09:12
Классно! :) Однозначно + ! Чувствуется уровень 1С:Эксперта! Но! Хотелось бы больше примеров по оптимизации кода. Уверен, такая информация пойдет в раздел "КАК ИЗНАЧАЛЬНО ПРОГРАММИРОВАТЬ ПРАВИЛЬНО". Дополнительно, напишите статью как получить псевдокод из кода запроса 1С. Конечно, тут потребуется раскрыть и понятия SQL, пусть хоть на начальном уровне. Думаю, получится отличный цикл статей про связку СУБД + 1C. :)
+ 1 [ AllexSoft; ]
# Ответить
28. sitec-it 13.05.2013 10:08
Спасибо за статью. Сил прочитать все не хватило, но возьму на заметку приемы в запросах, хотя я и так знал, что поля составного типа - проблемные в плане производительности.
# Ответить
29. Evil Beaver 13.05.2013 11:20
Статья объемная, работа проделана большая, спасибо. Однако, все это уже есть на ИТС и давно разжевано-пережевано. Составные типы - удобная фича, но использоваться должна с умом.
Ответили: (30) (32)
# Ответить
30. headMade 13.05.2013 15:03
(29) Evil Beaver,
а в каком разделе можно найти на ИТС, или может дадите ссылку на its.1c.ru ?

Спасибо.
Ответили: (31)
# Ответить
31. Evil Beaver 13.05.2013 15:21
(30) headMade, честно говоря, нет времени искать. Поищите по словам "составной тип". Там где-то точно было черным-по-белому, как составной тип отображается в структуру таблиц СУБД и как нельзя делать с точки зрения производительности. В любом случае, текущая статья более чем исчерпывающая.
# Ответить
32. speshuric 13.05.2013 16:25
(29) Не соглашусь. На ИТС относительно небольшой объём информации. Структура хранения на ИТС есть. Индексы, которые строятся - есть. Общая рекомендация "получение данных через точку от полей составного типа" и то, как разыменуются поля значений составных типов - есть. Рекомендация не делать нессылочные субконто - где-то тоже кажется была. Есть еще замечание про ограничения индексов на составные типы (тоже в контексте субконто) и замечание про разницу NULL/Неопределено в контексте запросов и составных типов.
Зато нет:
  • Дополнительная существенноа деградация, если "получение данных через точку от полей составного типа" используется в ГДЕ или ПО. Сколько раз я переубеждал программистов, которые утверждали что "Регистр.Регистратор.Дата>&Дата" должен использовать индексы.
  • Почти полная бесполезность индексирования составных типов, содержащих несколько базовых (фактически индекс работает только для "ГДЕ СоставноеПоле = &Параметр")
  • Неочевидная потеря производительности на "В (&Список)"
  • Тема того, как используется "ВЫБОР КОГДА ТОГДА" и МИНИМУМ/МАКСИМУМ
  • Почти нет приемов, как обходить проблемы производительности (за исключением ВЫРАЗИТЬ, которое, кстати, не всегда работает)
Но в целом - да, если хоть иногда приходится смотреть ТЖ, трассу и планы запросов, то мегаоткровений тут нет. И подходы 1С к тому, как преобразовать язык запросов в SQL тоже предсказуемы, понятны и объяснимы. Собственно причины написания статьи изложены в (15) - мне просто нужно было консолидировать информацию по этой проблематике.
Ответили: (33)
# Ответить
33. Evil Beaver 13.05.2013 16:48
(32) speshuric, да слов нет, Ваша статья - просто отличная. Имхо, ее можно было бы подсократить, т.к. несмотря на фундаментальность она сводится к известным тезисам:

1. Составной тип - часто источник проблем
2. Ссылочные типы вперемешку с примитивными - зло
3. Получение полей от ссылочного типа через точку - зло
4. Пункты 2+3 одновременно - аццкий сотона зохавал всех
5. Если в запросе присутствует составное поле - нужно смотреть, как оно отобразится на реальный запрос.

Итого, мне немного не хватило best practices в статье - как можно делать. Как нельзя, в-общем, и так было понятно. Тем не менее, прошу не воспринимать, как критику. Ни в коем случае. Это просто размышление вслух. Статья отличная, спасибо еще раз. Про потерю производительности по "СоставныеСсылки В (&СписокСсылок)" не знал, был уверен, что индекс гарантированно используется.
Ответили: (34)
+ 2 [ Jogeedae; speshuric; ]
# Ответить
34. speshuric 13.05.2013 17:06
(33) в этих формулировках согласен на 146%
# Ответить
35. tango 13.05.2013 17:14
за ссылку отдельный +
"1с как продукт мысли" - звучит
В этой статье мы решили сконцентрироваться не на описании возможностей продукта "1С:Предприятия", а именно на технологических инновациях, которые в совокупности определили ряд совершенно новых технологий разработки бизнес-приложений и новых качеств самих этих приложений.
При этом до идеала еще далеко - все имеющиеся и создаваемые технологии являются компромиссными решениями, нацеленными на улучшение показателей систем по нескольким указанным критериям.
Ответили: (36)
# Ответить
36. speshuric 14.05.2013 07:47
(35) tango, я думаю, что не стоит придираться к хорошей маркетингово-популяризационной статье 2004 года. Тем более, что автор фактически не словом, но делом доказал, что они способны успешно развивать и улучшать продукт. Отдельное им спасибо за эволюционное, а не революционное развитие.
# Ответить
37. vlad.frost 14.05.2013 12:17
Отличная статья в тему про дырявые абстракции http://russian.joelonsoftware.com/Articles/LeakyAbstractions.html (если вдруг кто ещё не читал)
Ответили: (39)
# Ответить
38. Yashazz 14.05.2013 13:08
Да, спасибо. Тоже далеко не всё знал насчёт использования (а точнее, неиспользования) индексов. Надеялся, что оно есть, а взаправду вот как. Отличная статья!
# Ответить
39. speshuric 14.05.2013 13:20
(37) У Спольского вообще много хороших статей. Но вот дырявые абстракции - они на самом деле лежат очень многослойно вокруг нас.
Ответили: (40)
# Ответить
40. Evil Beaver 14.05.2013 16:07
(39) speshuric, ну Спольски вообще любит покрасоваться, мол, во какую я штуку всем открыл, абстракции, оказывается, построены друг на друге ;) Одна мысль там в этой статье мне нравится, дословно не помню, смысл такой, что несмотря на все современные инструменты и абстракции я все равно обязан знать то, что написано у Кернигана&Ричи, иначе мне, как спецу, каюк. Это прям эпиграф к Вашей статье. Составной тип клево - но нужно знать, что он есть на самом деле
Ответили: (41) (42)
# Ответить
41. vlad.frost 14.05.2013 17:24
(40) Позволю себе ещё одну цитату: "Вычислительная техника - это дисциплина, в которой считается, что все проблемы можно решить благодаря введению одного или нескольких уровней косвенности." © Деннис Де Брюле

То есть в нашем деле без абстракций вообще никуда. Абстракции удобны, иначе нам всем пришлось бы кодировать в машинных кодах на перфокартах. Просто нужно знать, что иногда они дают протечки. Профессионалы, зная как реализована та или иная абстракция, могут пользоваться ей эффективнее.
# Ответить
42. speshuric 14.05.2013 18:05
(40) Ну Спольскому с его особенностями простительна "любовь покрасоваться". А в 1С до фига и других дуршлагов-абстракций. Начиная со строк и арифметики:
  • Арифметика пытается усидеть на 2 стульях (точные значения и плавающая точка). В результате производительность нигде, артефакты на интеграции так и прут, и СУБД про такую арифметику ваще не в курсе. Когда я вижу что браузерs в JavaScript фигачат до 500 млн операций с числами в секунду, то вспоминая 1С прям аж... Прям аж...
  • Строки. Я не буду вспоминать эпопею с преобразованиями crlf. Я промолчу про то, что интерпретатор 1С считает буквами, а что нет (вопрос, кстати, не так тривиален :) ). Давайте лучше про конкатенацию. А = А + "Ы" - что может быть проще. Пока этих выражений не 100500. Возникает глупая ситуация, когда для большого запроса к внешней системе основное время уходит на создание запроса, а не на выполнение или разбор. StringBuilder отсутствует. ТекстовыйДокумент не спасает. Зато спасает (кто бы мог подумать) ЗаписьXML:
    ТекстПостроитель = Новый ЗаписьXML;
    ТекстПостроитель.УстановитьСтроку();
    ТекстПостроитель.ЗаписатьБезОбработки("Кусочек строки");
    ТекстПостроитель.ЗаписатьБезОбработки(", и еще один");
    СтрокаРезультат = ТекстПостроитель.Закрыть();
    ...Показать Скрыть

    Я об этом писал у себя в бложике
Реально, куда ни копни - герметичности этих абстракций нет. Ну да и ладно. Зато вентиляция :)
Ответили: (46) (47) (52)
# Ответить
43. scape 15.05.2013 07:21
Полезная информация. Благодарю. Но зачем страх нагонять, используя обороты "ужас", "кошмар", "зло" в технической статье? Пойду застрелюсь...
+ 1 [ ftm; ]
# Ответить
44. higs 15.05.2013 07:49
А мне понравилось. Достаточно глубокая проработка и исследование, далеко не всегда доходят руки так глубоко лезть в скуль. Конечно, пока будешь приучать себя так писать запросы, производителность собственная упадет.
Есть только некие неточно сформулированные в голове противоречия для использования рекомендованных правил из статьи и рекомендаций по работе в СКД. Например, в СКД рекомендовано отборы оставлять для выбора пользователя. Очень часто, это как раз составной тип или будет при соединении. Ну и еще там есть.
Как я понимаю, для СКД все еще страшнее и ужаснее.

За статью просто твердущий плюс!
# Ответить
45. zqzq 15.05.2013 08:44
Статья во многом пересекается с приложением книги "Проф. разработка - реализация прикладных задач в системе 8.2", там с табличками и примерами расписаны особенности и неприятности составных типов (а также вообще хранение данных). Тут в статье более сумбурно и подробно мне показалось, в книге более структурировано и кратко.
Ответили: (49)
# Ответить
46. Evil Beaver 15.05.2013 09:54
(42) speshuric, ЗаписьXML - это стрингбилдер? Аааааа ну точно же! Чорт, как все просто иногда оказывается! Мегареспект!!!!

Про числа не понял... меня особо не докучала никогда производительность численных операций. А что за задачи?
# Ответить
47. ildarovich 15.05.2013 11:20
(42) Насчет
А = А + "Ы"
написано в статье Опять двойка Там предлагается для решения проблемы
первоначально работать с такими длинными строками как с массивом символов (строк), а затем преобразовывать массив символов(строк) в результирующую строку с помощью простейшей рекурсивной функции
. Интересно теперь будет сравнить по быстродействию с решением через ЗаписьXML. Кстати, проблема А = А + "Ё" существует в большинстве языков. Смотрите, например, Строки в C# и .NET / Хабрахабр.
Ответили: (50)
# Ответить
48. bulpi 15.05.2013 14:58
Все хорошо. Единственное замечание. Когда мне ради оптимизации предлагают заменить запрос в 3 строки на запрос в 30 строк, я останавливаюсь и начинаю думать :) Когда через полгода я (или не дай Бог не я!) снова полезет в этот модуль, то количество МОЕГО драгоценного времени ( а не железного компьютерного), потраченного на понимание , чего тут творится, вырастет в N раз и резко сократит общий выигрыш от оптимизации (аж до минуса). Так что я буду продолжать использовать вариант Регистратор.Основание.Контрагент=&Контрагент.
Ответили: (49)
# Ответить
49. speshuric 15.05.2013 16:36
(45) Да, 2-й том, страница 670 :) Но там не разобраны моменты с ВЫБОР и МАКСИМУМ, на грабли которых мы наступили. Если честно, то я пока долистываю первый том: на работе как-то не до чтения, домой купил недавно и дома больше 50-70 страниц в день не успеваю проработать.
(48) Пожалуйста-пожалуйста. Лишь бы это было осознанным выбором, и вы умели видеть и лечить свои же грабельки. У меня вот, например, стоимость месяца работы меньше стоимости пятиминутного простоя некоторых ИБ предприятия.
Ответили: (91)
+ 3 [ CratosX; apatyukov; kiros; ]
# Ответить
50. speshuric 15.05.2013 22:33
(47) Проверил. ЗаписьXML быстрее (для большого количества конкатенаций, конечно).
Ответили: (51)
+ 1 [ ildarovich; ]
# Ответить
51. ildarovich 15.05.2013 22:59
(50) Спасибо! - Буду иметь ввиду. Еще перепроверил, действительно, формирование строки из миллиона "Ы" занимает при помощи функции
Функция ДлиннаяСтрокаЧерезЗаписьXML(Длина, Чего = "") Экспорт
	Запись = Новый ЗаписьXML;
	Запись.УстановитьСтроку(); 
	Чего = Лев(Чего + " ", 1);
	Для ё = 0 По Длина - 1 Цикл Запись.ЗаписатьБезОбработки(Чего) КонецЦикла;
	Возврат Запись.Закрыть()
КонецФункции
...Показать Скрыть
примерно 2 секунды. Ближайший конкурент через представление массива при помощи функции
Функция ДлиннаяСтрокаЧерезПредставлениеМассива(Длина, Чего = "") Экспорт
	Массив = Новый Массив(Длина);
	Чего = Лев(Чего + " ", 1);
	Для ё = 0 По Длина - 1 Цикл Массив[ё] = Чего КонецЦикла; //на это уходит больше половины времени, а половина от этого просто на цикл!!!
	сё = Сред(СтрЗаменить(ЗначениеВСтрокуВнутр(Массив), """}," + Символы.ПС + "{""S"",""", ""), 53 + СтрДлина(Формат(Массив.Количество(), "ЧГ=")));
	Возврат Лев(сё, СтрДлина(сё) - 6)
КонецФункции
...Показать Скрыть
отстает на 12 процентов с результатом 2,5 секунды.
# Ответить
52. tormozit 15.05.2013 23:53
(42) Про ЗаписьXML я сам не догадался. Теперь буду применять. Спасибо!
# Ответить
53. LexSeIch 16.05.2013 11:18
Мир этому дому! Большое спасибо за серьезную статью. Информации много и она интересная, а тем более это не компиляция доков, а практически проверенно. Большой плюс.
# Ответить
54. kiros 16.05.2013 12:01
Александр, огромное спасибо за статью! Теперь в голове расставилось все по полочкам. А применение на практике дало просто колоссальный результат (а кто бы мог подумать, где собака порылась). Как то раньше сильно надеялся на платформу, а оказывается... буду сильнее изучать эту тему. ОГРОМНОЕ СПАСИБО.
# Ответить
55. DenisVT 16.05.2013 18:08
Спасибо огромное за статью. Прочитал как на духу. Написано очень подробно и особенно радуют примеры. Всегда радовался составным типам. А теперь вернулся на землю ) и уж точно буду использовать их впредь аккуратно.
# Ответить
56. uinx 17.05.2013 09:12
Помогите ускорить выполнение запроса:

Параметры:
"Подразделения" - одно значение - СправочникСсылка
"МестоХранения" - одно значение - СправочникСсылка
"Поставщик" - одно значение - СправочникСсылка
"сзПроизводители" - список значений вида - СправочникСсылка, не иерархический справочник
"СтатусТовара" - список значений вида - ПеречислениеСсылка
"Сезонность" - одно значение - ПеречислениеСсылка
"Формат" - одно значение - ПеречислениеСсылка
"Номенклатура" - список значений СправочникСсылка в котором и группы и элементы
"ДатаК" - одно значение - Дата
"Формат" - одно значение - СправочникСсылка
"НеактивнаяНоменклатура" - список значений СправочникСсылка в котором и группы и элементы
"НачПериода" - одно значение - Дата
"КонПериода" - одно значение - Дата
"ТекДата" - одно значение - Дата
"ТекФормат" - одно значение - Справочник.Ссылка
"ВидОперации" - список значений вида: ПеречислениеСсылка
"ВПСписаниеПроизводство" - одно значение - ПеречислениеСсылка




ВЫБРАТЬ
	ПрайсЛист.Номенклатура КАК Номенклатура,
	Продажи.ДеньПродажи КАК ДеньПродажи,
	(Продажи.КоличествоПродажи) КАК КоличествоПродажи,
	(Остатки.КоличествоОстаток) КАК Остаток,
   естьNULL(ОстаткиДляВозвратаП.Количество,0) как ОстаткиДляВозврата
ИЗ ПрайсЛист КАК ПрайсЛист
		ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ
			НАЧАЛОПЕРИОДА(Продажи.Период,День) КАК ДеньПродажи,
			Продажи.Номенклатура КАК Номенклатура,
			СУММА(Продажи.КоличествоОборот) КАК КоличествоПродажи
		ИЗ                    
			РегистрНакопления.Продажи.Обороты(
					&НачПериода,
					&КонПериода,
					День
					,ВидОперации В (&ВидОперации)  И (Подразделение В (&Подразделения) ИЛИ Подразделение В ИЕРАРХИИ (&Подразделения))
						 И (Номенклатура.Реквизиты.Сезонность В (&Сезонность))
						 И (Номенклатура В (&Номенклатура)
							ИЛИ Номенклатура В ИЕРАРХИИ (&Номенклатура))) КАК Продажи
	
		СГРУППИРОВАТЬ ПО 
			Продажи.Номенклатура,
			НАЧАЛОПЕРИОДА(Продажи.Период,День) 
ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
	НАЧАЛОПЕРИОДА(ПартииНоменклатуры.Период, ДЕНЬ),
	ПартииНоменклатуры.Номенклатура,
	СУММА(ПартииНоменклатуры.Количество)
ИЗ
	РегистрНакопления.ПартииНоменклатуры КАК ПартииНоменклатуры
ГДЕ
	ПартииНоменклатуры.ВидОперации = &ВПСписаниеПроизводство
	И ПартииНоменклатуры.Период МЕЖДУ &НачПериода И &КонПериода  И (Подразделение В (&Подразделения) ИЛИ Подразделение В ИЕРАРХИИ (&Подразделения))
						И (Номенклатура.Реквизиты.Сезонность В (&Сезонность))
						И (Номенклатура В (&Номенклатура)
							ИЛИ Номенклатура В ИЕРАРХИИ (&Номенклатура))
СГРУППИРОВАТЬ ПО
	ПартииНоменклатуры.Номенклатура,
	НАЧАЛОПЕРИОДА(ПартииНоменклатуры.Период, ДЕНЬ)
      ) КАК Продажи
		ПО ПрайсЛист.Номенклатура = Продажи.Номенклатура
		ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ
			Остатки.Номенклатура КАК Номенклатура,
			Остатки.КоличествоОстаток КАК КоличествоОстаток
		ИЗ 
			РегистрНакопления.ПартииНоменклатуры.Остатки(
					&ТекДата,
				(Номенклатура В (&Номенклатура)  ИЛИ Номенклатура В ИЕРАРХИИ (&Номенклатура))
					И (Номенклатура.Реквизиты.Сезонность В (&Сезонность)) И (Подразделение В (&Подразделения) ИЛИ Подразделение В ИЕРАРХИИ (&Подразделения)) И (Не МестоХранения.Хард)) КАК Остатки
			) КАК Остатки
		ПО ПрайсЛист.Номенклатура = Остатки.Номенклатура
		ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ
			ВозвратПоставщикутНоменклатура.Номенклатура,
			СУММА(ВозвратПоставщикутНоменклатура.Количество * ВозвратПоставщикутНоменклатура.Единица.Коэффициент) КАК Количество
		ИЗ
			Документ.ВозвратПоставщику.тНоменклатура КАК ВозвратПоставщикутНоменклатура
		ГДЕ
			ВозвратПоставщикутНоменклатура.Ссылка.Проведен
			И ВозвратПоставщикутНоменклатура.Ссылка.ПредварительноеПроведение
           И 
				(Номенклатура В (&Номенклатура)  ИЛИ Номенклатура В ИЕРАРХИИ (&Номенклатура))
					И (Номенклатура.Реквизиты.Сезонность В (&Сезонность)) И (Ссылка.Подразделение В (&Подразделения) ИЛИ Ссылка.Подразделение В ИЕРАРХИИ (&Подразделения)) И (Не Ссылка.Склад.Хард)
		СГРУППИРОВАТЬ ПО
			ВозвратПоставщикутНоменклатура.Номенклатура
			) КАК ОстаткиДляВозвратаП
		ПО ПрайсЛист.Номенклатура = ОстаткиДляВозвратаП.Номенклатура
УПОРЯДОЧИТЬ ПО
	ПрайсЛист.Номенклатура.Наименование
ИТОГИ
	СУММА(КоличествоПродажи),
	СРЕДНЕЕ(Остаток),
   СРЕДНЕЕ(ОстаткиДляВозврата)
ПО                                
	Номенклатура ,
	ДеньПродажи
...Показать Скрыть
Ответили: (59) (60)
# Ответить
57. burlakov 17.05.2013 10:46
Статья просто шикарная. На часть вещей просто глаза открылись. Даже не думал, что от составных типов может быть так плохо, хотя и зная о проблемах с производительностью составные типы практически не использую в своих разработках.
# Ответить
58. markgenuine 17.05.2013 15:50
Автор молодец, узнал много интересного, написано очень грамотно, подробно и понятно даже любому пользователю.
Спасибо.
# Ответить
59. speshuric 17.05.2013 16:48
(56) uinx, а какая структура регистров? И как организовано "Номенклатура.Реквизиты.Сезонность"?
# Ответить
60. speshuric 17.05.2013 19:43
(56) uinx,

1. Вообще, конечно, оптимизировать запрос вне контектса - это баловство и гадание на кофейной гуще. Он же зависит от структуры данных, от наличия индексов, от объёмов данных и от распределения частотной статистики по этим объёмам. Технология при этом простая: берём ТЖ, снимаем SDBL и SQL запросы, план, думаем, смотрим, устраняем самое тормозное, повторяем.

2. Части параметров в запросе не нашлось:
  • не найден в запросе "МестоХранения" - одно значение - СправочникСсылка
  • не найден в запросе "Поставщик" - одно значение - СправочникСсылка
  • не найден в запросе "сзПроизводители" - список значений вида - СправочникСсылка, не иерархический справочник
  • не найден в запросе "СтатусТовара" - список значений вида - ПеречислениеСсылка
  • не найден в запросе "Формат" - одно значение - ПеречислениеСсылка
  • не найден в запросе "ДатаК" - одно значение - Дата
  • опять не найден в запросе "Формат" - одно значение - СправочникСсылка
  • не найден в запросе "НеактивнаяНоменклатура" - список значений СправочникСсылка в котором и группы и элементы
  • не найден в запросе "ТекФормат" - одно значение - Справочник.Ссылка

3. Но в принципе даже без плана видны некоторые моменты (я предполааю, что запрос в пакете и на MS SQL)^
3.1. "Поле В (&Поле) ИЛИ Поле В ИЕРАРХИИ (&Поле)" - бессмысленно, т.к. В ИЕРАРХИИ включает В
3.2. во временной таблице ПрайсЛист - нужен индекс по Номенклатура
3.3. В MS SQL нужно попытаться вынести фильтры "В", "В ИЕРАРХИИ" и "Поле1.Поле2 = &Парам" наружу запросов к виртуальным таблицам остатков и оборотов. Почему - одна из тем для следующих статей. Если коротко, то это лишние соединения
3.4. В запросе есть корявый подзапрос к документам. Не зря 1С за такие запросы на экзамене по спецу отправляет на пересдачу.

4. Итого можно попробовать примерно следующее:
// Заранее получим номенклатуру из ПрайсЛист во временную индексированную таблицу. Заодно наименование подтянем 
ВЫБРАТЬ
	Номенклатура.Ссылка КАК Номенклатура,
	Номенклатура.Наименование КАК НоменклатураНаименование
ПОМЕСТИТЬ ФильтрНоменклатура
ИЗ
	Справочник.Номенклатура КАК Номенклатура
ГДЕ
	Номенклатура.Реквизиты.Сезонность В (&Сезонность) И
	Номенклатура.Ссылка В ИЕРАРХИИ (&Номенклатура) И
	Номенклатура.Ссылка В (ВЫБРАТЬ Номенклатура ИЗ ПрайсЛист)
// Может быть индекс Номенклатура, НоменклатураНаименование будет удачнее - надо смотреть план
ИНДЕКСИРОВАТЬ ПО НоменклатураНаименование, Номенклатура; 

// Подразделения тоже в ВТ
ВЫБРАТЬ
	Подразделения.Ссылка КАК Подразделение
ПОМЕСТИТЬ ФильтрПодразделения
ИЗ
	Справочник.Подразделения КАК Подразделения
ГДЕ
	Подразделения.Ссылка В ИЕРАРХИИ (&Подразделения)
ИНДЕКСИРОВАТЬ ПО Подразделение; 

ВЫБРАТЬ 
	ФильтрНоменклатура.Номенклатура КАК Номенклатура,
	// поле НоменклатураНаименование можно убрать - его не было в начальном запросе, 
	// но если это всё отчет, то лучше оставить для вывода в табличный документ как представления.
	ФильтрНоменклатура.НоменклатураНаименование КАК НоменклатураНаименование, 
	СуммированныеПродажи.ДеньПродажи КАК ДеньПродажи,
	СУММА(СуммированныеПродажи.КоличествоПродажи) КАК КоличествоПродажи,
	// Максимум/минимум считать проще, чем среднее, а разницы в данном случае нет
	МАКСИМУМ(СуммированныеОстаткиИВозвраты.КоличествоОстаток) КАК Остаток,
	МАКСИМУМ(СуммированныеОстаткиИВозвраты.ОстаткиДляВозврата) КАК ОстаткиДляВозврата
ИЗ
	ФильтрНоменклатура КАК ФильтрНоменклатура
	ЛЕВОЕ СОЕДИНЕНИЕ
		(ВЫБРАТЬ
			ОстаткиИВозвраты.Номенклатура КАК Номенклатура,
			СУММА(ОстаткиИВозвраты.КоличествоОстаток) КАК КоличествоОстаток,
			СУММА(ОстаткиИВозвраты.ОстаткиДляВозврата) КАК ОстаткиДляВозврата,
		ИЗ
			(ВЫБРАТЬ
				Остатки.Номенклатура КАК Номенклатура,
				Остатки.Подразделение КАК Подразделение,
				Остатки.МестоХранения КАК Склад,
				Остатки.КоличествоОстаток КАК КоличествоОстаток,
				0 КАК ОстаткиДляВозврата
			ИЗ
				РегистрНакопления.ПартииНоменклатуры.Остатки(&ТекДата) // условий в вирт. таблице нет сознательно
			ОБЪЕДИНИТЬ ВСЕ
			ВЫБРАТЬ 
				// В этом подзапросе нет хорошего фильтра по документам (в принципе понятно почему). Нужно бы сделать,
				// чтобы при ПредварительноеПроведение делались движения в какой-то РН.
				ВозвратПоставщикутНоменклатура.Номенклатура, // сильно надеюсь, что хотя бы тут есть индекс
				ВозвратПоставщику.Подразделение,
				ВозвратПоставщику.Склад,
				0,
				ВозвратПоставщикутНоменклатура.Количество * ВозвратПоставщикутНоменклатура.Единица.Коэффициент КАК Количество
			ИЗ
				Документ.ВозвратПоставщику.тНоменклатура КАК ВозвратПоставщикутНоменклатура
				ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.ВозвратПоставщику КАК ВозвратПоставщику
				ПО ВозвратПоставщику.Ссылка = ВозвратПоставщикутНоменклатура.Ссылка
			ГДЕ
				ВозвратПоставщику.Проведен И
				ВозвратПоставщику.ПредварительноеПроведение) КАК ОстаткиИВозвраты
		ГДЕ
			ОстаткиИВозвраты.Подразделение В 
				(ВЫБРАТЬ ФильтрПодразделения.Подразделение ИЗ ФильтрПодразделения КАК ФильтрПодразделения) И
			(НЕ ОстаткиИВозвраты.Склад.Хард)
		СГРУППИРОВАТЬ ПО
			ОстаткиИВозвраты.Номенклатура) КАК СуммированныеОстаткиИВозвраты
	ПО
		СуммированныеОстаткиИВозвраты.Номенклатура = ФильтрНоменклатура.Номенклатура
		
	ЛЕВОЕ СОЕДИНЕНИЕ
		(ВЫБРАТЬ
			ПродажиИПартии.ДеньПродажи КАК ДеньПродажи,
			ПродажиИПартии.Номенклатура КАК Номенклатура,
			СУММА(ПродажиИПартии.КоличествоПродажи) КАК КоличествоПродажи
		ИЗ
			(ВЫБРАТЬ
				Продажи.Период КАК ДеньПродажи,
				Продажи.Номенклатура КАК Номенклатура,
				Продажи.Подразделение КАК Подразделение,
				Продажи.КоличествоОборот КАК КоличествоПродажи
			ИЗ
				// в данном запросе может оказаться эффективным отказаться от виртуальной табилцы, но 
				// опять же - это можно сказать только по плану запроса
				РегистрНакопления.Продажи.Обороты(
					&НачПериода,
					&КонПериода,
					День,
					ВидОперации В (&ВидОперации)) КАК Продажи
			ОБЪЕДИНИТЬ ВСЕ
			ВЫБРАТЬ
				//Здесь есть потенциальная некорректность: не анализируется Активность и ВидДвижения
				НАЧАЛОПЕРИОДА(ПартииНоменклатуры.Период, ДЕНЬ),
				ПартииНоменклатуры.Номенклатура, 
				ПартииНоменклатуры.Подразделение,
				ПартииНоменклатуры.Количество
			ИЗ
				РегистрНакопления.ПартииНоменклатуры КАК ПартииНоменклатуры
			ГДЕ
				ПартииНоменклатуры.ВидОперации = &ВПСписаниеПроизводство И
				ПартииНоменклатуры.Период МЕЖДУ &НачПериода И &КонПериода) КАК ПродажиИПартии
		ГДЕ
			ПродажиИПартии.Подразделение В 
				(ВЫБРАТЬ ФильтрПодразделения.Подразделение ИЗ ФильтрПодразделения КАК ФильтрПодразделения)
		СГРУППИРОВАТЬ ПО
			ПродажиИПартии.ДеньПродажи,
			ПродажиИПартии.Номенклатура) КАК СуммированныеПродажи
	ПО 
		СуммированныеПродажи.Номенклатура = ФильтрНоменклатуры.Номенклатура
СГРУППИРОВАТЬ ПО
	Фильтр.Номенклатура, 
	Фильтр.НоменклатураНаименование,
	СуммированныеПродажи.ДеньПродажи
УПОРЯДОЧИТЬ ПО
	НоменклатураНаименование
ИТОГИ
	СУММА(КоличествоПродажи),
	МАКСИМУМ(Остаток),
	МАКСИМУМ(ОстаткиДляВозврата)
ПО
	Номенклатура,
	ДеньПродажи
...Показать Скрыть


Мог ошибиться, конечно.
Ответили: (67) (72) (73)
# Ответить
61. Sorm 19.05.2013 10:08
Хорошая грамотная статья, да только кто сначала будет разбираться в причинах резкого замедления исполнения запроса, когда "одно поле поменяли"? Скажут - "оборудование не тянет, давайте новое купим", ну или на крайний случай - 120 тыс рублей. Что интересно - когда на новом оборудовании запрос будет работать также(ну или чуть быстрее) вот тогда уже начинается "1С - г... " или там три конверта:), но чтобы профайлер открыть - "а что это"? Пример - недавно на мисте обсуждали какой-то запрос, что-то там запрос к виртуальным регистрам, с невъ... количеством джоинов по регистратору(здравствуйте, вычисляемые поля), так в профайлер в теме влезло два человека:).
# Ответить
62. mikhailovaew 20.05.2013 17:33
Статья феноменальная. Огромное спасибо автору за труд, систематизацию и оформление. За отстутствие снобизма, за готовность отвечать на комментарии.
Завидую вашим коллегам, как прекрасно, когда есть у кого перенять опыт! А то сидишь единственным программистом на фикси, и в свой же говнокод и ткнуть некому...
А теперь вопрос нуба: как неправильный пример приводится запрос "Регистр.Регистратор.Дата>&Дата". Имеется в виду, что отбор по дате должен идти через Период соответствующего регистра? А если нужно именно сделать отбор по каким-то свойствам регистратора, то как это правильно оформить в запросе? Возможно, потребность такого отбора сигнализирует о неправильной архитектуре регистра, но допустим, что регистр менять мы не будем, а отбор в запросе реализовать надо.
Ответили: (63)
# Ответить
63. speshuric 20.05.2013 19:32
(62) mikhailovaew:
1. Отбор замедлится по любым реквизитам поля составного типа. Дата или не дата - тут не важно
2. Архитектура, она обычно не в каменных скрижалях высечена и кривую архитектуру нужно менять. Пусть не за один день и не останавливая другие задачи, но менять придётся. Кроме изменения регистра есть еще как минимум прием - создание нового регистра.
3. Как костыльный вариант (один из многих):
ВЫБРАТЬ что-то 
ИЗ РегистрНакопления.МойРегистр КАК МойРегистр
ГДЕ 1 В 
    (ВЫБРАТЬ 1 ИЗ Документ.ДокументРегистратор1 КАК Док ГДЕ Док.Ссылка = МойРегистр.Регистратор И Док.Дата>&Дата
    ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 1 ИЗ Документ.ДокументРегистратор2 КАК Док ГДЕ Док.Ссылка = МойРегистр.Регистратор И Док.Дата>&Дата
    ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 1 ИЗ Документ.ДокументРегистратор3 КАК Док ГДЕ Док.Ссылка = МойРегистр.Регистратор И Док.Дата>&Дата
    ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 1 ИЗ Документ.ДокументРегистратор4 КАК Док ГДЕ Док.Ссылка = МойРегистр.Регистратор И Док.Дата>&Дата
    ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 1 ИЗ Документ.ДокументРегистратор5 КАК Док ГДЕ Док.Ссылка = МойРегистр.Регистратор И Док.Дата>&Дата
    ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 1 ИЗ Документ.ДокументРегистратор6 КАК Док ГДЕ Док.Ссылка = МойРегистр.Регистратор И Док.Дата>&Дата
    ОБЪЕДИНИТЬ ВСЕ .......
    ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 1 ИЗ Документ.ДокументРегистраторN КАК Док ГДЕ Док.Ссылка = МойРегистр.Регистратор И Док.Дата>&Дата)
...Показать Скрыть

Но потом править эту "прелесть" - проще застрелиться сразу. А когда таких запросов 500 в конфигурации?
Ответили: (65)
# Ответить
64. mikhailovaew 20.05.2013 20:24
(64) согласна, запрос душераздирающий.
спасибо за ответ )
Ответили: (64)
# Ответить
65. slazzy 23.05.2013 15:13
(63) speshuric, здравствуйте. Статья на самом деле классная. В общем очень радует эта тенденция, наконец появляются серьезные технические статьи.

Подскажите, где и вообще что можно читать в подобной тематике для начинающего программиста, который таки хочет стать хорошим программистом? ) есть ли вообще подобная литература/статьи, или единственный выход это исследовать самому?(ну помимо статей, которые есть на инфостарте)
Ответили: (66)
# Ответить
66. speshuric 23.05.2013 16:32
(65) Вон, выше "проф. разработку" советуют - в ней вагон информации. Ну и в принципе - ИТС и книги 1С. Остальное - как-то так, самостоятельно.
# Ответить
67. Ish_2 18.06.2013 09:02
(60) Ты написал :
"3.2. во временной таблице ПрайсЛист - нужен индекс по Номенклатура"
В запросе также используется индексирование временной таблицы "ФильтрПодразделения"
Вопрос : Зачем ?
Оптимизатор запроса сам создаст индекс.
Причем в моих опытах на временных таблицах значительного размера явное индексирование приводило
к некоторому замедлению выполнения запроса как в файловом варианте так и в клиент-серверном.
Ответили: (68) (69)
# Ответить
68. speshuric 18.06.2013 14:39
(67) Ish_2,
1. Оптимизатор индекс не строит. Никогда. В лучшем случае добавит операцию "Sort".
2. Но и операция Sort это ни разу не индекс на самом деле. Где-то сортированная выборка лучше обхода индексов (особенно для небольших таблиц), где-то хуже. Важно, что для каждого соединения таблиц скорее всего придётся серверу сортировать отдельной операцией с отдельными затратами.
3. Сервер совсем не всегда правильно догадывается о том, нужно сортировать или не нужно. Индекс на ВТ - "прозрачный намёк" (я намёки жены примерно также понимаю: когда мусорный пакет она мне даёт в руки, тогда я его и выношу).
4. И, да, индексирование не бесплатное. Но если утверждается, что у запроса план кривой и запрос долго выполняется, то для относительно мелкой ВТ можно и проиндексировать исходя из последующих сортировок/отборов/соединений - хуже скорее всего не станет.
5. За файловый ничего говорить не буду. Я в нём никто, и не представляю как там ВТ сделаны.
# Ответить
69. Ish_2 18.06.2013 15:46
(68) "Оптимизатор индекс не строит. Никогда. В лучшем случае добавит операцию "Sort". "

Отлично. Рассматриваем клиент-сервер.
1. Я сделал запросом временную таблицу ВРЕМ1 из двух-трех полей с уникальным полем "Ключ" ( скажем, 1 000 000 записей ). Индекс не строю.
Аналогичным запросом получаю таблицу ВРЕМ2. Индекс для ВРЕМ2 также не строю.
Затем делаю соединение двух таблиц по полю "КЛЮЧ" и вывожу результат. Засекаю время TIME1.

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

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

1. почему явное индексирование временных таблиц приводит к резкому замедлению ?
2. кто же индексирует ВРЕМ1 и ВРЕМ2 перед соединением ?
# Ответить
70. Ish_2 18.06.2013 16:20
На всякий случай реальный текст запроса для п.1.
В нем закомментирована опция "Индексировать по " .
ВРЕМ1 и ВРЕМ2 отсортированы по разным полям.
ВЫБРАТЬ ПЕРВЫЕ 500000
	ОП_ФинансовыеТранзакцииОпера.Ссылка,
	ОП_ФинансовыеТранзакцииОпера.НомерБрони,
	ОП_ФинансовыеТранзакцииОпера.КодУслуги
ПОМЕСТИТЬ Врем1
ИЗ
	Документ.ОП_ФинансовыеТранзакцииОпера КАК ОП_ФинансовыеТранзакцииОпера

УПОРЯДОЧИТЬ ПО
	ОП_ФинансовыеТранзакцииОпера.НомерБрони
// Индексировать по ОП_ФинансовыеТранзакцииОпера.Ссылка	
;

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

УПОРЯДОЧИТЬ ПО
	ОП_ФинансовыеТранзакцииОпера.КодУслуги
//Индексировать по ОП_ФинансовыеТранзакцииОпера.Ссылка	
;

////////////////////////////////////////////////////////////­////////////////////
ВЫБРАТЬ
	Врем1.Ссылка,
	Врем1.НомерБрони,
	Врем1.КодУслуги,
	Врем2.Ссылка КАК Ссылка1,
	Врем1.НомерБрони как НомерБрони1,
	Врем1.КодУслуги  как КодУслуги1
ИЗ
	Врем1 КАК Врем1
		ВНУТРЕННЕЕ СОЕДИНЕНИЕ Врем2 КАК Врем2
		ПО Врем1.Ссылка = Врем2.Ссылка
...Показать Скрыть
Ответили: (71) (72) (73) (74) (79)
# Ответить
71. speshuric 19.06.2013 00:27
(70) Ish_2,
Я сделал БД с 1 млн документов с полями запроса (неиндексированными, строковыми, заполнены случайно приведёнными к строкам числами 1-1000). Проверил (на 8.3.3, она под рукой была). Нет разницы в 2 раза. Всё логично и нормально. Есдинственно, я поправил список полей третьего запроса (номер брони и код услуги из врем2 потянул), запихнул результаты в Врем3 и четвёртым запросом просто получал количество записей в нём (это чтобы не мерять скорость доставки кучи данных с сервера).
Берём в руки профалер и наблюдаем:
1. При индексировании ВТ создаётся кластеризованный индекс по указанному полю. Для вставки в этот индекс нужно упорядочить по этому полю. Операция сортировки явно есть в плане. В моём примере она съедает примерно до полсекунды-секунды выполнения для каждой временной таблицы. Создавать единственный кластеризованный индекс в ВТ до заполнения - логичное и разумное решение 1С, здесь никаких вопросов у меня нет.
2. В моём примере заполнение ВТ без индексов было примерно 5,5 с (по данным трасс), с индексами 6,5 с. Это если параллелизм SQL настроен по умолчанию. Если он отключен, то дольше (отключать или повышать порог приходится из-за нестабильных планов при больших объёмах данных): 7,1 и 8,5 с. Данные усреднены по нескольким запускам.
3. Зато у меня индексированные фактически при тех же планах запроса (с поправкой на разницу table/clustered index scan) сами выполнились заметно быстрее и статистику временных таблиц собрали быстрее. Итог "забегов" примерно одинаковый - около 9 секунд в режиме "с параллельными планами".

Но тут и ситуация для индексов, прямо скажем, не выигрышная. Что может повлиять на использованием индексов:
1. Если поля номер брони и код услуги индексированы. Тогда выборка данных из основной таблицы может ускориться, а прибавка "на индекс" сохраняется. Тогда разница времени заполнения временных таблиц таблиц может получиться более чем в 2 раза.
2. Сам запрос можно улучшить. Например, временное соединение заменить на В. Мы же знаем что поле "Ссылка" уникально и записи не должны "размножаться". В этом случае для индексированных ВТ план будет несколько лучше.
3. Планы запросов сильно меняются, если одна из таблиц сильно меньше второй. Тогда будет Index Seek по большой таблице и сканирование кластеризованного индекса по маленькой с соединением по merge join (что в данном случае, наверное, лучше чем hash join неиндексированного плана).

Вообще, я согласен, что индексировать ВТ может быть бесполезно, а иногда и вредно, но если есть хороший кандидат на индекс в относительно небольшой ВТ, то проще его создать. Если индекса нет, то я не буду его создавать, если нет явной причины его создания. Более того, я вообще далеко не самый ярый сторонник ВТ. Если пришлось создавать ВТ в запросе для оптимизации и эта ВТ используется 1-2 раза, то это явный костыль, которым мы прикрываем дыру в архитектуре решения или обходим техническое ограничение. Кстати, это видно и в данном примере. Запрос без ВТ во много раз быстрее запроса с ВТ (особенно, если построить нужные индексы).

Ответы на вопросы:
1. почему явное индексирование временных таблиц приводит к резкому замедлению ?
Потому что происходит лишняя сортировка данных. Возможно, что индексы в данном случае не используются, а остальные операции происходят достаточно быстро, поэтому эта лишняя сортировка слишком заметна. Кстати, возможно, что в других версиях индекс создаётся явно после вставки, а это точно дороже.
2. кто же индексирует ВРЕМ1 и ВРЕМ2 перед соединением ?
Никто не индексирует. Смотрите планы в ТЖ или в отладчике.

ЗЫ: я избегаю прямого сравнения времени выполнения. Если речь не идёт о многократной разнице, то это очень "непоказательный показатель":
1. Зависит от кеширования. В этом тесте мне было тупо лень каждый раз дропать кеш.
2. Не показывает поведение системы под нагрузкой: блокировки, дисковые операции, время процессоров - все главные потенциальные узкие места остаются за бортом.
3. Зависит от кучи факторов, например, от данных и не всегда от этих факторов зависит "линейно".
Ответили: (72)
# Ответить
72. Ish_2 19.06.2013 11:16
(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.
Так ?
Ответили: (73)
# Ответить
73. speshuric 19.06.2013 18:31
(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.
Ответили: (74)
# Ответить
74. Ish_2 20.06.2013 08:53
(73) Почему на выходных ? Не пойму .
Ты работаешь что ли ?

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

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


Добавлено :
Текст в (70) исправил "по-твоему" : последний запрос поместил в таблицу ВРЕМ3.
И тогда TIME1 ( время без индексации) меньше TIME2 ( время с индексацией) в ~ 3 раза.
Ответили: (76) (77) (78)
# Ответить
75. Al-X 20.06.2013 10:39
Спасибо автору, статья СУПЕР !!! Эт скока же запросов мне надо просмотреть ?!!! Самое обидное, буквально вчера я в отчете использовал составные типы через точку в условии :( .
Мда... думаю я еще долго буду перечитывать статью и оптимизировать свои запросы.
Еще раз спасибо за статью !!!
# Ответить
76. speshuric 20.06.2013 14:34
(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 - заранее создаётся индекс (и вставка тупит лишь минимально).
Ответили: (77)
# Ответить
77. Ish_2 20.06.2013 15:38
(76) Вот и выяснили расхождения в результатах.
Тем не менее , если представишь контрпример (74) будет совсем всё ясно.

P.S. Ну , на выходных , конечно.
# Ответить
78. speshuric 20.06.2013 16:10
(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
Ответили: (79)
# Ответить
79. Ish_2 20.06.2013 18:54
(78) Релиз 8.2.18.61. Твой запрос (78) скопирован в КонсольЗапросов.
Время без индексирования 75 сек. ( сред.время 2 и 3 запуска)
Время с индексированием 170 сек ( сред.время 2 и 3 запуска).

Здесь всё понятно .
И мы понимаем , что время без индексирования меньше всего лишь из-за кривости релиза 8.2.
Но нас-то интересовал принцип ,
т.е. нужно было в принципе добиться чтобы время с индексированием было меньше.
И ты этого добился при использовании "некривого" 8.3.

Дело тут , надо думать, в сложности выражения соединения ( у тебя "Таб1.Знч МЕЖДУ Таб2.Знч И Таб2.Знч2")
Здесь получаем , что преимущества индексированной таблицы становятся очевидными.
В (70) же использовалось "слишком" простое условие "Врем1.Ссылка = Врем2.Ссылка".
Ок. Спасибо !
Ответили: (80)
# Ответить
80. speshuric 20.06.2013 19:16
(79) тест запускал на чем? В смысле: какая версия скуля и что там с памятью и дисками? Типа могу подогнать, теперь уже понятно как. Там вопрос не в сложности выражения, а в том, что без индексов "МЕЖДУ Таб2.Знч И Таб2.Знч2" приводит к жестокому соединению с обходом по декартову произведению, а с индексами лишь выгребания небольших диапазонов. Остаётся лишь подобрать размеры таблиц. Как несложно заметить, тут индексированная таблица достаточно жирная.
Ответили: (81) (83)
# Ответить
81. Ish_2 20.06.2013 23:58
(80)
1. Сам я не очень-то .. Завтра спрошу у наших сисадминов.
2. "Там вопрос не в сложности выражения, а в том, что без индексов "МЕЖДУ Таб2.Знч И Таб2.Знч2" приводит к жестокому соединению с обходом по декартову произведению, а с индексами лишь выгребания небольших диапазонов."
Ну, да. Это и имелось ввиду.
В случае усложнения ( добавления новых условий) резко увеличивается объем пробега для этой самой "hash join".
# Ответить
82. Ish_2 21.06.2013 00:48
"А осадочек остался...".

Дело в том , что я ни секунды не сомневался оптимизатор запроса сам строит необходимые индексы ( пусть даже некие "временные" , "виртуальные")- если нужно ,после анализа условий соединения или фильтраций.
Фразу "не индексируйте временные таблицы при однократном последующем использовании" я помню "со школы".
Ведь еще при царе Горохе старые СУБД именно так создавали недостающие индексы - насколько помню.
В последнем твоем примере необходимость создания недостающего индекса также очевидна.

И тут на тебе : MSSQL этого не делает.
Неужто трудно проанализировать выражение соединения и создать индекс ?
А ORacle и прочие ?
Ответили: (84)
# Ответить
83. Ish_2 21.06.2013 10:54
(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
# Ответить
84. hogik 22.06.2013 01:11
(82)
""не индексируйте временные таблицы при однократном последующем использовании" я помню "со школы"."(с)
Игорь (Ish_2).
Помнить мало, надо еще понимать. ;-)
Эта фраза относится к другому случаю - если "однократно" применяется запрос выборки записей из таблицы. Т.к. при индексировании выполняется полный просмотр всех записей таблицы. Плюс, затраты на построение самого индекса. А потом еще запрос будет просматривать N-ое количество записей. Т.е. быстрее будет работать сразу запрос без (вместо) предварительного индексирования.
Вроде, это так очевидно? Даже, я это смог понять... ;-)
Ответили: (85)
# Ответить
85. Ish_2 22.06.2013 12:18
(84) Дело прошлое , догадки - дело скользкое. Кто там что понял ,а что - нет..
Пока ясно одно , MSSQL сам не создает недостающие индексы.
Вчера возмущенный этим фактом я написал в MS. Жду вот.
Думаю , к осени поправят.
Как считаете ?
Ответили: (86)
# Ответить
86. hogik 22.06.2013 16:08
(85)
"... к осени поправят ... ?"(с)
Игорь (Ish_2).
Нет. Но, включат в планы для "MS SQL 2021".
# Ответить
87. MrFlanker 25.12.2013 18:56
Отличная статья, прочитал на одном дыхании
# Ответить
88. Bukaska 26.12.2013 23:27
Отличная статья! Много интересного...
# Ответить
89. agrustny 26.04.2014 01:01
Это замечательная великолепная статья, я Вас недавно рекламировал!
# Ответить
90. Збянтэжаны Саўка 05.11.2014 12:07
ух-ты какая шикарная статья!!!
я в шоке - она меня просто потрясла!!!
спасибо!
# Ответить
91. Ndochp 05.02.2015 11:20
(49) Вот ты не пожалел своего времени, так как оно дешевле простоя предприятия и переписал по всей конфе в 100500 местах на "оптимальное" решение с ручным объединением всего и вся.
А потом кто-то сделал движение корректировкой записи регистров, про которую ты забыл. Или у этого регистра добавился регистратор. Какова вероятность, что ты поправишь не в 100500 местах, а в 100499, и предприятие не останавливаясь продолжит работать с неверными данными?
И не больше ли это убытков принесет?
# Ответить
92. It-developer 11.09.2015 11:07
На 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 байт} (на реквизит) и какое-то количество байт на всю строку
Ответили: (93)
# Ответить
93. speshuric 11.09.2015 12:57
(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С с двухбайтными буквами - это вряд ли где-то отличается. Дополнительные поля не учтены. Страницы не учтены. Дополнительные расходы из этих данных считать нельзя.
Ответили: (94)
# Ответить
94. It-developer 11.09.2015 16:28
(93) Спасибо!
Я не учёл _IDRRef (16 байт в SQL), _Marked (1 байт в SQL), _Version (8 байт в SQL) :(
_IsMetadata (1 байт в SQL) там нет, но есть пустое _PredefenedID
В общем 16+1+8 = 25 байт.
Т.е. в итоге 2 байта на символ строки :)
Моя цель исследования была понять сколько можно запихнуть данных в файловую версию в справочник :)
В итоге недосчитался и поэтому написал на форуме :)
Понятно, что все в основном пользуются SQL, но бывают разные исключения.
К составным типам да, мои тесты отношения не имеют

Вообще мне Ваша статья понравилась. Я ранее ее использовал в работе
Ответили: (95)
# Ответить
95. speshuric 11.09.2015 16:55
(94) It-developer,
Ну тогда всегда пожалуйста. Жаль, конечно, что для 1С нет аналога такой статьи, по которой можно достаточно точно определить сайзинг.
# Ответить
96. Sergey.Noskov 10.08.2016 18:16
Спасибо, то что надо для объяснения "молодым и дерзким" чего надо бояться в составных типах. Статья актуальности не потеряет.

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

В свое время, похожая на эту рекомендация, у нас спровоцировала появление в коде примерно таких условий:
… ГДЕ ВЫРАЗИТЬ(Таблица.ПолеСоставногоТипа КАК ТипСсылки) = &Ссылка
пришлось пояснять отдельно где ВЫРАЗИТЬ точно не надо делать.
Ответили: (97)
# Ответить
97. speshuric 23.08.2016 00:10
(96) Sergey.Noskov,
Ну дай дураку рекомендацию, он и руки порежет и рекомендацию извратит.
# Ответить
Внимание! За постинг в данном форуме $m не начисляются.
Внимание! Для написания сообщения необходимо авторизоваться
Текст сообщения*
Прикрепить файл






IE 2016