Oracle оптимизация запросов с большими объемами данных. Индексируйте соединенные столбцы. Сформируйте не кластеризованные индексы для запросов на поиск уникальных значений

Теперь настало время произвести оптимизацию самих условных операторов SQL. Большинство запросов используют директиву SQL WHERE, поэтому, оптимизируя условия, можно добиться значительной производительности запросов. При этом почему-то лишь небольшая часть приложений для БД используют оптимизацию условий.

AND

Очевидно, что в серии из нескольких операторов AND условия должны располагаться в порядке возрастания вероятности истинности данного условия. Это делается для того, чтобы при проверке условий БД не проверяла остальную часть условия. Эти рекомендации не относится к БД Oracle, где условия начинают проверяться с конца. Соответственно, их порядок должен быть обратным - по убыванию вероятности истинности.

Чтобы прийти к тому, что, по его мнению, является наилучшим планом для выполнения запроса, процессор запросов выполняет ряд различных шагов; весь процесс обработки запросов показан на рисунке 1. Рисунок 1 - Процесс обработки запроса. Предполагая, что запрос действителен, выход этой фазы является логическим деревом, каждый узел которого представляет собой логическую операцию, которую должен выполнять запрос, например, чтение конкретной таблицы или выполнение внутреннего соединения. Это логическое дерево затем используется для запуска процесса оптимизации запросов, который примерно состоит из следующих двух шагов: Создайте возможные планы выполнения - используя логическое дерево, оптимизатор запросов разработает несколько возможных способов выполнения запроса, то есть ряд возможных планов выполнения. План выполнения - это, по сути, набор физических операций, которые могут быть выполнены для получения требуемого результата, как описано логическим деревом; Оценка затрат по каждому плану. Хотя Оптимизатор запросов не генерирует каждый возможный план выполнения, он оценивает ресурс и временную стоимость каждого плана, который он создает. Выбран план, который Оптимизатор запросов считает наименьшей стоимостью оцениваемых, и передается вместе с механизмом выполнения; Выполнение запроса, кэширование плана - запрос выполняется механизмом выполнения в соответствии с выбранным планом. План может храниться в памяти, в кеше плана. Анализ и привязка - запрос анализируется и привязывается. . Связывание в основном касается разрешения имен.

OR

Ситуация с данным оператором прямо противоположна ситуации с AND. Условия должны располагаться в порядке убывания вероятности истинности. Фирма Microsoft настойчиво рекомендует использовать данный метод при построении запросов, хотя многие даже не знают об этом или, по крайней мере, не обращают на него внимание. Но опять-таки это не относится к БД Oracle, где условия должны располагаться по возрастанию вероятности истинности.

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

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

Еще одним условием для оптимизации можно считать тот факт, что если одинаковые колонки располагаются рядом, запрос выполняется быстрее. Например, запрос ".. WHERE column1 = 1 OR column2 = 3 OR column1 = 2" будет выполняться медленней, чем запрос "WHERE column1 = 1 OR column1 = 2 OR column2 = 3" . Даже если вероятность истинности условия column2 = 3 выше, чем column1 = 2.

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

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

Еще в школе мне рассказывали про распределительный закон. Он гласит, что A AND (B OR C) - то же самое, что и (A AND B) OR (A AND C ). Опытным путем было установлено, что запрос вида "...WHERE column1 = 1 AND (column2 = "A" OR column2 = "B")" выполняется несколько быстрее, чем "...WHERE (column1 = 1 AND column2 = "A") OR (column1 = 1 AND column2 = "B")" . Некоторые БД сами умеют оптимизировать запросы такого типа, но лучше перестраховаться.

Создание планов исполнения кандидатов

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

NOT

Эту операцию всегда следует приводить к более "читабельному" виду (в разумных пределах, конечно). Так, запрос "...WHERE NOT (column1 > 5)" преобразуется в "...WHERE column1 <= 5" . Более сложные условия можно преобразовать используя правило де Моргана, которое ты тоже должен был изучить в школе. Согласно этому правилу NOT(A AND B) = (NOT A) OR (NOT B) и NOT(A OR B) = (NOT A) AND (NOT B) . Например, условие "...WHERE NOT (column1 > 5 OR column2 = 7)" преобразуется в более простую форму: ...WHERE column1 <= 5 AND column2 <> 7 .

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

