SQLinfo.ru - Все о MySQL

Улучшаем производительность запросов, меняя IN-подзапросы на производные таблицы

Дата: 31.12.2016

Данный материал является переводом статьи Øystein Grøvlen.

В MySQL 5.6 добавили преобразование IN-подзапросов в semi-join. Появились новые стратегии для выполнения IN-подзапросов; исчезла необходимость выполнять подзапрос для каждой строки внешнего запроса. Это резко повысило производительность многих таких запросов. Однако, преобразование в semi-join применимо не для всех типов запросов. Например, если подзапрос содержит GROUP BY, то его нельзя преобразовать в semi-join. (Полный список ограничений представлен в документации).

К счастью, в MySQL 5.6 также добавили материализацию подзапросов, которая может быть использована для независимых подзапросов, которые нельзя преобразовать в semi-join. (Независимые - это такие подзапросы, которые не содержат ссылок на таблицы внешнего запроса.) В одной из ранее написанных статей, я показал как для запроса №18 из теста DBT-3 изменяется время выполнения с длительности более месяца до нескольких секунд за счет использования стратегии материализации подзапроса. В данной статье я покажу как можно ещё улучшить производительность этого запроса, переписав его.

В исходном варианте запрос №18 выглядит следующим образом:

SELECT c_name, c_custkey, o_orderkey, o_orderdate, o_totalprice, SUM(l_quantity)  
 FROM customer, orders, lineitem  
 WHERE o_orderkey IN (  
    SELECT l_orderkey  
    FROM lineitem  
    GROUP BY l_orderkey  
    HAVING SUM(l_quantity) > 313  
  )  
  AND c_custkey = o_custkey  
  AND o_orderkey = l_orderkey  
 GROUP BY c_name, c_custkey, o_orderkey, o_orderdate, o_totalprice  
 ORDER BY o_totalprice DESC, o_orderdate  
 LIMIT 100;  

Этот запрос называется Large Volume Customer Query, т.к. он находит крупные заказы (более 313 товаров) и возвращает первые 100 заказов после сортировки по итоговой цене и дате. В MySQL 5.5 и ранее подзапрос выполнялся для каждой строки внешнего запроса. В MySQL 5.6 стратегия материализации сделала возможным однократное выполнение подзапроса. Это можно увидеть в плане выполнения, который показывает EXPLAIN для этого запроса (часть колонок опущена для удобства восприятия):

id  select_type     table   type    key     rows    Extra
1   PRIMARY     orders      ALL     NULL    1500000     Using where; Using temporary; Using filesort
1   PRIMARY     customer    eq_ref  PRIMARY     1   NULL
1   PRIMARY     lineitem    ref     i_l_orderkey_quantity   4   Using index
2   SUBQUERY    lineitem    index   i_l_orderkey_quantity   6001215     Using index

Тип выборки SUBQUERY означает, что запрос выполняется однократно. (В противном случае тип выборки был бы DEPENDENT SUBQUERY.) Вот диаграмма, которую показывает Visual EXPLAIN для этого случая:

Основной компонент обеих стратегий (и преобразования в semi-join, и материализации) - это исключение дубликатов. В отличии от операции JOIN, результат которой содержит все комбинации строк, соответствующих условию соединения, выражения с предикатом IN возвращают только по одной строке независимо от количества совпадений в подзапросе. При трансформации в semi-join существует несколько способов исключение дубликатов, при материализации только один - с помощью временной таблицы.

Внимательно посмотрев на запрос № 18, мы можем понять, что подзапрос никогда не вернет дубликатов. Выбирается только одно поле `l_orderkey` и по этому полю проводится группировка, т.е. все строки, возвращаемые подзапросом, будут уникальными. Это означает, что мы можем заменить IN-выражение на join и получить эквивалентный запрос:

SELECT c_name, c_custkey, o_orderkey, o_orderdate, o_totalprice, SUM(lineitem.l_quantity)
FROM customer, orders, lineitem,
     ( SELECT l_orderkey
       FROM lineitem
       GROUP BY l_orderkey
       HAVING SUM(l_quantity) > 313
     ) l2
WHERE o_orderkey = l2.l_orderkey
  AND c_custkey = o_custkey
  AND o_orderkey = lineitem.l_orderkey
GROUP BY c_name, c_custkey, o_orderkey, o_orderdate, o_totalprice
ORDER BY o_totalprice DESC, o_orderdate
LIMIT 100;

Теперь наш подзапрос стал производной таблицей (так называется подзапрос в части FROM). План выполнения для переписанного запроса выглядит следующим образом:

Обратите внимание, подзапрос материализуется как и раньше, но теперь происходит полное сканирование временной таблицы, а не таблицы orders как было в исходном запросе. Это означает, что вместо перебора всех заказов MySQL перебирает только большие заказы (более 313 товаров). Учитывая, что большинство заказов содержит меньше товаров, такое преобразование должно дать улучшение производительности. Действительно, в MySQL 5.7.14 время выполнения переписанного запроса сократилось где-то на треть:

Замена IN-подзапросов на производные таблицы дает возможность оптимизатору выбрать оптимальный порядок соединения таблиц. Эта же идея лежит в основе semi-join преобразования, которое, начиная с MySQL 5.6, производится автоматически, и именно поэтому наше ручное преобразование улучшило план выполнения. Надеюсь, когда-нибудь в будущем оптимизатор MySQL научится делать такие преобразования автоматически. А пока, следите за возможностью вручную переписать IN-подзапросы в тех случаях, когда к ним не применимо преобразование в semi-join!

Дата публикации: 31.12.2016

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

Статьи :
 Установка и настройка MySQL
 Коды ошибок в MySQL
 Программирование в MySQL
>Оптимизация производительности
 Кодировка символов в MySQL
 Хранение данных в MySQL
 MySQL Cluster
См. также:
 Оптимизация производительности MySQL
 Услуги по оптимизации MySQL