Описание языка VRML. О методике изложения

ВАЛЕРИЙ МИХЕИЧЕВ , эксперт Oracle, ОСАО «Ингосстрах», [email protected]

Причины неэффективности SQL-запросов в Oracle
Оптимизация производительности SQL-запросов

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

Диагностика эффективности выполнения запросов

Существуют разнообразные методы диагностики производительности работы запросов, например, использование трассировочных файлов, средства Oracle DBMS_SQLTUNE (с выдачей рекомендаций по оптимизации запроса), AWR (Автоматический репозиторий рабочей нагрузки) и др.

Среди средств диагностики производительности работы запросов наиболее простым и довольно действенным является просмотр планов выполнения запросов через инструменты Toad, PL/SQL Developer и др., а также на основе Oracle-представлений, например, таких как v$sql_plan (v$sql_plan_monitor появился в Oracle 11g) и v$sql_bind_capture – последний показывает значения переменных, используемых в запросе.

При этом следует заметить, что, если через Oracle-представления v$sql_plan и v$sql_plan_monitor мы получаем реальный план выполнения запроса, то через подстановку текста запроса в Toad, PL/SQL Developer и другие аналогичные средства можем получить предполагаемый, а не реальный план выполнения.

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

  • Cost – стоимость выполнения;
  • CPU Cost – процессорная стоимость выполнения;
  • IO Cost – стоимость ввода-вывода;
  • Temp Space – показатель использования запросом временного пространства.

Чем больше значение первых трех показателей, тем менее эффективен запрос.

Наличие ненулевых значений в показателе Temp Space говорит об использовании временного пространства (например, для проведения сортировок, группировок, hash-join и т.д.), при этом с большой вероятностью можно говорить о неэффективности работы такого запроса, и начинатьискать проблемы следует с анализа строк, где стоят Temp Space.

Опыт оптимизации запросов показал, что анализ планов выполнения имеет определенную последовательность действий:

  • план выполнения начинают просматривать снизу вверх. В процессе просмотра в первую очередь обращается внимание на строки с большими значениями Cost и CPU Cost;
  • помимо поиска больших Cost и CPU Cost, в плане следует обратить внимание на наличие в нем полного сканирования таблиц и индексов: FULL – для таблиц и FULL SCAN или FAST FULL SCAN – для индексов, в том числе, обратить внимание на SKIP SCAN индексов (поскольку там тоже могут таиться проблемы). В плане, полученном из v$sql_plan, для выявления наличия полного сканирования таблиц или индексов следует исследовать столбец Options (Plan_Options для v$sql_plan_monitor);
  • следует также обратить внимание на наличие в плане фразы Hash_Join. Соединение по Hash_Join приводит к соединению таблиц в памяти и, казалось бы, более эффективно, чем вложенные соединения Nested Loops. Вместе с тем Hash_Join эффективно при наличии таблиц, хотя бы одна из которых помещается в память базы данных, или при наличии соединения таблиц с низко селективными индексами. Недостатком этого соединения является то, что при нехватке памяти для таблицы (таблиц) будут задействованы диски, которые существенно затормозят работу запроса (появится в плане показатель Temp Space). В связи с чем при наличии высокоселективных индексов целесообразно посмотреть, а не улучшит ли план выполнения запроса хинт (подсказка оптимизатору) Use_NL, приводящий к соединению по вложенным циклам Nested Loops. Если план будет лучше, то нужно оставить этот хинт. В плане, полученном из v$sql_plan, для выявления Hash_Join следует исследовать столбец Operations;
  • стоит обратить внимание также на наличие в плане фразы Merge Join Cartesian, которая говорит о том, что между какими-то таблицами нет полной связки. Решением проблемы может быть добавление недостающей связки, иногда помогает использование хинта Ordered.

К вышеуказанныму следует добавить детализирующие параметры, такие как Elapsed_time, CPU_time, Executions, Disk Read и Buffer Gets. Первый показывает полное время выполнения запроса, второй – процессорное время выполнения запроса, третий – число выполнений запроса, четвертый и пятый – соответственно интенсивность использования дисков и памяти.

Как показывает опыт, при значении Disk Reads более 250 000 имеем интенсивное использование дисков, а при Buffer Gets более 10 000 000 на одно выполнение имеем интенсивное использование памяти, т.е. в том и другом случаях имеем проблемы в запросе.

Проблемы в запросе также имеются, если число Elapsed_time существенно превосходит CPU_time (что, как правило, связано с событиями ожидания). Значения параметров можно получить из представления Oracle v$sql или v$sql_monitor по sql_id (уникальный идентификатор запроса), а если sql_id неизвестен, то по уникальным элементам текста запроса из этих представлений. Для поиска указанных параметров в долго работающем запросе удобно представление v$sql_monitor, которое в отличие от старого представления v$sql имеет столбец sid-сессии, позволяющий выявить долго работающие запросы, выполняющиеся в данной сессии, и их sql_id.

