В этой статье рассматриваются некоторые менее известные функции и ограничения оптимизатора запросов и объясняются причины крайне низкой производительности хеш-соединения в конкретном случае.
В приведенном ниже примере сценария создания данных используется существующая таблица чисел. Если у вас его еще нет, приведенный ниже скрипт можно использовать для его эффективного создания. Результирующая таблица будет содержать один целочисленный столбец с числами от одного до миллиона:
С ДЕСЯТЬЮ (N) КАК (ВЫБРАТЬ 1 СОЕДИНЕНИЕ ВСЕ ВЫБРАТЬ 1 СОЮЗ ВСЕ ВЫБРАТЬ 1 СОЮЗ ВСЕ ВЫБРАТЬ 1 СОЮЗ ВСЕ ВЫБРАТЬ 1 СОЮЗ ВСЕ ВЫБРАТЬ 1 СОЮЗ ВСЕ ВЫБРАТЬ 1 СОЮЗ ВСЕ ВЫБРАТЬ 1 СОЮЗ ВСЕ ВЫБРАТЬ 1 СОЮЗ ВСЕ ВЫБРАТЬ 1) ВЫБРАТЬ ТОП (1000000 ) n = IDENTITY (int, 1, 1) INTO dbo. Номера ОТ Десять T10, Десять T100, Десять T1000, Десять T10000, Десять T100000, Десятка T1000000; ALTER TABLE dbo. Числа ADD CONSTRAINT PK_dbo_Numbers_n КЛАВИША PRIMARY CLUSTERED (n) WITH (SORT_IN_TEMPDB = ON, MAXDOP = 1, FILLFACTOR = 100);
Сам пример данных состоит из двух таблиц, T1 и T2. Оба имеют последовательный целочисленный столбец первичного ключа с именем pk и второй обнуляемый столбец с именем c1. Таблица T1 имеет 600 000 строк, где строки с четными номерами имеют то же значение для c1, что и столбец pk, а строки с нечетными номерами равны нулю. Таблица c2 имеет 32 000 строк, где столбец c1 равен NULL в каждой строке. Следующий скрипт создает и заполняет эти таблицы:
СОЗДАТЬ ТАБЛИЦУ dbo. T1 (целое число pk NOT NULL, целое число c1 NULL, CONSTRAINT PK_dbo_T1 КЛАВИША ПЕРВИЧНОГО КЛЮЧА (pk)); СОЗДАТЬ ТАБЛИЦУ dbo. T2 (целое число pk NOT NULL, целое число c1 NULL, CONSTRAINT PK_dbo_T2 КЛАВИША ПЕРВИЧНОГО КЛЮЧА (pk)); Вставить дбо. T1 С (TABLOCKX) (pk, c1) ВЫБРАТЬ N. n, случай, когда n. n% 2 = 1 ТОЛЬКО NULL ИЛИ N. И КОНЕЦ ИЗ ДБО. Числа КАК N, ГДЕ N. n между 1 и 600000; Вставить дбо. T2 С (TABLOCKX) (pk, c1) ВЫБРАТЬ N. n, NULL FROM dbo. Числа КАК N, ГДЕ N. n между 1 и 32000; ОБНОВЛЕНИЕ СТАТИСТИКИ ДБО. T1 с полным сканированием; ОБНОВЛЕНИЕ СТАТИСТИКИ ДБО. T2 с полной проверкой;
Первые десять строк образцов данных в каждой таблице выглядят так:
Этот первый тест включает в себя объединение двух таблиц в столбце c1 (не в столбце pk) и возвращение значения pk из таблицы T1 для строк, которые объединяются:
ВЫБЕРИТЕ T1. ПК ОТ ДБО. T1 AS T1 JOIN. T2 AS T2 ON T2. с1 = т1. с1;
Запрос фактически не возвращает строк, поскольку столбец c1 равен NULL во всех строках таблицы T2, поэтому ни одна строка не может соответствовать предикату равенства. Это может показаться странным, но я уверен, что он основан на реальном производственном запросе (значительно упрощенном для упрощения обсуждения).
Обратите внимание, что этот пустой результат не зависит от настройки ANSI_NULLS потому что это только контролирует, как обрабатываются сравнения с нулевым литералом или переменной. Для сравнения столбцов предикат равенства всегда отклоняет нули.
План выполнения этого простого запроса соединения имеет некоторые интересные особенности. Сначала мы рассмотрим предварительный («оценочный») план в SQL Sentry Plan Explorer :
Предупреждение на значке SELECT просто жалуется на отсутствующий индекс в таблице T1 для столбца c1 (с pk в качестве включенного столбца). Индексное предложение здесь не имеет значения.
Первый реальный интерес в этом плане - это фильтр:
Этот предикат IS NOT NULL не появляется в исходном запросе, хотя он подразумевается в предикате соединения, как упоминалось ранее. Интересно, что он был разбит как явный дополнительный оператор и помещен перед операцией соединения. Обратите внимание, что даже без фильтра запрос все равно будет давать правильные результаты - само соединение все равно будет отклонять пустые значения.
Фильтр любопытен и по другим причинам. Его предполагаемая стоимость равна нулю (хотя ожидается, что он будет работать на 32 000 строк), и он не был перенесен в сканирование кластерного индекса как остаточный предикат. Оптимизатор обычно очень заинтересован в этом.
Обе эти вещи объясняются тем фактом, что этот фильтр введен в перезаписи после оптимизации. После того как оптимизатор запросов завершит свою обработку на основе затрат, будет рассмотрено относительно небольшое количество переписок с фиксированным планом. Один из них отвечает за введение фильтра.
Мы можем видеть выходные данные выбора плана на основе стоимости (до перезаписи), используя недокументированные флаги трассировки 8607 и знакомый 3604 для направления текстового вывода на консоль (вкладка сообщений в SSMS):
Дерево вывода показывает хеш-соединение, два сканирования и некоторые операторы параллелизма (обмена). В столбце c1 таблицы T2 нет отклоняющего фильтра.
Конкретное переписывание постоптимизации смотрит исключительно на ввод сборки хеш-соединения. В зависимости от оценки ситуации он может добавить явный фильтр для отклонения строк, которые являются нулевыми в ключе соединения. Влияние фильтра на расчетное количество строк также записывается в план выполнения, но поскольку оптимизация на основе затрат уже завершена, стоимость фильтра не рассчитывается. В случае, если это не очевидно, вычислительные затраты являются пустой тратой усилий, если все основанные на затратах решения уже приняты.
Фильтр остается непосредственно на входе сборки, а не помещается в сканирование кластерного индекса, потому что основное действие по оптимизации завершено. Постоптимизация переписывает фактически последние изменения в готовый план выполнения.
Второе, и совершенно отдельное, постоптимизационное переписывание отвечает за оператор Bitmap в окончательном плане (вы могли заметить, что он также отсутствовал в выходных данных 8607):
Этот оператор также имеет нулевую оценочную стоимость как для ввода-вывода, так и для процессора. Другая вещь, которая идентифицирует его как оператор, введенный поздней настройкой (а не во время оптимизации на основе затрат), заключается в том, что его имя - Bitmap, за которым следует число. Есть и другие типы растровых изображений, представленные во время оптимизации на основе затрат, как мы увидим чуть позже.
На данный момент важным моментом в этом растровом изображении является то, что он записывает значения c1, которые были видны на этапе построения хеш-соединения. Готовое растровое изображение передается на сторону зонда соединения, когда хэш переходит от фазы сборки к фазе зонда. Битовая карта используется для раннего сокращения полусоединения, исключая строки со стороны зонда, которые не могут быть соединены. если вам нужно больше подробностей об этом, пожалуйста, смотрите мой предыдущая статья на предмет.
Второй эффект растрового изображения можно увидеть при сканировании кластерного индекса на стороне зонда:
На приведенном выше снимке экрана показано заполненное растровое изображение, проверяемое в рамках сканирования кластерного индекса по таблице T1. Так как исходный столбец является целым числом (bigint также будет работать), проверка растрового изображения помещается в механизм хранения (как указано в квалификаторе INROW), а не проверяется обработчиком запросов. В более общем смысле, битовая карта может применяться к любому оператору на стороне зонда, начиная с обмена. То, насколько далеко обработчик запросов может передать растровое изображение, зависит от типа столбца и версии SQL Server.
Чтобы завершить анализ основных характеристик этого плана выполнения, нам нужно взглянуть на план после выполнения («фактический»):
Первое, на что стоит обратить внимание, - это распределение строк по потокам между сканированием T2 и обменом потоками перераспределения непосредственно над ним. Во время одного теста я увидел следующий дистрибутив в системе с четырьмя логическими процессорами:
Распределение не особенно равномерно, как часто бывает для параллельного сканирования на относительно небольшом количестве строк, но по крайней мере все потоки получили некоторую работу. Распределение потоков между одним и тем же обменом потоками и фильтром сильно отличается:
Это показывает, что все 32 000 строк из таблицы T2 были обработаны одним потоком. Чтобы понять почему, нам нужно взглянуть на свойства exchange:
Этот обмен, как и в случае зонда на стороне хеш-соединения, должен гарантировать, что строки с одинаковыми значениями ключей соединения окажутся в одном и том же экземпляре хеш-соединения. В DOP 4 есть четыре хеш-соединения, каждое со своей хеш-таблицей. Для получения правильных результатов строки на стороне компоновки и строки на стороне зонда с одинаковыми ключами соединения должны прийти к одному и тому же хэш-соединению; в противном случае мы могли бы проверить строку на стороне зонда по неверной хеш-таблице.
В параллельном плане в режиме строки SQL Server достигает этого, перераспределяя оба входа, используя одну и ту же хэш-функцию в столбцах соединения. В данном случае объединение выполняется в столбце c1, поэтому входные данные распределяются по потокам путем применения хэш-функции (тип разделения: hash) к столбцу ключа объединения (c1). Проблема здесь заключается в том, что столбец c1 содержит только одно значение - нулевое - в таблице T2, поэтому всем 32 000 строк присваивается одинаковое значение хеш-функции, поскольку все они попадают в один и тот же поток.
Хорошей новостью является то, что все это не имеет значения для этого запроса. Фильтр перезаписи после оптимизации удаляет все строки до того, как будет проделана большая работа. На моем ноутбуке вышеуказанный запрос выполняется (без результатов, как и ожидалось) в течение примерно 70 мс .
Для второго теста мы добавляем дополнительное соединение из таблицы T2 к себе на его первичный ключ:
ВЫБЕРИТЕ T1. ПК ОТ ДБО. T1 AS T1 JOIN. T2 AS T2 ON T2. с1 = т1. c1 ПРИСОЕДИНЯЙТЕСЬ к dbo. T2 AS T3 - новый! НА Т3. рк = Т2. рк;
Это не меняет логических результатов запроса, но меняет план выполнения:
Как и ожидалось, самообъединение таблицы T2 по ее первичному ключу не влияет на количество строк, соответствующих этой таблице:
Распределение строк по потокам также хорошо в этом разделе плана. Для сканирования это аналогично предыдущему, поскольку параллельное сканирование распределяет строки по потокам по требованию. Обмен перераспределяется на основе хэша ключа соединения, который на этот раз является столбцом pk. Учитывая диапазон различных значений pk, результирующее распределение потоков также очень равномерно:
Обращаясь к более интересному разделу предполагаемого плана, есть некоторые отличия от теста с двумя таблицами:
Еще раз, обмен на стороне компоновки завершает маршрутизацию всех строк в один и тот же поток, потому что c1 является ключом соединения, и, следовательно, столбец разделения для обменов потоками перераспределения (помните, c1 равен нулю для всех строк в таблице T2).
В этом разделе плана есть два других важных отличия от предыдущего теста. Во-первых, отсутствует фильтр для удаления строк null-c1 со стороны сборки хеш-соединения. Объяснение этому связано со вторым отличием - битмап изменился, хотя это не очевидно из рисунка выше:
Это Opt_Bitmap, а не Bitmap. Разница в том, что это растровое изображение было введено во время оптимизации на основе затрат, а не путем переписывания в последнюю минуту. Механизм, который учитывает оптимизированные растровые изображения, связан с обработкой запросов типа «звезда». Логика объединения звезд требует как минимум трех соединенных таблиц, поэтому это объясняет, почему в примере объединения двух таблиц не было рассмотрено оптимизированное растровое изображение.
Это оптимизированное растровое изображение имеет ненулевую оценочную стоимость ЦП и напрямую влияет на общий план, выбранный оптимизатором. Его влияние на оценку мощности на стороне зонда можно увидеть у оператора Repartition Streams:
Обратите внимание, что на обмене наблюдается эффект кардинальности, даже несмотря на то, что растровое изображение в конечном итоге выталкивается полностью в механизм хранения («INROW»), как мы видели в первом тесте (но обратите внимание на ссылку Opt_Bitmap сейчас):
План после исполнения («фактический») выглядит следующим образом:
Прогнозируемая эффективность оптимизированного растрового изображения означает, что отдельная перезапись после оптимизации для нулевого фильтра не применяется. Лично я думаю, что это неудачно, потому что удаление пустых значений с помощью фильтра на раннем этапе устраняет необходимость в создании растрового изображения, заполнении хеш-таблиц и выполнении расширенного растрового сканирования таблицы T1. Тем не менее, оптимизатор решает иначе, и в этом случае спорить с ним просто невозможно.
Несмотря на дополнительное самостоятельное объединение таблицы T2 и дополнительную работу, связанную с отсутствующим фильтром, этот план выполнения по-прежнему дает ожидаемый результат (без строк) в короткие сроки. Типичное исполнение на моем ноутбуке занимает около 200 мс .
Для этого третьего теста мы изменим тип данных столбца c1 в обеих таблицах с целого на десятичный. В этом выборе нет ничего особенного; тот же эффект можно увидеть с любым числовым типом, который не является целочисленным или bigint.
ALTER TABLE dbo. T1 ALTER COLUMN c1 десятичный (9, 0) NULL; ALTER TABLE dbo. T2 ALTER COLUMN c1 десятичный (9, 0) NULL; ALTER INDEX PK_dbo_T1 ON dbo. T1 REBUILD WITH (MAXDOP = 1); ALTER INDEX PK_dbo_T2 ON dbo. T2 REBUILD WITH (MAXDOP = 1); ОБНОВЛЕНИЕ СТАТИСТИКИ ДБО. T1 с полным сканированием; ОБНОВЛЕНИЕ СТАТИСТИКИ ДБО. T2 с полной проверкой;
Повторное использование запроса на три объединения:
ВЫБЕРИТЕ T1. ПК ОТ ДБО. T1 AS T1 JOIN. T2 AS T2 ON T2. с1 = т1. c1 ПРИСОЕДИНЯЙТЕСЬ к dbo. T2 AS T3 ON T3. рк = Т2. рк;
Предполагаемый план выполнения выглядит очень знакомым:
Помимо того факта, что оптимизированное растровое изображение больше не может быть применено «INROW» механизмом хранения из-за изменения типа данных, план выполнения по существу идентичен. На снимке ниже показано изменение свойств сканирования:
К сожалению, производительность довольно сильно пострадала. Этот запрос выполняется не за 70 или 200 мс, а за 20 минут . В тесте, который дал следующий план после выполнения, время выполнения составило 22 минуты и 29 секунд:
Наиболее очевидное отличие состоит в том, что сканирование кластеризованного индекса в таблице T1 возвращает 300 000 строк даже после применения оптимизированного фильтра растровых изображений. Это имеет некоторый смысл, поскольку растровое изображение построено на строках, которые содержат только нули в столбце c1. Растровое изображение удаляет ненулевые строки из сканирования T1, оставляя только 300 000 строк с нулевыми значениями для c1. Помните, что половина строк в T1 равна нулю.
Несмотря на это, кажется странным, что объединение 32 000 строк с 300 000 строк должно занять более 20 минут. В случае, если вам интересно, одно ядро процессора было привязано к 100% для всего выполнения. Объяснение этой низкой производительности и чрезмерного использования ресурсов основывается на некоторых идеях, которые мы исследовали ранее:
Например, мы уже знаем, что, несмотря на значки параллельного выполнения, все строки из T2 оказываются в одном потоке. Напомним, что параллельное хеш-соединение в режиме строки требует перераспределения в столбцах соединения (c1). Все строки из T2 имеют одинаковое значение - ноль - в столбце c1, поэтому все строки попадают в один и тот же поток. Точно так же все строки из T1, которые проходят фильтр точечного рисунка, также имеют ноль в столбце c1, поэтому они также перераспределяются в один и тот же поток. Это объясняет, почему одно ядро выполняет всю работу.
Может показаться неоправданным, что хеш-соединение, объединяющее 32 000 строк с 300 000 строк, должно занять 20 минут, тем более что столбцы соединения с обеих сторон имеют нулевое значение и не будут объединяться в любом случае. Чтобы понять это, нам нужно подумать о том, как работает это хеш-соединение.
Входные данные сборки (32 000 строк) создают хеш-таблицу с использованием столбца соединения c1. Поскольку каждая строка на стороне сборки содержит одно и то же значение (ноль) для столбца соединения c1, это означает, что все 32 000 строк оказываются в одном и том же блоке хэша. Когда хеш-соединение переключается на проверку совпадений, каждая строка на стороне зондирования с нулевым столбцом c1 также хэшируется в один и тот же сегмент. Хеш-соединение должно затем проверить все 32 000 записей в этом сегменте на совпадение.
Проверка 300 000 рядов зондов приводит к 32 000 сравнений, выполненных 300 000 раз. Это наихудший случай для хеш-соединения: все строки на стороне компоновки хэшируются в одно и то же ведро, в результате чего получается, по сути, декартово произведение. Это объясняет длительное время выполнения и постоянную 100% загрузку процессора, поскольку хэш следует длинной цепочке сегментов хэша.
Эта низкая производительность помогает объяснить, почему существует перезапись после оптимизации, чтобы исключить пустые значения при вводе сборки в хеш-соединение. К сожалению, фильтр не был применен в этом случае.
Оптимизатор выбирает эту форму плана, потому что он неправильно оценивает, что оптимизированное растровое изображение отфильтрует все строки из таблицы T1. Несмотря на то, что эта оценка показана в потоках перераспределения, а не в сканировании кластерного индекса, это все же является основой решения. Напомним, что здесь снова есть соответствующий раздел плана предварительного исполнения:
Если бы это была правильная оценка, обработка хеш-соединения заняла бы совсем немного времени. К сожалению, оценка селективности для оптимизированного растрового изображения является настолько неправильной, когда тип данных не является простым целым числом или bigint. Кажется, растровое изображение, построенное на ключе целого числа или bigint, также может отфильтровывать пустые строки, которые не могут объединяться. Если это действительно так, то это основная причина, по которой предпочтение отдается столбцам с целочисленным или большим числом.
Последующие обходные пути в значительной степени основаны на идее устранения проблемных оптимизированных растровых изображений.
Одним из способов предотвращения рассмотрения оптимизированных растровых изображений является требование непараллельного плана. Операторы растрового изображения в режиме строки (оптимизированные или иные) отображаются только в параллельных планах:
ВЫБЕРИТЕ T1. pk ОТ (dbo. T2 AS T2 JOIN. DBO. T2 AS T3 ON T3. pk = T2. pk) JOIN dbo. T1 AS T1 ON T1. с1 = т2. c1 ВАРИАНТ (MAXDOP 1, FORCE ORDER);
Этот запрос выражается с использованием слегка отличающегося синтаксиса с подсказкой FORCE ORDER для создания формы плана, которая легче сопоставима с предыдущими параллельными планами. Важной особенностью является подсказка MAXDOP 1.
Этот примерный план показывает восстановление фильтра перезаписи после оптимизации:
Версия плана после выполнения показывает, что он отфильтровывает все строки из входных данных построения, что означает, что сканирование на стороне зонда может быть полностью пропущено:
Как и следовало ожидать, эта версия запроса выполняется очень быстро - в среднем около 20 мс для меня. Мы можем добиться аналогичного эффекта без подсказки FORCE ORDER и переписывания запроса:
ВЫБЕРИТЕ T1. ПК ОТ ДБО. T1 AS T1 JOIN. T2 AS T2 ON T2. с1 = т1. c1 ПРИСОЕДИНЯЙТЕСЬ к dbo. T2 AS T3 ON T3. рк = Т2. ОПЦИЯ ПК (MAXDOP 1);
В этом случае оптимизатор выбирает другую форму плана с фильтром, расположенным непосредственно над сканированием T2:
Это выполняется еще быстрее - примерно за 10 мс - как и следовало ожидать. Естественно, это не было бы хорошим выбором, если бы количество присутствующих (и соединяемых) строк было намного больше.
Нет запроса на отключение оптимизированных растровых изображений, но мы можем добиться того же эффекта, используя пару недокументированных флагов трассировки. Как всегда, это только для интереса; Вы не хотели бы использовать их в реальной системе или приложении:
ВЫБЕРИТЕ T1. ПК ОТ ДБО. T1 AS T1 JOIN. T2 AS T2 ON T2. с1 = т1. c1 ПРИСОЕДИНЯЙТЕСЬ к dbo. T2 AS T3 ON T3. рк = Т2. ПК ОПЦИЯ (QUERYTRACEON 7497, QUERYTRACEON 7498);
Результирующий план выполнения:
Битовая карта - это битовая карта перезаписи постоптимизации, а не оптимизированная битовая карта:
Обратите внимание на нулевую оценку стоимости и имя растрового изображения (а не Opt_Bitmap). без оптимизированного растрового изображения для искажения оценок затрат активируется перезапись после оптимизации с включением фильтра отклонения нуля. Этот план выполнения занимает около 70 мс .
Тот же план выполнения (с фильтром и неоптимизированным растровым изображением) также можно создать, отключив правило оптимизатора, отвечающее за создание растровых планов соединения типа «звезда» (опять же, строго недокументированное и не для реального использования):
ВЫБЕРИТЕ T1. ПК ОТ ДБО. T1 AS T1 JOIN. T2 AS T2 ON T2. с1 = т1. c1 ПРИСОЕДИНЯЙТЕСЬ к dbo. T2 AS T3 ON T3. рк = Т2. pk OPTION (QUERYRULEOFF StarJoinToHashJoinsWithBitmap);
Это самый простой вариант, но можно было бы подумать, если бы он знал о обсуждаемых до сих пор проблемах. Теперь, когда мы знаем, что нам нужно исключить пустые значения из T2.c1, мы можем добавить это к запросу напрямую:
ВЫБЕРИТЕ T1. ПК ОТ ДБО. T1 AS T1 JOIN. T2 AS T2 ON T2. с1 = т1. c1 ПРИСОЕДИНЯЙТЕСЬ к dbo. T2 AS T3 ON T3. рк = Т2. ПК ГДЕ Т2. с1 НЕ НУЛЬ; - Новый!
Полученный примерный план выполнения, возможно, не совсем то, что вы могли ожидать:
Добавленный нами дополнительный предикат был добавлен в середину сканирования кластеризованного индекса T2:
План после исполнения:
Обратите внимание, что Merge Join завершает работу после чтения одной строки с ее верхнего входа, а затем не может найти строку на своем нижнем входе из-за эффекта предиката, который мы добавили. Сканирование кластеризованного индекса таблицы T1 никогда не выполняется вообще, поскольку объединение Nested Loops никогда не получает строки на своем входном входе. Эта окончательная форма запроса выполняется за одну или две миллисекунды.
В этой статье было рассмотрено довольно много оснований для изучения некоторых менее известных поведений оптимизатора запросов и объяснения причин крайне низкой производительности хеш-соединения в конкретном случае.
Может возникнуть соблазн спросить, почему оптимизатор не добавляет обычно отклоняющие нуль фильтры до объединений равенства. Можно только предположить, что это не было бы полезно в достаточно общих случаях. Ожидается, что в большинстве объединений не будет много отклонений null = null, и добавление предикатов обычно может быстро привести к обратным результатам, особенно если присутствует много столбцов объединения. Для большинства объединений отклонение пустых значений внутри оператора объединения, вероятно, является лучшим вариантом (с точки зрения модели затрат), чем введение явного фильтра.
Похоже, что предпринимаются попытки предотвратить появление наихудших случаев с помощью перезаписи постоптимизации, предназначенной для отклонения строк с нулевым соединением до того, как они достигнут входных данных сборки хеш-соединения. Кажется, что между эффектом оптимизированных растровых фильтров и применением этого переписывания существует неудачное взаимодействие. Также прискорбно, что когда эта проблема с производительностью действительно возникает, диагностировать ее очень сложно только из одного плана выполнения.
На данный момент лучший вариант, похоже, знает об этой потенциальной проблеме производительности с хэш-соединениями в пустых столбцах, а также добавлять явные отклоняющие нулевые предикаты (с комментарием!), Чтобы при необходимости создать эффективный план выполнения. Использование подсказки MAXDOP 1 может также выявить альтернативный план с присутствующим контрольным фильтром.
Как правило, запросы, объединяющие столбцы целого типа и ищущие существующие данные, как правило, лучше соответствуют возможностям модели оптимизатора и механизма выполнения, чем альтернативы.
Я хочу поблагодарить SQL_Sasquatch ( @sqL_handLe ) за его разрешение ответить на его оригинальная статья с техническим анализом. Используемые здесь примеры данных в значительной степени основаны на этой статье.
Я также хочу поблагодарить Роба Фарли ( блог | щебет ) за наши технические обсуждения в течение многих лет, особенно в январе 2015 года, где мы обсуждали последствия дополнительных нулевых отклоняющих предикатов для равных объединений. Роб писал о смежных темах несколько раз, в том числе в Обратные предикаты - смотри в обе стороны, прежде чем пересечься ,
Copyleft © 2017 . www.info-center.od.ua Информационный центр - Всегда в центре событий