...

суббота, 1 февраля 2014 г.

[Перевод] Использование EXPLAIN. Улучшение запросов

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


Что предлагает EXPLAIN?




Использовать оператор EXPLAIN просто. Его необходимо добавлять в запросы перед оператором SELECT. Давайте проанализируем вывод, чтобы познакомиться с информацией, возвращаемой командой.

********************** 1. row **********************

id: 1

select_type: SIMPLE

table: categories

type: ALL

possible_keys: NULL

key: NULL

key_len: NULL

ref: NULL

rows: 4

Extra:

1 row in set (0.00 sec)


Вывод может не выглядеть точь-в-точь так, тем не менее, в нем будут содержаться те же 10 столбцов. Что же это за возвращаемые столбцы?



  • id – порядковый номер для каждого SELECT’а внутри запроса (когда имеется несколько подзапросов)

  • select_type – тип запроса SELECT.

    • SIMPLE — Простой запрос SELECT без подзапросов или UNION’ов

    • PRIMARY – данный SELECT – самый внешний запрос в JOIN’е

    • DERIVED – данный SELECT является частью подзапроса внутри FROM

    • SUBQUERY – первый SELECT в подзапросе

    • DEPENDENT SUBQUERY – подзапрос, который зависит от внешнего запроса

    • UNCACHABLE SUBQUERY – не кешируемый подзапрос (существуют определенные условия для того, чтобы запрос кешировался)

    • UNION – второй или последующий SELECT в UNION’е

    • DEPENDENT UNION – второй или последующий SELECT в UNION’е, зависимый от внешнего запроса

    • UNION RESULT – результат UNION’а




  • Table – таблица, к которой относится выводимая строка

  • Type — указывает на то, как MySQL связывает используемые таблицы. Это одно из наиболее полезных полей в выводе потому, что может сообщать об отсутствующих индексах или почему написанный запрос должен быть пересмотрен и переписан.

    Возможные значения:

    • System – таблица имеет только одну строку

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

    • Eq_ref – все части индекса используются для связывания. Используемые индексы: PRIMARY KEY или UNIQUE NOT NULL. Это еще один наилучший возможный тип связывания.

    • Ref – все соответствующие строки индексного столбца считываются для каждой комбинации строк из предыдущей таблицы. Этот тип соединения для индексированных столбцов выглядит как использование операторов = или < = >

    • Fulltext – соединение использует полнотекстовый индекс таблицы

    • Ref_or_null – то же самое, что и ref, но также содержит строки со значением null для столбца

    • Index_merge – соединение использует список индексов для получения результирующего набора. Столбец key вывода команды EXPLAIN будет содержать список использованных индексов.

    • Unique_subquery – подзапрос IN возвращает только один результат из таблицы и использует первичный ключ.

    • Index_Subquery – тоже, что и предыдущий, но возвращает более одного результата.

    • Range – индекс, использованный для нахождения соответствующей строки в определенном диапазоне, обычно, когда ключевой столбец сравнивается с константой, используя операторы вроде: BETWEEN, IN, >, >=, etc.

    • Index – сканируется все дерево индексов для нахождения соответствующих строк.

    • All – Для нахождения соответствующих строк используются сканирование всей таблицы. Это наихудший тип соединения и обычно указывает на отсутствие подходящих индексов в таблице.




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

  • Key – указывает актуальный, использованный MySQL, индекс. Этот столбец может содержать индекс, не указанный в столбце possible_keys. Оптимизатор MySQL всегда ищет оптимальный ключ, который она может использовать для запроса. В процессе соединения таблиц она может находить какие-то другие ключи, которые не отображены в possible_keys, но являются более оптимальными для использования.

  • Key_len – длина индекса, которую оптимизатор MySQL выбрал для использования. Например, значение key_len, равное 4, означает, что памяти требуется для хранения 4 знаков. На эту тему вот ссылка — vhttp://dev.mysql.com/doc/refman/5.0/en/storage-requirements.html

  • Ref – указываются столбцы или константы, которые сравниваются с индексом, указанным в поле key. MySQL выберет либо значение константы для сравнения, либо само поле, основываясь на плане выполнения запроса.

  • Rows – отображает число записей, обработанных для получения выходных данных. Это еще одно очень важное поле, которое дает повод оптимизировать запросы, особенно те, которые используют JOIN’ы и подзапросы.

  • Extra – содержит дополнительную информацию, относящуюся к плану выполнения запроса. Такие значения как “Using temporary”, “Using filesort” и т.д могут быть индикатором проблемного запроса. С полным списком возможных значений вы можете ознакомиться здесь — http://ift.tt/1hZNo3C