Широкий спектр информации по всем четырем параметрам можно получить из такого мощного средства диагностики запросов, как AWR (его можно запустить из Toad – меню Database – подменю Monitor – ADDM/AWR Reports).

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

Select * from TABLE(DBMS_WORKLOAD_REPOSITORY.AWR_REPORT_TEXT((select dbid from V$DATABASE), 1, (select max(snap_id)-1 from sys.WRM$_SNAPSHOT), (select max(snap_id) from sys.WRM$_SNAPSHOT)));

Статью целиком читайте в журнале «Системный администратор», №6 за 2015 г. на страницах 47-51.

PDF-версию данного номера можно приобрести в нашем магазине .


Вконтакте

От автора: вы знаете, что быстрый сайт == счастливые пользователи, выше рейтинг в google и повышенная конверсия. Возможно, вы думаете, что ваш сайт на WordPress максимально быстрый: вы смотрите за производительностью по статьям «лучшие практики настройки сервера», «решение проблем с медленным кодом» и «перемещение изображений на CDN». Но все ли это?

В динамических сайтах типа WordPress с базой данных у вас будет одна и та же проблема – запросы к базе данных замедляют сайт. В этой статье я покажу вам, как делается оптимизация SQL запросов, вызывающих замедления, и как понять, в чем проблема в этих запросах. Я буду использовать реальный запрос, который мы недавно правили, так как он замедлял портал покупателя deliciousbrains.com.

Определение

Первый шаг в решении медленных SQL запросов – найти их. Ashley восхваляла плагин отладки Query Monitor в блоге. Бесценным этот инструмент делает функция плагина запросов к базе данных для определения медленных SQL запросов. Вы можете фильтровать запросы по коду или компоненту (плагин, тема или ядро WordPress), которые их совершают. А также плагин умеет выделять повторяющиеся и медленные запросы:

Если не хотите ставить плагин отладки на продакшн сайт (может, вас волнует производительность), можете запустить MySQL Slow Query Log , который логирует все запросы, занимающие определенное время на выполнение. Инструмент очень легко настроить и указать папку для логирования запросов. Так как это серверное решение, то удар на производительность будет меньше, чем плагин отладки. Однако когда он не нужен, его нужно отключить.

Понятие

MySQL

SELECT l.key_id, l.order_id, l.activation_email, l.licence_key, l.software_product_id, l.software_version, l.activations_limit, l.created, l.renewal_type, l.renewal_id, l.exempt_domain, s.next_payment_date, s.status, pm2.post_id AS "product_id", pm.meta_value AS "user_id" FROM oiz6q8a_woocommerce_software_licences l INNER JOIN oiz6q8a_woocommerce_software_subscriptions s ON s.key_id = l.key_id INNER JOIN oiz6q8a_posts p ON p.ID = l.order_id INNER JOIN oiz6q8a_postmeta pm ON pm.post_id = p.ID AND pm.meta_key = "_customer_user" INNER JOIN oiz6q8a_postmeta pm2 ON pm2.meta_key = "_software_product_id" AND pm2.meta_value = l.software_product_id WHERE p.post_type = "shop_order" AND pm.meta_value = 279 ORDER BY s.next_payment_date

SELECT

l.activation_email,

l.software_product_id,

l.software_version,

l.activations_limit,

l.exempt_domain,

s.next_payment_date,

s.status ,

pm2.post_idAS "product_id" ,

pm.meta_valueAS "user_id"

FROM

oiz6q8a_woocommerce_software_licencesl

INNER JOIN

oiz6q8a_woocommerce_software_subscriptionssON s.key_id=l.key_id

INNER JOIN

oiz6q8a_postspON p.ID=l.order_id

INNER JOIN

oiz6q8a_postmetapmON pm.post_id=p.ID

AND pm.meta_key="_customer_user"

INNER JOIN

oiz6q8a_postmetapm2ON pm2.meta_key="_software_product_id"

AND pm2.meta_value=l.software_product_id

p.post_type="shop_order"

AND pm.meta_value=279

ORDER BY s.next_payment_date

Для запуска магазина плагинов мы использовали WooCommerce и плагин WooCommerce Software Subscriptions. Суть запроса – получение всех подписок покупателя по его номеру. В WooCommerce довольно сложная модель данных. Хотя заказ и хранится в виде кастомного типа поста, но id покупателя не хранится в post_author, а является частью post мета данных. Также есть несколько подключений к пользовательским таблицам, созданным плагином подписки на ПО. Давайте более подробно разберем запрос.

MySQL твой друг

В MySQL есть полезное выражение DESCRIBE, с помощью которого можно выводить информацию о структуре таблицы (ее колонки, типы данных, значения по умолчанию). Если выполнить DESCRIBE wp_postmeta;, то вы увидите:

Круто, но вы, возможно, уже знаете об этом. Но знали ли вы, что префикс DESCRIBE можно использовать на SELECT, INSERT, UPDATE, REPLACE и DELETE? Более широко известен синоним EXPLAIN, который даст нам подробную информацию о том, как будет выполняться выражение.

Результат для медленного запроса:

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

Самая главная колонка type, в ней описывается способ объединения таблиц. Если стоит ALL, значит MySQL читает всю таблицу с диска, увеличивает скорость чтения/записи и перекладывает загрузку на CPU. Процесс называется полное сканирование таблицы (более подробно позже).

Колонка rows также указывает на то, что MySQL должен делать. Она показывает, сколько строк необходимо просмотреть для нахождения результата.

Explain дает больше информации, чем мы можем оптимизировать. Например, таблица pm2 (wp_postmeta) говорит нам, что мы используем Using filesort, так как хотим, чтобы результаты хранились с помощью выражения ORDER BY. Если бы мы еще группировали запрос, это добавило бы еще больше нагрузки на выполнение.

Визуальное расследование

MySQL Workbench – еще один хороший бесплатный инструмент для расследований. Для баз данных MySQL 5.6 и выше результат EXPLAIN можно вывести в виде JSON, а MySQL Workbench превращает этот JSON в визуальный план выполнения выражения:

Он автоматически рисует предупреждения по проблемам, окрашивая части запроса, которые будут долго выполняться. Мы сразу видим, что присоединение к таблице wp_woocommerce_software_licences (алиас |) добавляет проблем.

Решение

Часть запроса выполняет полное сканирование таблицы, чего следует избегать, так как она использует колонку без индекса order_id для объединения между таблицами wp_woocommerce_software_licences и wp_posts. Распространенная проблема в медленных запросах. Ее очень легко решить.

Индексы

Order_id – довольно важная часть определения данных в базе данных, и если мы будем строить запрос таким образом, то нам понадобится добавить индекс в эту колонку, или MySQL будет буквально сканировать все строки таблицы, пока не найдет необходимые. Добавим индекс и посмотрим, что он даст:

MySQL

CREATE INDEX order_id ON wp_woocommerce_software_licences(order_id)

CREATE INDEX order_idON wp_woocommerce_software_licences(order_id)

Нам удалось сэкономить 5 секунд запроса простым добавлением индекса, круто!

Знайте свой запрос

Проанализируйте запрос – join за join, подзапрос за подзапросом. Запрос делает то, что не должен? Его можно оптимизировать?

В нашем случае мы подключаем таблицу лицензий к таблице постов с помощью order_id, постоянно ограничивая выражение типами постов shop_order. Это необходимо для обеспечения целостности данных, чтобы убедиться, что мы используем только правильные записи заказов. На самом деле эта часть запроса устарела. Мы знаем, что безопаснее, чтобы в строке лицензии ПО в таблице был order_id, относящийся к заказу WooCommerce в таблице постов, поскольку это применяется в коде плагина PHP. Давайте удалим join и посмотрим, улучшит ли это запрос:

Несильно, но запрос теперь выполняется меньше 3 секунд.

Кэшируйте все!

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

Query Monitor выяснил, что наш запрос запускается 4 раза за одну загрузку страницы. Хотя и хорошо кэшировать MySQL запросы, но дублировать считывания из базы данных в одном запросе точно не стоит. Статическое кэширование в коде PHP – простой и крайне эффективный способ решения проблемы. Вы получаете результат запроса из базы данных при первом запросе и храните результат в статическом свойстве класса. В последующих вызовах будет возвращаться результат из статического свойства:

class WC_Software_Subscription { protected static $subscriptions = array(); public static function get_user_subscriptions($user_id) { if (isset(static::$subscriptions[ $user_id ])) { return static::$subscriptions[ $user_id ]; } global $wpdb; $sql = "..."; $results = $wpdb->get_results($sql, ARRAY_A); static::$subscriptions[ $user_id ] = $results; return $results; } }