IN

Многие наивно полагают, что запрос "... WHERE column1 = 5 OR column1 = 6" равносилен запросу "...WHERE column1 IN (5, 6)" . На самом деле это не так. Операция IN работает гораздо быстрее, чем серия OR. Поэтому всегда следует заменять OR на IN, где это возможно, несмотря на то, что некоторые БД сами производят эту оптимизацию. Там, где используется серия последовательных чисел, IN следует поменять на BETWEEN. Например, "...WHERE column1 IN (1, 3, 4, 5)" оптимизируется к виду: …WHERE column1 BETWEEN 1 AND 5 AND column1 <> 2 . И этот запрос действительно быстрее.

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

Поскольку Оптимизатор запросов работает в ограничении по времени, есть вероятность, что выбранный план может быть оптимальным планом, но также вероятно, что он может быть просто близок к оптимальному плану. Чтобы исследовать пространство поиска, оптимизатор запросов использует правила трансформации и эвристики. Генерация планов исполнения кандидатов выполняется внутри Оптимизатора запросов с использованием правил преобразования, а использование эвристики ограничивает количество вариантов, рассмотренных для обеспечения разумного времени оптимизации.

LIKE

Эту операцию следует использовать только при крайней необходимости, потому что лучше и быстрее использовать поиск, основанный на full-text индексах. К сожалению, я вынужден направить тебя за информацией о поиске на просторы всемирной паутины.

CASE

Сама эта функция может использоваться для повышения скорости работы запроса, когда в нем есть более одного вызова медленной функции в условии. Например, чтобы избежать повторного вызова slow_function() в запросе "...WHERE slow_function(column1) = 3 OR slow_function(column1) = 5" , нужно использовать CASE:

Оценка стоимости каждого плана

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

Выполнение запроса и кэширование плана

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

... WHERE 1 = CASE slow_function(column1)

WHEN 3 THEN 1

WHEN 5 THEN 1

Эта документация перемещена в архив и не поддерживается.

Оптимизация производительности запросов SQL Server

Мацей Пилеки (Maciej Pilecki)

Краткий обзор:

  • Анализ планов выполнения
  • Оптимизация запросов
  • Определение запросов, нуждающихся в настройке

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

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

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

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

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

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


Анализ планов выполнения

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

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

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

Текущие задачи оптимизатора запросов

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

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

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

Это приводит нас к еще одной важной технической проблеме для Оптимизатора запросов: точная оценка стоимости и мощности. Поскольку оптимизатор, основанный на затратах, выбирает план выполнения с наименьшими затратами, качество выбора плана не хуже точности оценки затрат и мощности оптимизатора. Даже если предположить, что время не вызывает беспокойства и что оптимизатор запросов может анализировать все пространство поиска без проблем, ошибки мощности и оценки стоимости могут все еще сделать оптимизатор запросов, выберите неправильный план.

Существует несколько способов извлечения плана выполнения запроса:

  • В Management Studio есть функции отображения реального и приблизительного плана выполнения, представляющие план в графической форме. Это наиболее удобная возможность непосредственной проверки и, по большому счету, наиболее часто используемый способ отображения и анализа планов выполнения (примеры из этой статьи я буду иллюстрировать графическими планами, созданными именно таким способом).
  • Различные параметры SET, например, SHOWPLAN_XML и SHOWPLAN_ALL, возвращают план выполнения в виде документа XML, описывающего план в виде специальной схемы, или набора строк с текстовым описанием каждой операции.
  • Классы событий профайлера SQL Server, например, Showplan XML, позволяют собирать планы выполнения выражений методом трассировки.