После EXPLAIN в запросе вы можете использовать ключевое слово EXTENDED и MySQL покажет вам дополнительную информацию о том, как выполняется запрос. Чтобы увидеть эту информацию, вам нужно сразу после запроса с EXTENDED выполнить запрос SHOW WARNINGS. Наиболее полезно смотреть эту информацию о запросе, который выполнялся после каких-либо изменений сделанных оптимизатором запросов.


EXPLAIN EXTENDED SELECT City.Name FROM City JOIN Country ON (City.CountryCode = Country.Code) WHERE City.CountryCode = 'IND' AND Country.Continent = 'Asia'


********************** 1. row **********************

id: 1

select_type: SIMPLE

table: Country

type: const

possible_keys: PRIMARY

key: PRIMARY

key_len: 3

ref: const

rows: 1

filtered: 100.00

Extra:

********************** 2. row **********************

id: 1

select_type: SIMPLE

table: City

type: ALL

possible_keys: NULL

key: NULL

key_len: NULL

ref: NULL

rows: 4079

filtered: 100.00

Extra: Using where

2 rows in set, 1 warning (0.00 sec)


SHOW WARNINGS


********************** 1. row **********************

Level: Note

Code: 1003

Message: select `World`.`City`.`Name` AS `Name` from `World`.`City` join `World`.`Country` where ((`World`.`City`.`CountryCode` = 'IND'))

1 row in set (0.00 sec)


Поиск и устранение проблем с производительностью с помощью EXPLAIN.




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

Я создал тестовую базу данных для приложения электронной торговли, которая не имеет никаких индексов или первичных ключей, и продемонстрирую влияние такого не очень хорошего способа создания таблиц при помощи “страшных” запросов. Дамп это таблицы вы можете скачать здесь — http://ift.tt/1hZNmIV


EXPLAIN SELECT * FROM

orderdetails d

INNER JOIN orders o ON d.orderNumber = o.orderNumber

INNER JOIN products p ON p.productCode = d.productCode

INNER JOIN productlines l ON p.productLine = l.productLine

INNER JOIN customers c on c.customerNumber = o.customerNumber

WHERE o.orderNumber = 10101


Если вы посмотрите на результат (на него вам придется посмотреть только в примере ниже, по ссылке выше лежит дамп с уже добавленными ключами), то увидите все симптомы плохого запроса. Но даже если я напишу запрос получше, результат будет тем же самым, пока я не добавлю индексов. Указанный тип соединения ALL (худший), что означает, что MySQL не смог определить ни одного ключа, который бы мог использоваться при соединении. Отсюда следует и то, что possible_keys и key имеют значение NULL. Самым важным является то, что поле rows показывает, что MySQL сканирует все записи каждой таблицы для запроса. Это означает, что она просканирует 7 × 110 × 122 × 326 × 2996 = 91,750,822,240 записей, чтобы найти подходящие четыре (уберите из запроса EXPLAIN, проверьте сами). Это очень нехорошо и количество этих записей будет экспоненциально увеличиваться по мере роста базы данных.


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


ALTER TABLE customers

ADD PRIMARY KEY (customerNumber);

ALTER TABLE employees

ADD PRIMARY KEY (employeeNumber);

ALTER TABLE offices

ADD PRIMARY KEY (officeCode);

ALTER TABLE orderdetails

ADD PRIMARY KEY (orderNumber, productCode);

ALTER TABLE orders

ADD PRIMARY KEY (orderNumber),

ADD KEY (customerNumber);

ALTER TABLE payments

ADD PRIMARY KEY (customerNumber, checkNumber);

ALTER TABLE productlines