class WC_Software_Subscription {

protected static $ subscriptions = array () ;

public static function get_user_subscriptions ($ user _ id) {

$ user _ id] = $ results ;

return $ results ;

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

Мыслите нестандартно

Можно предпринять и другие способы для ускорения выполнения запросов, в которых нужно чуть больше, чем просто поправить запрос или добавить индекс. Одна из самых медленных частей нашего запроса – процесс объединения таблиц для перехода от id покупателя к id товара, и это необходимо делать для каждого покупателя. А что если сделать все объединения за раз, чтобы получать данные о покупателе тогда, когда это необходимо?

Вы можете денормализовать данные, создав таблицы, в которой хранятся данные лицензии, а также id пользователя и товара для всех лицензий, а также запрос к конкретному покупателю. Понадобится пересобрать таблицу с помощью MySQL triggers на INSERT/UPDATE/DELETE для таблицы лицензий (или других в зависимости от изменения данных), но это значительно повысит производительность запроса данных.

Если несколько join замедляют запрос, можно ускорить его, разбив на 2 и более выражения, после чего выполнять их отдельно в PHP, собирать и фильтровать результаты в коде. Laravel делает что-то похожее в жадной загрузке в Eloquent.

WordPress может замедлять запросы к таблице wp_posts, если объем данных большой, и присутствует множество кастомных типов постов. Если запросы типов постов замедляют сайт, попробуйте уйти от модели хранения кастомных типов постов в сторону кастомной таблицы.

Результаты

С помощью этих подходов по оптимизации запросов нам удалось ускорить наш запрос с 8 до 2 секунд, а также снизить количество вызовов с 4 до 1. Время запросов записывалось на версии для разработки, в продакшн оно было бы меньше.

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

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

  1. Оптимизация таблиц . Необходима, когда было произведено много изменений в таблице: либо удалена большая часть данных, либо много изменений со строками переменной длины - text, varchar, blob. Дело в том, что удалённые записи продолжают поддерживаться в индексном файле, и при последующей вставке новых записей используются позиции старых записей. Чтобы дефрагментировать файл с данными, используюется команда OPTIMIZE.

    OPTIMIZE TABLE `table1`, `table2`…

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

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

    ALTER TABLE `table1` ORDER BY `id`

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

    NOT NULL и поле по умолчанию . Лучше всего помечать поля как NOT NULL, так как они немного экономят место и исключают лишние проверки. При этом стоит задавать значение полей по умолчанию и новые данные вставлять только в том случае, если они от него отличаются. Это ускорит добавление данных и снизит время на анализ таблиц. И стоит помнить, что типы полей BLOB и TEXT не могут содержать значения по умолчанию.

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

    Разделение данных. Длинные не ключевые поля советуют выделить в отдельную таблицу в том случае, если по исходной таблице происходит постоянная выборка данных и которая часто изменяется. Данный метод позволит сократить размер изменяемой части таблицы, что приведёт к сокращению поиска информации.
    Особенно это актуально в тех случаях, когда часть информации в таблице предназначена только для чтения, а другая часть - не только для чтения, но и для модификации (не забываем, что при записи информации блокируется вся таблица). Яркий пример - счётчик посещений.
    Есть таблица (имя first) с полями id, content, shows. Первое ключевое с auto_increment, второе - текстовое, а третье числовое - считает количество показов. Каждый раз загружая страницу, к последнему полю прибавляется +1. Отделим последнее поле во вторую таблицу. Итак, первая таблица (first) будет с полями id, content, а вторая (second) с полями shows и first_id. Первое поле понятно, второе думаю тоже - отсыл к ключевому полю id из первой таблицы.
    Теперь постоянные обновления будут происходить во второй таблице. При этом изменять количество посещений лучше не программно, а через запрос:

    А выборка будет происходить усложнённым запросом, но одним, двух не нужно:

    SELECT first.id, first.content, second.first_id, second.shows FROM second INNER JOIN first ON (first.id = second.first_id)

    Стоит помнить, что всё это не актуально для сайтов с малой посещаемостью и малым количеством информации.

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

    Требовать меньше данных . При возможности избегать запросов типа:

    SELECT * FROM `table1`

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

    SELECT id, name FROM table1 ORDER BY id LIMIT 25

    Тут же сделаю добавление о желательности использования LIMIT. Данная команда ограничивает количество строк, возвращаемых запросом. То есть запрос становится "легче" и производительнее.
    Если стоит LIMIT 10, то после получения десяти строк запрос прерывается.
    Если в запросе применяется сортировка ORDER BY, то она происходит не по всей таблице, а только по выборке.
    Если использовать LIMIT совместно с DISTINCT, то запрос прервётся после того, как будет найдено указанное количество уникальных строк.
    Если использовать LIMIT 0, то возвращено будет пустое значение (иногда нужно для определения типа поля или просто проверки работы запроса).

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

    Ограничить использование SELECT для постоянно изменяющихся таблиц .

  3. Не забывайте про временные таблицы типа HEAP . Несмотря на то, что таблица имеет ограничения, в ней удобно хранить промежуточные данные, особенно когда требуется сделать ещё одну выборку из таблицы без повторного обращения. Дело в том, что эта таблица хранится в памяти и поэтому доступ к ней очень быстрый.
  4. Поиск по шаблону . Зависит от размера поля и если уменьшить размер с 400 байтов до 300, то время поиска сократиться на 25%.


Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: