Песочница →
Mysql performance
Написание этой статьи навеяно вот этой трилогией: один, два, три. Захотелось добавить свои 0.02$, по использованию трюков и особенностей.
Чтоб мне не запутаться в мыслях, в первом своём посте, возмём примеры запросов из одного open-source продукта. Используемые «хитрости» работают также на PostgreSQL, Oracle, SQLite, DB2 и не являются MySQL направленными, хотя оптимизация в первую очередь нацелена на MySQL InnoDB:
Ситуация: есть авторизированный пользователь, пользователь принадлежит к группе пользователей. Есть хосты принадлежащие группе хостов.
Суть: вытащить хосты к которым пользователь имеет доступ. Доступ даётся группам пользователей на группы хостов.
Первое на что хочу обратить внимание это на условие EXISTS. Очень редко вижу использование этой структуры. EXISTS — это подзапрос, который проверят наличие строк в подзапросе. Такая конструкция запроса позволяет манипулировать используемыми в запросе индексами (как в основном запросе, так и в подзапросе) вне зависимости от общего запроса, к тому же, в случае успеха, подзапрос останавливается на первой найденной строке, удовлетворяющей запросу. Манипулировать ключами часто приходится когда в запросе используется сортировка. Поскольку MySQL не может использовать разные ключи для поиска и сортировки.
Если запрос происходит по одной таблице, то иногда можно использовать такие хитрости:
Например MySQL посчитал, что использовать в WHERE выгоднее индекс «events_1», это и понятно, ведь в WHERE задействованы два поля ключа, но он не учёл, что результат содержит 100к строк и их надо сортировать.
В данном случае, изменив в запросе критерий первого поля выбранного MySQL индекса:
MySQL не может использовать индексы на модифицированных значениях полей, в данном случае применив арифметическое присваивание мы заставляем использовать индекс events_2, который подходит как для выборки данных так и для сортировки, что видно из EXPLAIN.
Обращаю внимание, что MySQL не умеет сортировать по индексу, если используемые поля сортируются в разном порядке:
Так же, индекс не используется, если сортировка происходит по полям стоящим в другом порядке нежели в индексе. Вообще, MySQL хранит индексы как B-дерево, по-этому использовать поля стоящие в середине или конце индекса не получится.
Если невозможно использовать один индекс для выборки и сортировки, то случалось так, что MySQL не правильно делал оценку и выбирал индексы для сортировки. Это случается при прохождении некого количества записей в таблице, когда становится выгодней, чтоб MySQL использовать индекс для выборки, а не для сортировки или на оборот. Такие моменты предусмотреть сложно, и выявляются проверяя одни и те же запросы на разном количестве записей.
Вернёмся к первому запросу. На мой взгляд лучше сортировать уже средствами исполняемого скрипта, разумеется если выборка в пределах тысяч строк.
Во-первых, в этом случае MySQL не приходится делать полную выборку из таблицы, а останавливается по достижению LIMIT;
Во-вторых, редко когда действительно нужно одновременно выводить больше информации, если это только не отчёты;
В-третьих, страницы с большим количеством информации будут медленными, даже если это просто выпадающий список с 1000+ вариантами, это уже не юзабельно;
В-четвёртых, в MySQL нет натуральной сортировки;
Но в реальных примерах это редко возможно, т.к. приходится давать пользователям возможность сортировки, да и без указания сортировки MySQL не гарантирует одинаковую выборку. Как вариант можно в индекс ставить первым полем, то по которому чаще происходит сортировка, тем самым MySQL будет использовать один индекс и для поиска и для сортировки.
COUNT, многие делают второй запрос для постраничного вывода, но тот же гугле, хоть и говорит, что найдено миллионы+ совпадений, реально вам выдаст, примерно, первую тысячу и всё. Причём на последней странице сообщит, что на самом деле найдено меньше. Так вот, выбрав 1001 строку мы просто говорим пользователю, что найдено 1000+ совпадений и нет нужды выбирать на данном этапе больше. Кода пользователь запросит больше, тогда и будем выбирать шаг за шагом по 1ой страничке. Проверяя на 1 строку больше чем надо.
Индексы. По мере запросов к таблицам, индексы этих таблиц кешируются в памяти и там остаются пока не закончится память и тогда их «просят» выйти. Так вот, если у вас гигабайты информации, то индексы будут занимать ± 40% процентов места, зависит от количества тех самых индексов. К примеру у Вас скромный сервер с 16гб оперативки выделенными под MySQL. При запросе в таблицу с индексом весом в 10гб+, вся память выделенная под MySQL будет высвобождена и заполнена этим индексом и все предыдущие закешированные индексы скинутся в небытие. Таким образом, делая один тяжёлый запрос вы можете убить весь перформанс сервера. Что делать? Вариантов много, но не сказал бы, что они просты и сходятся к хранению больших >10 миллионов+ таблиц на отдельных хранителях, например BigTable, NoSQL или даже NoSQL для MySQL и т.д.
На этом, пока всё. Буду рад выслушать ваше решения и советы по выше сказанному.
Чтоб мне не запутаться в мыслях, в первом своём посте, возмём примеры запросов из одного open-source продукта. Используемые «хитрости» работают также на PostgreSQL, Oracle, SQLite, DB2 и не являются MySQL направленными, хотя оптимизация в первую очередь нацелена на MySQL InnoDB:
SELECT h.hostid,hg.groupid,h.name
FROM hosts h,hosts_groups hg
WHERE (hg.groupid IN ('4'))
AND hg.hostid=h.hostid
AND hg.groupid BETWEEN 000000000000000 AND 099999999999999
AND h.status IN (0,1)
AND EXISTS (
SELECT hh.hostid
FROM hosts hh, hosts_groups hgg, rights r, users_groups ug
WHERE hh.hostid=h.hostid
AND hh.hostid=hgg.hostid
AND r.id=hgg.groupid
AND r.groupid=ug.usrgrpid
AND ug.userid=3
AND r.permission>=3
AND NOT EXISTS(
SELECT hggg.groupid
FROM hosts_groups hggg, rights rr, users_groups gg
WHERE hggg.hostid=hgg.hostid
AND rr.id=hggg.groupid
AND rr.groupid=gg.usrgrpid
AND gg.userid=3
AND rr.permission<3
))
ORDER BY h.name ASC
LIMIT 1001
* This source code was highlighted with Source Code Highlighter.
Ситуация: есть авторизированный пользователь, пользователь принадлежит к группе пользователей. Есть хосты принадлежащие группе хостов.
Суть: вытащить хосты к которым пользователь имеет доступ. Доступ даётся группам пользователей на группы хостов.
Первое на что хочу обратить внимание это на условие EXISTS. Очень редко вижу использование этой структуры. EXISTS — это подзапрос, который проверят наличие строк в подзапросе. Такая конструкция запроса позволяет манипулировать используемыми в запросе индексами (как в основном запросе, так и в подзапросе) вне зависимости от общего запроса, к тому же, в случае успеха, подзапрос останавливается на первой найденной строке, удовлетворяющей запросу. Манипулировать ключами часто приходится когда в запросе используется сортировка. Поскольку MySQL не может использовать разные ключи для поиска и сортировки.
Если запрос происходит по одной таблице, то иногда можно использовать такие хитрости:
CREATE TABLE events (
eventid bigint unsigned NOT NULL,
source integer DEFAULT '0' NOT NULL,
object integer DEFAULT '0' NOT NULL,
objectid bigint unsigned DEFAULT '0' NOT NULL,
clock integer DEFAULT '0' NOT NULL,
value integer DEFAULT '0' NOT NULL,
acknowledged integer DEFAULT '0' NOT NULL,
ns integer DEFAULT '0' NOT NULL,
value_changed integer DEFAULT '0' NOT NULL,
PRIMARY KEY (eventid)
) ENGINE=InnoDB;
CREATE INDEX events_1 ON events (object,objectid,eventid);
CREATE INDEX events_2 ON events (clock,eventid);
* This source code was highlighted with Source Code Highlighter.
EXPLAIN
SELECT eventid,clock,value
FROM events
WHERE objectid=17131
AND object=0
AND clock>=1325635327
ORDER BY clock DESC, eventid DESC;
* This source code was highlighted with Source Code Highlighter.
+----+-------------+--------+------+-------------------+----------+---------+-------------+------+-----------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+------+-------------------+----------+---------+-------------+------+-----------------------------+
| 1 | SIMPLE | events | ref | events_1,events_2 | events_1 | 12 | const,const | 113056 | Using where; Using filesort |
+----+-------------+--------+------+-------------------+----------+---------+-------------+------+-----------------------------+
Например MySQL посчитал, что использовать в WHERE выгоднее индекс «events_1», это и понятно, ведь в WHERE задействованы два поля ключа, но он не учёл, что результат содержит 100к строк и их надо сортировать.
В данном случае, изменив в запросе критерий первого поля выбранного MySQL индекса:
EXPLAIN
SELECT eventid,clock,value
FROM events
WHERE objectid=17131
AND object+0=0
AND clock>=1325635327
ORDER BY clock DESC, eventid DESC;
* This source code was highlighted with Source Code Highlighter.
+----+-------------+--------+-------+---------------+----------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+-------+---------------+----------+---------+------+------+-------------+
| 1 | SIMPLE | events | range | events_2 | events_2 | 4 | NULL | 113056 | Using where |
+----+-------------+--------+-------+---------------+----------+---------+------+------+-------------+
MySQL не может использовать индексы на модифицированных значениях полей, в данном случае применив арифметическое присваивание мы заставляем использовать индекс events_2, который подходит как для выборки данных так и для сортировки, что видно из EXPLAIN.
Обращаю внимание, что MySQL не умеет сортировать по индексу, если используемые поля сортируются в разном порядке:
EXPLAIN
SELECT eventid,clock,value
FROM events
WHERE objectid=17131
AND object+0=0
AND clock>=1325635327
ORDER BY clock ASC, eventid DESC;
* This source code was highlighted with Source Code Highlighter.
+----+-------------+--------+-------+---------------+----------+---------+------+------+-----------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+-------+---------------+----------+---------+------+------+-----------------------------+
| 1 | SIMPLE | events | range | events_2 | events_2 | 4 | NULL | 113056 | Using where; Using filesort |
+----+-------------+--------+-------+---------------+----------+---------+------+------+-----------------------------+
Так же, индекс не используется, если сортировка происходит по полям стоящим в другом порядке нежели в индексе. Вообще, MySQL хранит индексы как B-дерево, по-этому использовать поля стоящие в середине или конце индекса не получится.
Если невозможно использовать один индекс для выборки и сортировки, то случалось так, что MySQL не правильно делал оценку и выбирал индексы для сортировки. Это случается при прохождении некого количества записей в таблице, когда становится выгодней, чтоб MySQL использовать индекс для выборки, а не для сортировки или на оборот. Такие моменты предусмотреть сложно, и выявляются проверяя одни и те же запросы на разном количестве записей.
Вернёмся к первому запросу. На мой взгляд лучше сортировать уже средствами исполняемого скрипта, разумеется если выборка в пределах тысяч строк.
Во-первых, в этом случае MySQL не приходится делать полную выборку из таблицы, а останавливается по достижению LIMIT;
Во-вторых, редко когда действительно нужно одновременно выводить больше информации, если это только не отчёты;
В-третьих, страницы с большим количеством информации будут медленными, даже если это просто выпадающий список с 1000+ вариантами, это уже не юзабельно;
В-четвёртых, в MySQL нет натуральной сортировки;
Но в реальных примерах это редко возможно, т.к. приходится давать пользователям возможность сортировки, да и без указания сортировки MySQL не гарантирует одинаковую выборку. Как вариант можно в индекс ставить первым полем, то по которому чаще происходит сортировка, тем самым MySQL будет использовать один индекс и для поиска и для сортировки.
COUNT, многие делают второй запрос для постраничного вывода, но тот же гугле, хоть и говорит, что найдено миллионы+ совпадений, реально вам выдаст, примерно, первую тысячу и всё. Причём на последней странице сообщит, что на самом деле найдено меньше. Так вот, выбрав 1001 строку мы просто говорим пользователю, что найдено 1000+ совпадений и нет нужды выбирать на данном этапе больше. Кода пользователь запросит больше, тогда и будем выбирать шаг за шагом по 1ой страничке. Проверяя на 1 строку больше чем надо.
Индексы. По мере запросов к таблицам, индексы этих таблиц кешируются в памяти и там остаются пока не закончится память и тогда их «просят» выйти. Так вот, если у вас гигабайты информации, то индексы будут занимать ± 40% процентов места, зависит от количества тех самых индексов. К примеру у Вас скромный сервер с 16гб оперативки выделенными под MySQL. При запросе в таблицу с индексом весом в 10гб+, вся память выделенная под MySQL будет высвобождена и заполнена этим индексом и все предыдущие закешированные индексы скинутся в небытие. Таким образом, делая один тяжёлый запрос вы можете убить весь перформанс сервера. Что делать? Вариантов много, но не сказал бы, что они просты и сходятся к хранению больших >10 миллионов+ таблиц на отдельных хранителях, например BigTable, NoSQL или даже NoSQL для MySQL и т.д.
На этом, пока всё. Буду рад выслушать ваше решения и советы по выше сказанному.
06.01.2012 16:49+0400