ADD PRIMARY KEY (productLine);

ALTER TABLE products

ADD PRIMARY KEY (productCode),

ADD KEY (buyPrice),

ADD KEY (productLine);

ALTER TABLE productvariants

ADD PRIMARY KEY (variantId),

ADD KEY (buyPrice),

ADD KEY (productCode);


Давайте выполним наш прежний запрос после добавления индексов. Вы увидите это:


********************** 1. row **********************

id: 1

select_type: SIMPLE

table: o

type: const

possible_keys: PRIMARY,customerNumber

key: PRIMARY

key_len: 4

ref: const

rows: 1

Extra:

********************** 2. row **********************

id: 1

select_type: SIMPLE

table: c

type: const

possible_keys: PRIMARY

key: PRIMARY

key_len: 4

ref: const

rows: 1

Extra:

********************** 3. row **********************

id: 1

select_type: SIMPLE

table: d

type: ref

possible_keys: PRIMARY

key: PRIMARY

key_len: 4

ref: const

rows: 4

Extra:

********************** 4. row **********************

id: 1

select_type: SIMPLE

table: p

type: eq_ref

possible_keys: PRIMARY,productLine

key: PRIMARY

key_len: 17

ref: classicmodels.d.productCode

rows: 1

Extra:

********************** 5. row **********************

id: 1

select_type: SIMPLE

table: l

type: eq_ref

possible_keys: PRIMARY

key: PRIMARY

key_len: 52

ref: classicmodels.p.productLine

rows: 1

Extra:

5 rows in set (0.00 sec)


После добавления индексов, число считанных записей упало до 1 × 1 × 4 × 1 × 1 = 4 Для каждой записи order_number = 10101 в таблице orderdetails – это значит, что MySQL смогла найти соответствующие записи во всех других таблицах с использованием индексов и не стала прибегать к полному сканированию таблицы.


В первом выводе вы можете что использован тип соединения – “const”, который является самым быстрым типом соединения для таблиц с более, чем одной записью. MySQL смогла использовать PRIMARY KEY как индекс. В поле “ref” отображается “const”, что есть ни что иное, как значение 10101, указанное в запросе после ключевого слова WHERE.


Смотрим на еще один запрос. В нем мы выбираем объединение двух таблиц, products и productvariants, каждая объединена с productline. productvariants, которая состоит из разных вариантов продуктов с полем productCode – ссылкой на их цены.


EXPLAIN SELECT * FROM (

SELECT p.productName, p.productCode, p.buyPrice, l.productLine, p.status, l.status AS lineStatus FROM

products p

INNER JOIN productlines l ON p.productLine = l.productLine

UNION

SELECT v.variantName AS productName, v.productCode, p.buyPrice, l.productLine, p.status, l.status AS lineStatus FROM productvariants v

INNER JOIN products p ON p.productCode = v.productCode

INNER JOIN productlines l ON p.productLine = l.productLine

) products

WHERE status = 'Active' AND lineStatus = 'Active' AND buyPrice BETWEEN 30 AND 50

********************** 1. row **********************

id: 1

select_type: PRIMARY

table: type: ALL

possible_keys: NULL

key: NULL

key_len: NULL

ref: NULL

rows: 219

Extra: Using where

********************** 2. row **********************

id: 2

select_type: DERIVED

table: p

type: ALL

possible_keys: NULL

key: NULL

key_len: NULL

ref: NULL

rows: 110

Extra:

********************** 3. row **********************

id: 2

select_type: DERIVED

table: l

type: eq_ref

possible_keys: PRIMARY

key: PRIMARY

key_len: 52

ref: classicmodels.p.productLine

rows: 1

Extra:

********************** 4. row **********************

id: 3

select_type: UNION

table: v

type: ALL

possible_keys: NULL

key: NULL

key_len: NULL

ref: NULL

rows: 109

Extra:

********************** 5. row **********************

id: 3

select_type: UNION

table: p

type: eq_ref

possible_keys: PRIMARY

key: PRIMARY

key_len: 17

ref: classicmodels.v.productCode

rows: 1

Extra:

********************** 6. row **********************

id: 3

select_type: UNION

table: l