Хотя XML-представление плана выполнения не самый удобный для пользователя формат, эта команда позволяет использовать самостоятельно написанные процедуры и служебные программы для анализа, поиска проблем с производительностью и практически оптимальных планов. Представление на базе XML можно сохранить в файл с расширением sqlplan, открывать в Management Studio и создавать графическое представление. Кроме того, эти файлы можно сохранять для последующего анализа без необходимости воспроизводить их каждый раз, как этот анализ понадобится. Это особенно полезно для сравнения планов и выявления возникающих со временем изменений.


Оценка стоимости выполнения

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

Существует несколько распространенных, но неверных представлений о приблизительной стоимости выполнения. Особенно часто считается, что приблизительная стоимость выполнения является хорошим показателем того, сколько времени займет выполнение запроса и что эта оценка позволяет отличить хорошие планы от плохих. Это неверно. Во-первых, есть много документов касающихся того, в каких единицах выражается приблизительная стоимость и имеют ли они непосредственное отношение ко времени выполнения. Во-вторых, поскольку значение это приблизительно и может оказаться ошибочным, планы с большими оценочными затратами иногда оказываются значительно эффективнее с точки зрения ЦП, ввода/вывода и времени выполнения, несмотря на предположительно высокую стоимость. Это часто случается с запросами, где задействованы табличные переменные. Поскольку статистики по ним не существует, оптимизатор запросов часто предполагает, что в таблице есть всего одна строка, хотя их во много раз больше. Соответственно, оптимизатор выберет план на основе неточной оценки. Это значит, что при сравнении планов выполнения запросов не следует полагаться только на приблизительную стоимость. Включите в анализ параметры STATISTICS I/O и STATISTICS TIME, чтобы определить истинную стоимость выполнения в терминал ввода/вывода и времени работы ЦП.

Здесь стоит упомянуть об особом типе плана выполнения, который называется параллельным планом. Такой план можно выбрать при отправке на сервер с несколькими ЦП запроса, поддающегося параллелизации (В принципе, оптимизатор запроса рассматривает использование параллельного плана только в том случае, если стоимость запроса превышает определенное настраиваемое значение.) Из-за дополнительных расходов на управление несколькими параллельными процессами выполнения, связанными с распределением заданий, выполнением синхронизации и сведением результатов, параллельные планы обходятся дороже, что отражает их приблизительная стоимость. Тогда чем же они предпочтительнее более дешевых, не параллельных планов? Благодаря использованию вычислительной мощности нескольких ЦП параллельные планы обычно выдают результат быстрее стандартных. В зависимости от конкретного сценария (включая такие переменные, как доступность ресурсов с параллельной нагрузкой других запросов) эта ситуации для кого-то может оказаться желательной. Если это ваш случай, нужно будет указать, какие из запросов можно выполнять по параллельному плану и сколько ЦП может задействовать каждый. Для этого нужно настроить максимальную степень параллелизма на уровне сервера и при необходимости настроить обход этого правила на уровне отдельных запросов с помощью параметра OPTION (MAXDOP n).


Анализ плана выполнения

Теперь рассмотрим простой запрос, его план выполнения и некоторые способы повышения производительности. Предположим, что я выполняю этот запрос в Management Studio с включенным параметром включения реального плана выполнения в примере базы данных Adventure Works SQL Server 2005:

SELECT c.CustomerID, SUM(LineTotal) FROM Sales.SalesOrderDetail od JOIN Sales.SalesOrderHeader oh ON od.SalesOrderID=oh.SalesOrderID JOIN Sales.Customer c ON oh.CustomerID=c.CustomerID GROUP BY c.CustomerID

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

Рис. 1 Пример плана выполнения

Рис. 2 Сведения об операции (Щелкните изображение, чтобы увеличить его)

