понедельник, 2 февраля 2009 г.

Оптимизация запроса в MySQL

У сервера, любезно предоставленного мне для размещения сайта, были довольно средние характеристики. Поэтому было решено все страницы, за исключением feedback, делать статическими. Для генерации страниц был разработан скрипт на perl. После отладки скрипта оказалось, что на генерацию 24 отзывов вместе с индексными файлами уходит примерно 120 секунд. На мой взгляд, непростительно много. После анализа времени функцией, которое тратится на производство одной страницы, выяснилось, что построение ссылок на страны, курорты, отели, относящиеся к опубликованному отзыву занимает больше всего времени.
Но почему? Вот запрос, получающий эту информацию:
SELECT MIN(p.name) name, count(*) cnt, p.id
FROM props p
,article_props ap
,articles a
WHERE p.up_id = 171
AND p.tp = 'P'
AND p.id = ap.prop_id
AND ap.art_id = a.id
AND a.is_can_show = 'Y'
GROUP BY p.id
Для таблицы props (в дальнейшем p) есть индекс по полям up_id и tp, для таблицы article_props (в дальнейшем ap) — индекс по полю prop_id, ну а у таблицы articles (a) — первичный ключ по id. Но запрос сначала шел по таблице a (по индексу по полю is_can_show), затем по ap и по p. В таблице ap записей примерно в 5 раз меньше, чем в p, может быть по-этому анализатор выбирал сначала эту таблицу.
Знаток MySQL рекомендовал мне:
  1. переписываем с оракловского синтаксиса на общечеловеческий, расставляя таблицы в желаемом порядке
    SELECT MIN(p.name) name, count(*) cnt, p.id
    FROM props p
    INNER JOIN article_props ap ON p.id = ap.prop_id
    INNER JOIN articles a ON (ap.art_id = a.id AND a.is_can_show = 'Y')
    WHERE p.up_id = 171
    AND p.tp = 'P'
    GROUP BY p.id
  2. Применить хинт
    SELECT /*+ORDERED*/ ...
  3. А лучше переделать все на LEFT JOIN
    SELECT /*+ORDERED*/
    MIN(p.name) name, count(*) cnt, p.id
    FROM props p
    LEFT JOIN article_props ap ON p.id = ap.prop_id
    LEFT JOIN articles a ON (ap.art_id = a.id AND a.is_can_show = 'Y')
    WHERE p.up_id = 171
    AND p.tp = 'P'
    AND a.id IS NOT NULL
    GROUP BY p.id
И добавил: «вообще я INNER JOIN стараюсь не применять вообще никогда.
Потому что LEFT JOIN как раз диктует порядок построения запроса и не даёт оптимизатору умничать. ВСЕГДА ставьте LEFT JOIN. Почувствуйте себя хозяином положения».
И действительно, после этих манипуляций запрос «пошёл» по нужным индексам, и стал отрабатывать за 0.6 секунды, вместо 9.6.
Я создал все страницы, на что ушло около 15 минут, начал проверять что получилось, и заметил, что запрос-то ошибочный. При его выполнении выдаются все записи из таблицы p, даже те, для которых нет ссылок в ap. При замене первого LEFT JOIN article_props ap ON p.id = ap.prop_id на INNER JOIN и эта проблема была решена. Но все же, использование LEFT JOIN меня сильно коробило. Не красиво это.
И элегантное решение было найдено. Оказывается, в MySQL есть возможность указания индексов в хинтах. В результате запрос стал
SELECT MIN(p.name) name, count(*) cnt, p.id
FROM props p,
article_props ap,
articles a IGNORE INDEX(art_is_can_show_i)
WHERE p.up_id = 171
AND p.tp = 'P'
AND p.id = ap.prop_id
AND ap.art_id = a.id
AND a.is_can_show = 'Y'
GROUP BY p.id
24 отзыва вместе с индексными файлами созданы менее чем за 12 секунд (сравните со 120 секундами до оптимизации).

1 комментарий: