При каждой попытке выполнения запроса конвейер запросов обращается в кэш планов запросов, чтобы определить, что конкретный запрос уже скомпилирован и доступен. Whenever an attempt to execute a query is made, the query pipeline looks up its query plan cache to see whether the exact query is already compiled and available. Если это так, то кэшированный план используется повторно вместо построения нового плана. If so, it reuses the cached plan rather than building a new one. Если совпадение в кэше планов запросов не обнаружено, запрос компилируется и кэшируется. If a match is not found in the query plan cache, the query is compiled and cached. Запрос идентифицируется его текстом Entity SQL Entity SQL и коллекцией параметров (имен и типов). A query is >Entity SQL Entity SQL text and parameter collection (names and types). Все текстовые сравнения выполняются с учетом регистра. All text comparisons are case-sensitive.
Параметр Configuration Configuration
Кэширование планов запросов можно настроить с помощью EntityCommand. Query plan caching is configurable through the EntityCommand.
Чтобы включить или выключить кэширование планов запросов с помощью свойства EntityCommand.EnablePlanCaching, назначьте этому свойству значение true или false . To enable or disable query plan caching through EntityCommand.EnablePlanCaching, set this property to true or false . Отключение кэширования планов для отдельных динамических запросов, которые вряд ли будут использованы повторно, повышает производительность. Disabling plan caching for individual dynamic queries that are unlikely to be used more then once improves performance.
Можно включить кэширование планов запросов с помощью EnablePlanCaching. You can enable query plan caching through EnablePlanCaching.
Рекомендации Recommended Practice
Как правило, следует избегать динамических запросов. Dynamic queries should be avoided, in general. Динамический запрос в следующем примере уязвим для атак путем внедрения кода SQL, так как входные данные пользователя принимаются напрямую без проверки. The following dynamic query example is vulnerable to SQL injection attacks, because it takes user input directly without any validation.
Если используются динамически формируемые запросы, подумайте об отключении кэширования плана запроса, чтобы избежать ненужного расходования памяти для записей кэша, которые вряд ли будут использоваться повторно. If you do use dynamically generated queries, consider disabling query plan caching to avoid unnecessary memory consumption for cache entries that are unlikely to be reused.
Кэширование планов запросов для статических запросов и параметризованных запросов может увеличить производительность. Query plan caching on static queries and parameterized queries can provide performance benefits. Ниже приводится пример статического запроса: The following is an example of a static query:
Для правильного сопоставления запросов кэшу планов запросов они должны соответствовать следующим требованиям. For queries to be matched properly by the query plan cache, they should comply with the following requirements:
Текст запроса должен быть постоянным шаблоном, предпочтительно постоянной строкой или ресурсом. Query text should be a constant pattern, preferably a constant string or a resource.
Экземпляр EntityParameter или ObjectParameter должен быть использован всегда, когда нужно передать введенное пользователем значение. EntityParameter or ObjectParameter should be used wherever a user-supplied value must be passed.
Необходимо избегать следующих шаблонов запросов, которые напрасно занимают слоты в кэше планов запросов. You should avoid the following query patterns, which unnecessarily consume slots in the query plan cache:
Изменение регистра символов в тексте. Changes to letter case in the text.
Изменение пробельных символов. Changes to white space.
Изменение литеральных значений. Changes to literal values.
Изменение текста внутри комментариев. Changes to text inside comments.
Один из моих проектов имеет очень большую базу данных, в которой я не могу редактировать индексы и т. Д., И должен работать как есть.
При тестировании некоторых запросов, которые я буду выполнять в своей базе данных через службу, которую я пишу в .net, я видел, что они выполняются довольно медленно при первом запуске.
То, что они делали раньше, — это две основные (большие) таблицы, которые используются в основном. Они показали мне, что они открывают SQL Server Management Studio и запускают
запрос, выполнение которого занимает около 5 минут в первый раз, но затем занимает около 30 секунд, если вы запустите его снова, не закрывая SQL Server Management Studio. Что они делают, так это оставляют SQL Server Management Studio открытой 24/7, поэтому, когда одна из их программ выполняет запросы, связанные с этими двумя таблицами (которые, по-видимому, являются почти всеми запросами, выполняемыми их программой), чтобы иметь 30 секунд Время выполнения вместо 5 минут.
Это происходит потому, что я предполагаю, что две таблицы кэшируются, а затем нет (или почти нет) чтения с диска.
Является ли это хорошей идеей иметь службу, которая затем запускает запрос для кэширования этих 2 таблиц время от времени? Или есть лучшее решение для этого, учитывая тот факт, что я не могу редактировать индексы или разбивать таблицы и т. Д.?
Изменить: Извините, просто я, возможно, неясно, надеюсь, что БД уже имеет индексы, просто мне не разрешено редактировать их или что-то еще.
В данной статье рассматриваются особенности запросов на выборку данных, виды индексов, использование планов запросов, различные подходы к оптимизации запросов. Во второй части статьи «Оптимизация SQL-запросов (Часть 2)» рассматриваются рекомендации по разработке оптимальной структуры БД.
Введение
Все более-менее сложные веб-приложения взаимодействуют с базами данных. Причем получение данных из базы является одним из узких мест приложения. Таким образом, оптимизация запросов к базе способна существенно повысить производительность веб-приложения в целом.
1. Демонстрационная база данных
Для демонстрации запросов в данном докладе потребуется база данных. Допустим, необходимо автоматизировать следующую предметную область: у торгового предприятия есть различные товары, которые продаются различным заказчикам. Необходимо обеспечить хранение данных по заказам клиентам, включая заказанные товары, и о статусах заказа, развернутых во времени. Создадим упрощенную базу данных.
БД имеет следующую структуру:
База наполнена тестовыми данными:
- Status — 3 записи
- Customer — 3 записи
- Product — 10 записей
- Order — 3 млн. записей.
- OrderStatus — 9 млн. записей.
- OrderProduct — 2,5 млн. записей
2. Запросы на выборку данных
Запросы на выборку данных реализуются с помощью конструкции SELECT языка SQL.
Запрос в MS SQL имеет следующий синтаксис:
2.1. Типы соединений таблиц
Если запрос адресован к нескольким таблицам БД, то выполняется соединение этих таблиц по определенным полям.
Существует несколько типов соединений:
Внутреннее (INNER JOIN)
Внешнее левое/правое (LEFT/RIGHT JOIN)
Перекрестное (CROSS JOIN)
(Используется редко)
2.2. Вложенные запросы
Вложенный запрос представляет собой запрос типа select from select
Пример:
2.3. Короткие и соотнесенные запросы
Пример короткого селекта:
Пример соотнесенного запроса:
В MS SQL короткие и соотнесенные запросы могут быть реализованы с помощью конструкции OUTER/CROSS APPLY.
Существует два типа оператора APPLY: CROSS APPLY и OUTER APPLY. Оператор CROSS APPLY возвращает только строки из внешней таблицы, которые создают результирующий набор из возвращающей табличное значение функции. Оператор OUTER APPLY возвращает и строки, которые формируют результирующий набор, и строки, которые этого не делают, со значениями NULL в столбцах, созданных возвращающей табличное значение функцией.
3. Индексы
3.1. Виды индексов MS-SQL
В MS SQL Индексы могут быть двух типов: кластерные и не кластерные.
Если упрощенно, то кластерный индекс отличается от не кластерного тем, что в листьях этого индекса содержатся не ссылки на записи в таблице, а сами записи. Таким образом, для таблицы допустим только один кластерный индекс.
Скорость доступа к строкам при указании условия where, совпадающего с условием индексирования для кластерного индекса, будет выше, чем при поиске записей по простому индексу.
Если в кластерный индекс включены часто обновляемые поля, то это отрицательно скажется на производительности, т. к. обновления будут приводить к логическому переупорядочиванию данных.
Если из таблицы, имеющей кластерный индекс, часто удаляются записи, то это приводит к сильной фрагментации таблицы.
MS SQL по умолчанию создает кластерные индексы по первичному ключу.
3.2. Ограничения MS SQL при создании индексов
При создании индекса максимальная суммарная длина полей допускается равной 900 байт. Причем индекс может быть создан по полям, типы которых допускают большую длину полей, но при этом хранящиеся данные обязательно должны быть меньше.
Т.е., например, в таблице имеется 2 поля varchar(500) , MSSQL допускает создание индекса при условии, что реальная длина записей по этим полям не превышает 900. При наличии такого индекса в таблице, при попытке вставки (или обновления) данных, если сумма реальных полей будет превышать 500 байт, то будет выдано сообщение об ошибке.
Для хранения каждого символа в типах nvarchar и nchar MS SQL использует 2 байта.
4. Планы запросов
В реляционной СУБД оптимальный план выполнения запроса — это такая последовательность применения операторов реляционной алгебры к исходным и промежуточным отношениям, которое для конкретного текущего состояния БД (её структуры и наполнения) может быть выполнено с минимальным использованием вычислительных ресурсов.
4.1. Анализ плана запроса
Ядро SQL Server Database Engine может показывать, каким образом оно переходит к таблицам и использует индексы для доступа к данным. Это называется выводом плана выполнения. Для проведения анализа медленно выполняемого запроса полезно изучить план выполнения запроса, чтобы определить причину проблемы.
Планы выполнения запросов в MS SQL отображаются cредой SQL Server Management Studio. При этом отображается наилучший план выполнения, используемый ядром Database Engine для отдельных инструкций языка DML и Transact-SQL. В этом плане содержатся сведения о процессе компиляции хранимых процедур и о вызовах хранимых процедур произвольной глубины вложенности. Например, при выполнении инструкции SELECT можно увидеть, что Database Engine выполняет просмотр таблицы для получения данных. Выполнение инструкции SELECT может также показать, что просмотр индекса будет использоваться, если Database Engine определит, что просмотр индекса является наиболее быстрым способом получения данных из таблицы.
Таким образом, выбор оптимального плана во многом зависит от наличия в базе индексов и их оптимальности.
При выполнении запроса в MSSQL есть возможность поиска “отсутствующих” индексов. Т. е. MS SQL подсказывает, какие индексы можно создать, чтобы данный конкретный запрос выполнялся быстрее.
Пример:
Запрос выдает рекомендации на создание индекса:
4.2. Способы применения индекса в планах запроса
Основные типы получения записей индекса это Seek и Scan.
Index Scan получает все записи индекса, указанного в столбце Argument. Если необязательный предикат WHERE:() появится в столбце Argument, то возвращаются только те строки, которые удовлетворяют условию, указанному в этом предикате.
Оператор Index Seek использует возможности поиска по индексам для получения строк из индекса. С самого начала поиск обрабатывает только те строки, которые квалифицированы, и страницы, которые содержат эти квалифицированные строки, поэтому стоимость пропорциональна числу квалифицированных строк и содержащих их страниц, а не к общему числу строк в таблице. Таким образом, поиск является более эффективной стратегией, если мы имеем дело с очень селективным поисковым предикатом; то есть если мы имеем поисковый предикат, который отсекает большую часть таблицы.
4.3. Способы изменения плана
В MS SQL при выполнении запросов можно указать специальные «подсказки» (hint), с помощью которых можно повлиять на план запроса.
Оптимизатор запросов SQL Server обычно автоматически выбирает наилучший план выполнения запроса. Поэтому подсказки, в том числе , рекомендуется использовать только опытным пользователям и администраторам базы данных в случае крайней необходимости.
Примечание. В некоторых СУБД, например Firebird, можно косвенно влиять на план запроса, например добавлением «+0» к полю таблицы. В этом случае оптимизатор перестает использовать индекс.
4.4. Кэширование и повторное использование плана
В SQL Server есть пул памяти, предназначенный для хранения планов выполнения и буферов данных. Процентное соотношение размера пула, выделенного для планов выполнения и буферов данных, динамически изменяется в зависимости от состояния системы. Часть пула памяти, используемого для хранения планов выполнения, называется кэшем процедур.
В SQL Server планы выполнения состоят из следующих основных компонентов.
План запроса. Тело плана выполнения является структурой данных только для чтения, которая предназначена для использования любым числом пользователей. Оно называется планом запроса. План запроса не содержит контекста пользователя.
Контекст выполнения. Для каждого пользователя, который в настоящий момент выполняет запрос, имеется структура данных, которая содержит данные, относящиеся к данному выполнению, например значения параметров. Эта структура данных называется контекстом выполнения. Структуры данных контекста выполнения являются повторно используемыми. Если пользователь выполняет запрос и одна из структур не используется, она повторно инициализируется контекстом нового пользователя.
4.5. Проблема параметров при кэшировании запросов
Если к серверу выполняется несколько однотипных параметризованных запросов, то сервер строит план только для первого запроса, далее этот план попадает в кэш, и остальные запросы выполняются с уже кэшированным планом.
Рассмотрим 2 запроса:
Для выполнения этих запросов используются разные планы, т. к. при построении плана, MS SQL анализирует количество возвращаемых данных.
Если данный запрос сделать параметризованным:
то план запроса будет определятся самым первым запросом.
Это демонстрирует следующий скрипт:
Таким образом, оптимальный план, построенный по первому параметризованному запросу, из-за кэширования будет распространен на остальные запросы, где этот план может оказаться, мягко говоря, не совсем оптимальным.
5. Подходы к оптимизации запросов
5.1. Поиск неоптимальных запросов
Критичные запросы. Выполняются достаточно редко, но их длительное время выполнения заметно невооруженным взглядом.
Часто выполняемые запросы, которые сами по себе не занимают много времени, но из-за того, что выполняются часто, общее время их выполнения становится критичным.
К сожалению, самый надежный метод довольно сложен и предусматривает отслеживание всех выполненных запросов к серверу с их последующей группировкой.
Существует более простой, но не столь надежный метод. Можно положиться на статистику всех запросов, которая хранится в кэше плана выполнения, и опросить их с использованием динамических административных представлений.
5.2. Статистика индексов
Для индекса существует понятие плотности распределения. Если каждое значение в таблице уникальное, то плотность будет 1/ . Например, если в таблице 100 записей, то плотность будет равна 1%. Теория говорит, что чем меньше плотность, тем лучше – это увеличивает избирательность, а, следовательно, и ценность построенного индекса.
Например, если колонка содержит только 3 значения, плотность распределения будет равна 33.3%, что показывает бесполезность построения индекса по данному полю. Индексы занимают место на диске и в оперативной памяти и отнимают быстродействие. В идеале, самый лучший индекс имеет плотность распределения равную единице, деленной на количество записей в таблице — все записи уникальны.
При построении индексов, обращайте внимание на плотность распределения – если она превышает 10%, то индекс можно считать бесполезным. Сканирование по таблице в таком случае будет более эффективным.
Для просмотра статистики индекса используется команда:
5.3. Фильтрация в индексах
Фильтруемый индекс — это оптимизированный некластеризованный индекс, особенно подходящий для запросов, осуществляющих выборку из хорошо определенного подмножества данных. Он использует предикат фильтра для индексирования части строк в таблице. Хорошо спроектированный фильтруемый индекс позволяет повысить производительность запросов, а также снизить затраты на обслуживание и хранение индексов по сравнению с полнотабличными индексами.
Пример. Отберем все строки продуктов, где количество больше 9900.
Создадим индекс по полю Quantity:
Создадим фильтрованный индекс:
Кроме того, фильтрованный индекс создается гораздо быстрее.
5.4. Индексы с включенными полями
Индекс с неключевыми столбцами может значительно повысить производительность запроса, когда все столбцы запроса включены в индекс как ключевые или неключевые. Производительность повышается благодаря тому, что оптимизатор запросов может найти все значения столбцов в этом индексе. При этом нет обращения к данным таблиц или кластеризованных индексов, что приводит к меньшему количеству дисковых операций ввода-вывода.
Пример. Допустим, необходимо найти номера и даты заказов по определенному поставщику.
Запрос:
Создаем индекс на поле CustomerId:
Результат такой же. MSSQL индекс не использует. Оптимизатор решает, что быстрее сделать Scan по первичному ключу, т.к. селективность индекса маленькая.
Создаем индекс с включенными полями:
Время выполнения запроса уменьшается, индекс попадает в план:
5.5. Дополнительные команды при создании индексов
Команда CREATE INDEX имеет дополнительные параметры. Рассмотрим некоторые из них, которые в дальнейшем могут повлиять на производительность запросов.
ASC|DESC
ASC значит что индекс будет построен по возрастанию ключей. DESC соответственно – по убыванию. Эта опция не дает никакой разницы на поиск данных, но оказывает существенное влияние на скорость выполнения ORDER BY опции в запросах.
Пример.
SORT_IN_TEMPDB
Полезна при создании индексов. Эта опция может увеличить производительность системы во время создания индексов. Индексы создаются в два этапа. На первом создается временный набор данных с отсортированными ключами для некластерного индекса. На втором – окончательный результат переносится на место его хранения в базе данных. Без указания данной опции временный результат создается в той же file group, где и будет создан индекс. При указанной опции временный результат будет находиться в базе данных Tempdb. Преимущества: индекс будет менее фрагментирован, временную базу можно поместить на другой диск и тем самым увеличить скорость чтения/записи. Недостаток: требуется больше памяти на дисках и надо уделять дополнительное внимание базе данных Tempdb.
5.6. Оптимизация композитных индексов
В создании композитного (сложного) индекса участвуют несколько полей таблицы. При создании индекса следует обращать внимание на порядок следования полей в индексе.
Например, если создается индекс по полям Field1, Field2, то он может быть применен только в запросе, где в критериях используются оба этих поля. Также этот индекс будет полезен для условий, построенных для одного Field1. Для одного Field2 этот индекс не может быть применен.
Если в дополнение к индексу по полям Field1, Field2 добавить индекс по полям Field2, Field1, то SQL Server при построении плана запроса будет анализировать, какой из них более селективен в применении к ограничениям на условия запроса. Последний момент в построении композитного индекса по полям Field1, Field2 – он не равен сумме индексов по указанным полям. В случае, когда в запросе могут быть использованы оба поля как критерий поиска при раздельных индексах по полям, будет построено пересечение по индексам, что медленнее чем выборка из композитного индекса.
Примечание. В некоторых СУБД, например Firebird, применение нескольких индексов в запросе для одной таблицы ведет к существенному снижению производительности.
Пример. Отберем заказы определенного клиента, начиная с указанной даты.
План показывает сканирование по первичному ключу. MS SQL рекомендует создание композитного индекса по коду поставщика и дате.
Создадим индекс:
Тот же самый запрос стал выполняться быстрее.
Уберем из условия отбор по дате:
Индекс по-прежнему используется.
Теперь вернем условие по дате, но уберем условие по клиенту:
Индекс используется, только вместо более быстрой операции Seek используется Scan.
5.7. Общие рекомендации по проектированию индексов
Большое количество индексов в таблице снижает производительность инструкций INSERT, UPDATE, DELETE и MERGE, потому что при изменении данных в таблице все индексы должны быть изменены соответствующим образом.
Большое число индексов может повысить производительность запросов, которые не изменяют данных, таких как инструкции SELECT, поскольку у оптимизатора запросов будет больший выбор индексов при определении самого быстрого способа доступа.
Индексирование маленьких таблиц может оказаться не лучшим выбором, так как поиск данных в индексе может потребовать у оптимизатора запросов больше времени, чем простой просмотр таблицы. Следовательно, для маленьких таблиц индексы могут вообще не использоваться, но тем не менее их необходимо поддерживать при изменении данных в таблице.
5.8. Выбор типа соединения таблиц
Выбор типа соединения таблиц в запросе (INNER/LEFT/RIGHT) зависит от задачи. Практика показывает, что если в запросе, который подразумевает однозначное соединение одной таблицы с другой, нет большой разницы, использовать INNER или LEFT JOIN. Оптимизатор генерирует для таких запросов одинаковый план.
Пример:
5.9. Рекомендации по оптимизации запросов от Microsoft
Ниже перечислены случаи, когда SQL Server не в состоянии точно вычислить мощность. Это приводит к неточному определению стоимости, что, в свою очередь, может привести к созданию неоптимальных планов запросов. Если избегать применения таких конструкций, можно повысить производительность выполнения запросов. Иногда в таких случаях доступны альтернативные формулировки запросов и другие средства, помогающие решить эту проблему.
- Запросы с предикатами, которые содержат операторы сравнения столбцов одной и той же таблицы.
- Запросы с предикатами, использующими операторы, и выполнение одного из следующих условий:
- отсутствует статистика для столбца, указанного с любой стороны от оператора;
- распределение значений в статистике неоднородно, а запрос выполняет поиск весьма ограниченного набора значений. Эта ситуация особенно вероятна, если оператор не является оператором равенства (=);
- предикат использует оператор неравенства (!=) или логический оператор NOT.
Для повышения производительности перечисленных типов запросов можно попробовать предпринять следующие меры:
Постройте индексы или статистику для столбцов, участвующих в запросе.
Попробуйте использовать вычисляемые столбцы или перепишите запрос, если он содержит операторы, которые сравнивают или производят арифметические операции над двумя и более столбцами. Например, в следующем запросе сравниваются значения в двух столбцах:
Можно повысить производительность, добавив вычисляемый столбец Col3 к таблице MyTable, который подсчитывает разницу между Col1 и Col2 (Col1 минус Col2). Затем переписать запрос:
Возможно, производительность еще больше повысится, если построить индекс для столбца MyTable.Col3.
Во второй части статьи «Оптимизация SQL-запросов (Часть 2)» рассматриваются рекомендации по разработке оптимальной структуры БД.