Глядя на операторы, можно анализировать последовательность выполненных этапов:

  1. Ядро базы данных выполняет операцию сканирования кластеризированных индексов с таблицей Sales.Customer и возвращает столбец CustomerID со всеми строками из этой таблицы.
  2. Затем оно выполняет сканирование индексов (не кластеризированных) над одним из индексов из таблицы Sales.SalesOrderHeader. Это индекс столбца CustomerID, но подразумевается, что в него входит столбец SalesOrderID (ключ кластеризации таблицы). Сканирование возвращает значения обоих столбцов.
  3. Результаты обоих сеансов сканирования объединяются в столбце CustomerID с помощью физического оператора слияния (это один из трех возможных физических способов выполнения операции логического объединения. Операция выполняется быстро, но входные данные приходится сортировать в объединенном столбце. В данном случае обе операции сканирования уже возвратили строки, рассортированные в столбце CustomerID, так что дополнительную сортировку выполнять не нужно).
  4. Затем ядро базы данных выполняет сканирование кластеризированного индекса в таблице Sales.SalesOrderDetail, извлекая значения четырех столбцов (SalesOrderID, OrderQty, UnitPrice и UnitPriceDiscount) из всех строк таблицы (предполагалось, что возвращено будет 123,317 строк. Как видно из свойств Estimated Number of и and Actual Number of Rows на рис. 2 , получилось именно это число, так что оценка оказалась очень точной).
  5. Строки, полученные при сканировании кластеризованного индекса, передаются оператору вычисления стоимости, умноженной на коэффициент, чтобы вычислить значение столбца LineTotal для каждой строки на основе столбцов OrderQty, UnitPrice и UnitPriceDiscount, упомянутых в формуле.
  6. Второй оператор вычисления стоимости, умноженной на коэффициент, применяет к результату предыдущего вычисления функцию ISNULL, как и предполагает формула вычисленного столбца. Он завершает вычисление в столбце LineTotal и возвращает его следующему оператору вместе со столбцом SalesOrderID.
  7. Вывод оператора слияния с этапа 3 объединяется с выводом оператора стоимости, умноженной на коэффициент с этапа 6 и использованием физического оператора совпадения значений хэша.
  8. Затем к группе строк, возвращенных оператором слияния по значению столбца CustomerID и вычисленному сводному значению SUM столбца LineTotal применяется другой оператор совпадения значений хэша.
  9. Последний узел, SELECT - это не физический или логический оператор, а местозаполнитель, соответствующий сводным результатам запроса и стоимости.

В созданном на моем ноутбуке плане выполнения приблизительная стоимость равнялась 3,31365 (как видно на рис. 3 ). При выполнении с включенной функцией STATISTICS I/O ON отчет по запросу содержал упоминание о 1,388 логических операциях чтения из трех задействованных таблиц. Процентное значение под каждым оператором - это его стоимость в процентах от общей приблизительной стоимости всего плана. На плане на рис. 1 видно, что большая часть общей стоимости связана со следующими тремя операторами: сканирование кластеризованного индекса таблицы Sales.SalesOrderDetail и два оператора совпадения значений хэша. Перед тем как приступить к оптимизации, хотелось отметить одно очень простое изменение в моем запросе, которое позволило полностью устранить два оператора.

Рис. 3 Общая приблизительная стоимость выполнения запроса

Поскольку я возвращал из таблицы Sales.Customer только столбец CustomerID, и тот же столбец включен в таблицу Sales.SalesOrderHeaderTable в качестве внешнего ключа, я могу полностью исключить из запроса таблицу Customer без изменения логического значения или результата нашего запроса. Для этого используется следующий код:

SELECT oh.CustomerID, SUM(LineTotal) FROM Sales.SalesOrderDetail od JOIN Sales.SalesOrderHeader oh ON od.SalesOrderID=oh.SalesOrderID GROUP BY oh.CustomerID

Получился другой план выполнения, который изображен на рис. 4 .


Рис. 4 План выполнения после устранения из запроса таблицы Customer (Щелкните изображение, чтобы увеличить его)

Полностью устранены две операции - сканирование кластеризированного индекса таблицы Customer и слияние Customer и SalesOrderHeader, а совпадение значений хэша заменено на куда более эффективную операцию слияния. При этом для слияния таблиц SalesOrderHeader и SalesOrderDetail нужно вернуть строки обеих таблиц, рассортированные по общему столбцу SalesOrderID. Для этого оптимизатор кластера выполнил сканирование кластеризованного индекса таблицы SalesOrderHeader вместо того, чтобы использовать сканирование некластеризованного индекса, который был бы дешевле с точки зрения ввода/вывода. Это хороший пример практического применения оптимизатора запроса, поскольку экономия, получающаяся при изменении физического способа слияния, оказалась больше дополнительной стоимости ввода/вывода при сканировании кластеризованного индекса. Оптимизатор запроса выбрал получившуюся комбинацию операторов, поскольку она дает минимально возможную примерную стоимость выполнения. На моем компьютере, несмотря на то, что количество логических считываний возросло (до 1,941), временные затраты ЦП стали меньше, и приблизительная стоимость выполнения данного запроса упала на 13 процентов (2,89548).

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

CREATE INDEX IDX_OrderDetail_OrderID_TotalLine ON Sales.SalesOrderDetail (SalesOrderID) INCLUDE (LineTotal)

Обратите внимание, что в созданном индексе есть вычисленный столбец. Это возможно не всегда - все зависит от определения такого столбца.

Создав этот индекс и выполнив тот же запрос, я получил новый план, который изображен на рис. 5 .


Рис. 5 Оптимизированный план выполнения (Щелкните изображение, чтобы увеличить его)

Сканирование кластеризованного индекса таблицы SalesOrderDetail заменено некластеризованным сканированием с заметно меньшими затратами на ввод/вывод. Кроме того, я исключил один из операторов вычисления стоимости, умноженной на коэффициент, поскольку в моем индексе уже есть вычисленное значение столбца LineTotal. Теперь приблизительная стоимость плана выполнения составляет 2,28112 и при выполнении запроса производится 1,125 логических считываний.


Индекс покрытия

Упражнение. Запрос заказа покупателя

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

Ответ. Я предложил рассчитать оптимальный индекс покрытия для создания таблицы Sales.SalesOrderHeader на примере запроса из моей статьи. При этом нужно в первую очередь отметить, что запрос использует только два столбца из таблицы: CustomerID и SalesOrderID. Если вы внимательно прочли эту статью, то заметили, что в случае с таблицей SalesOrderHeader индекс покрытия запроса уже существует, это индекс CustomerID, который косвенно содержит столбец SalesOrderID, являющийся ключом кластеризации таблицы.

Конечно, я объяснял и то, почему оптимизатор запроса не стал использовать этот индекс. Да, можно заставить оптимизатор запроса использовать этот индекс, но это решение было бы менее эффективным, чем существующий план с операторами сканирования кластеризованного индекса и слияния. Дело в том, что оптимизатор запроса пришлось бы принудить либо выполнить дополнительную операцию сортировки, необходимую для использования слияния, либо откатиться назад, к использованию менее эффективного оператора совпадения значений хэша. В обоих вариантах приблизительная стоимость выполнения выше, чем в существующем плане (версия с оператором сортировки работала бы особенно плохо), поэтому оптимизатор запроса не будет их использовать без принуждения. Итак, в данной ситуации лучше сканирования кластеризованного индекса будет работать только некластеризованный индекс в столбцах SalesOrderID, CustomerID. При этом нужно отметить, что столбцы должны идти именно в таком порядке:

CREATE INDEX IDX_OrderHeader_SalesOrderID_CustomerID ON Sales.SalesOrderHeader (SalesOrderID, CustomerID)

Если вы создадите этот индекс, в плане выполнения будет использовано не сканирование кластеризованного индекса, а сканирование индекса. Разница существенная. В данном случае некластеризованный индекс, который содержит только два столбца, заметно меньше всей таблицы в виде кластеризованного индекса. Соответственно, при считывании нужных данных будет меньше задействован ввод/вывод.

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


Индекс, созданный из таблицы SalesOrderDetail, представляет собой так называемый «индекс покрытия». Это некластеризованный индекс, где содержатся все столбцы, необходимые для выполнения запроса. Он устраняет необходимость сканирования всей таблицы с помощью операторов сканирования таблицы или кластеризованного индекса. По сути индекс представляет собой уменьшенную копию таблицы, где содержится подмножество ее столбцов. В индекс включаются только столбцы, которые необходимы для ответа на запрос или запросы, то есть только то, что «покрывает» запрос.

Создание индексов покрытия наиболее частых запросов - один из самых простых и распространенных способов тонкой настройки запроса. Особенно хорошо он работает в ситуациях, когда в таблице несколько столбцов, но запросы часто ссылаются только на некоторые из них. Создав один или несколько индексов покрытия, можно значительно повысить производительность соответствующих запросов, так как они будут обращаться к заметно меньшему количеству данных и, соответственно, количество вводов/выводов сократится. Тем не менее, поддержка дополнительных индексов в процессе модификации данных (операторы INSERT, UPDATE и DELETE) подразумевает некоторые расходы. Следует четко определить, оправдывает ли увеличение производительности эти дополнительные расходы. При этом учтите характеристики своей среды и соотношение количества запросов SELECT и изменений данных.

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

С моим примером запроса можно сделать еще кое-что. Создав индекс покрытия таблицы SalesOrderHeader, можно дополнительно оптимизировать запрос. При этом будет использовано сканирование некластеризованного индекса вместо кластеризованного. Предлагаю вам выполнить это упражнение самостоятельно. Попробуйте получить определение индекса: выясните, наличие каких столбцов превратит его в индекс покрытия данного запроса и повлияет ли порядок столбцов на производительность. Решение см. в боковой панели "Упражнение. Запрос заказа покупателя".


Индексированные представления

Если выполнение моего примера запроса очень важно, я могут пойти немного дальше и создать индексированное представление, в котором физически хранятся материализованные результаты запроса. При создании индексированных представлений существуют некоторые предварительные условия и ограничения, но если их удастся использовать, производительность сильно повысится. Обратите внимание, что расходы на обслуживание индексированных представлений выше, чем у обычных индексов. Их нужно использовать с осторожностью. В данном случае определение индекса выглядит примерно так:

CREATE VIEW vTotalCustomerOrders WITH SCHEMABINDING AS SELECT oh.CustomerID, SUM(LineTotal) AS OrdersTotalAmt, COUNT_BIG(*) AS TotalOrderLines FROM Sales.SalesOrderDetail od JOIN Sales.SalesOrderHeader oh ON od.SalesOrderID=oh.SalesOrderID GROUP BY oh.CustomerID

SELECT CustomerID, OrdersTotalAmt FROM vTotalCustomerOrders WITH (NOEXPAND)

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


Поиск запросов, нуждающихся в настройке

Как я определяют, что запрос стоит настроить? Я ищу часто выполняемые запросы, возможно, с невысокой стоимостью выполнения в отдельном случае, но в целом более дорогие, чем крупные, но редко встречающиеся запросы. Это не значит, что последние настраивать не нужно. Я просто считаю, что для начала нужно сосредоточиться на более частых запросах. Так как же их найти?

К сожалению, самый надежный метод довольно сложен и предусматривает отслеживание всех выполненных запросов к серверу с последующий группировкой по подписям. При этом текст запроса с реальными значениями параметров заменяется на замещающий текст, который позволяет выбрать однотипные запросы с разными значениями. Подписи запроса создать тяжело, так что это сложный процесс. Ицик Бен-Ган (Itzik Ben-Gan) описывает решение с использованием пользовательских функций в среде CLR и регулярных выражений в своей книге «Microsoft SQL Server 2005 изнутри: запросы T-SQL».

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

Удачной настройки!


Мацей Пилеки (Maciej Pilecki) - младший преподаватель компании Solid Quality Mentors, глобальной организации, которая специализируется на обучении и предоставлении консультационных услуг. Он является сертифицированным преподавателем Майкрософт (MCT) и ценнейшим специалистом по SQL Server (MVP), часто ведет курсы и читает лекции по многим аспектам SQL Server и разработки приложений.
© 2008 Корпорация Майкрософт и компания CMP Media, LLC. Все права защищены; полное или частичное воспроизведение без разрешения запрещено .

Показ:



 

Пожалуйста, поделитесь этим материалом в социальных сетях, если он оказался полезен!