type: eq_ref

possible_keys: PRIMARY

key: PRIMARY

key_len: 52

ref: classicmodels.p.productLine

rows: 1

Extra:

********************** 7. row **********************

id: NULL

select_type: UNION RESULT

table: <union2,3>

type: ALL

possible_keys: NULL

key: NULL

key_len: NULL

ref: NULL

rows: NULL

Extra:

7 rows in set (0.01 sec)


Вы можете заметить ряд проблем в этом запросе. Он сканирует все записи в products и productvarians. Т.к. в этих таблицах нет индексов для столбцов productLine и buyPrice, в полях possible_keys и key отображаются значения NULL. Статус таблиц products и productlines проверяется после UNION’а, поэтому перемещение их внутри UNION’а уменьшит число записей. Добавим индексы.


CREATE INDEX idx_buyPrice ON products(buyPrice);

CREATE INDEX idx_buyPrice ON productvariants(buyPrice);

CREATE INDEX idx_productCode ON productvariants(productCode);

CREATE INDEX idx_productLine ON products(productLine);


EXPLAIN SELECT * FROM (

SELECT p.productName, p.productCode, p.buyPrice, l.productLine, p.status, l.status as lineStatus FROM products p

INNER JOIN productlines AS l ON (p.productLine = l.productLine AND p.status = 'Active' AND l.status = 'Active')

WHERE buyPrice BETWEEN 30 AND 50

UNION

SELECT v.variantName AS productName, v.productCode, p.buyPrice, l.productLine, p.status, l.status FROM productvariants v

INNER JOIN products p ON (p.productCode = v.productCode AND p.status = 'Active')

INNER JOIN productlines l ON (p.productLine = l.productLine AND l.status = 'Active')

WHERE

v.buyPrice BETWEEN 30 AND 50

) product


********************** 1. row **********************

id: 1

select_type: PRIMARY

table: type: ALL

possible_keys: NULL

key: NULL

key_len: NULL

ref: NULL

rows: 12

Extra:

********************** 2. row **********************

id: 2

select_type: DERIVED

table: p

type: range

possible_keys: idx_buyPrice,idx_productLine

key: idx_buyPrice

key_len: 8

ref: NULL

rows: 23

Extra: Using where


********************** 3. row **********************

id: 2

select_type: DERIVED

table: l

type: eq_ref

possible_keys: PRIMARY

key: PRIMARY

key_len: 52

ref: classicmodels.p.productLine

rows: 1

Extra: Using where


********************** 4. row **********************

id: 3

select_type: UNION

table: v

type: range

possible_keys: idx_buyPrice,idx_productCode

key: idx_buyPrice

key_len: 9

ref: NULL

rows: 1

Extra: Using where


********************** 5. row **********************

id: 3

select_type: UNION

table: p

type: eq_ref

possible_keys: PRIMARY,idx_productLine

key: PRIMARY

key_len: 17

ref: classicmodels.v.productCode

rows: 1

Extra: Using where


********************** 6. row **********************

id: 3

select_type: UNION

table: l

type: eq_ref

possible_keys: PRIMARY

key: PRIMARY

key_len: 52

ref: classicmodels.p.productLine

rows: 1

Extra: Using where


********************** 7. row **********************

id: NULL

select_type: UNION RESULT

table: <union2,3>

type: ALL

possible_keys: NULL

key: NULL

key_len: NULL

ref: NULL

rows: NULL

Extra:

7 rows in set (0.01 sec)


Как вы видите, в результате количество сканированных строк уменьшилось с 2,625,810 (219 × 110 × 109) до 276 (12 × 23), что является отличным приобретением в производительности. Если вы выполните этот же запрос без предыдущих перестановок в запросе сразу после добавления индексов, вы не увидите такого уменьшения просканированных строк. MySQL не способна использовать индексы, когда в производном результате используется WHERE. После помещения этих условий внутри UNION становится возможных использование индексов. Это значит, что добавления индексов не всегда достаточно. MySQL не сможет их использовать до тех пор, пока вы не будете писать подходящие запросы. (http://ift.tt/1hZNmIZ – доп. информация).


Итог




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

This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий