0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MySQLのインデックスマージとORDER BY LIMIT最適化を実測で確認した

0
Posted at

はじめに

「MySQLは単一テーブルに1つのインデックスしか使えない」は実は正確ではありません。条件によっては2つのインデックスを合成して使うインデックスマージが発生します。

また ORDER BY + LIMIT の組み合わせでは、複合インデックスを使うことで Using filesort(追加ソート)を消せます。

今回は両方を実際に EXPLAIN で確認しました。


環境構築

Docker で MySQL 8.0 を起動

docker run -d \
  --name mysql-practice \
  -e MYSQL_ROOT_PASSWORD=root \
  -p 3306:3306 \
  mysql:8.0

world データベースをインポート

curl -L https://downloads.mysql.com/docs/world-db.tar.gz -o world-db.tar.gz
tar xzf world-db.tar.gz
docker exec -i mysql-practice mysql -uroot -proot < world-db/world.sql

インデックスマージ最適化

インデックスを追加して確認

USE world;
ALTER TABLE city ADD INDEX idx_name (Name);
ALTER TABLE city ADD INDEX idx_population (Population);

EXPLAIN SELECT * FROM city
WHERE Name = 'Tokyo' OR Population > 5000000;
+----+-------+-------------+-------------+-------------------------+-------------------------+----------+------+----------+--------------------------------------------------------+
| id | table | type        | possible_keys | key                   | key_len | ref  | rows | filtered | Extra                                                  |
+----+-------+-------------+-------------+-------------------------+-------------------------+----------+------+----------+--------------------------------------------------------+
|  1 | city  | index_merge | idx_population,idx_name | idx_name,idx_population | 140,4   | NULL |   25 |   100.00 | Using sort_union(idx_name,idx_population); Using where |
+----+-------+-------------+-------------+-------------------------+-------------------------+----------+------+----------+--------------------------------------------------------+

読み解き

カラム 意味
type index_merge 2つのインデックスを合成して使っている
key idx_name,idx_population 両方のインデックスが使われた
Extra Using sort_union(...) 2つのインデックス結果をソートしてマージした

OR 条件なので2つの条件を別々にインデックスで検索してから結果を合成しています。

① idx_name で Name='Tokyo' にマッチする行を取得
② idx_population で Population>5000000 にマッチする行を取得
③ 両方の結果をソートして重複を除いてマージ

インデックスマージの種類

種類 発生条件 内容
sort_union OR条件 各インデックスの結果をソートしてマージ
union OR条件(range同士) ソートなしでマージ
intersect AND条件 各インデックスの結果の積集合

注意点

インデックスマージより複合インデックスの方が効率的なことが多いです。また UPDATE / DELETE でインデックスマージが起きると両方のインデックスレコードがロックされてロック範囲が広がります。

-- 後片付け
ALTER TABLE city DROP KEY idx_name;
ALTER TABLE city DROP KEY idx_population;

ORDER BY LIMIT最適化

インデックスなしの状態で確認

EXPLAIN SELECT * FROM country
WHERE Continent = 'Asia'
ORDER BY Population DESC
LIMIT 10;
+----+-------+------+---------------+---------------+---------+-------+------+----------+---------------------------------------+
| id | table | type | possible_keys | key           | key_len | ref   | rows | filtered | Extra                                 |
+----+-------+------+---------------+---------------+---------+-------+------+----------+---------------------------------------+
|  1 | country | ref | idx_continent | idx_continent | 1       | const |   51 |   100.00 | Using index condition; Using filesort |
+----+-------+------+---------------+---------------+---------+-------+------+----------+---------------------------------------+

Using filesort が出ています。Continent のインデックスで絞り込んだあと、Population での追加ソートが発生しています。

単独インデックスでは解決できない

ALTER TABLE country ADD INDEX idx_population (Population);

EXPLAIN SELECT * FROM country
WHERE Continent = 'Asia'
ORDER BY Population DESC
LIMIT 10;
-- 結果は変わらず Using filesort のまま

WHEREORDER BY が別々のカラムにあるため、単独インデックスでは解決できません。Continent で絞り込んでから Population でソートするという2ステップが必要です。

複合インデックスで解決する

ALTER TABLE country DROP KEY idx_population;
ALTER TABLE country ADD INDEX idx_continent_population (Continent, Population);

EXPLAIN SELECT * FROM country
WHERE Continent = 'Asia'
ORDER BY Population DESC
LIMIT 10;
+----+-------+------+----------------------------------------+--------------------------+---------+-------+------+----------+----------------------------------+
| id | table | type | possible_keys                          | key                      | key_len | ref   | rows | filtered | Extra                            |
+----+-------+------+----------------------------------------+--------------------------+---------+-------+------+----------+----------------------------------+
|  1 | country | ref | idx_continent,idx_continent_population | idx_continent_population | 1       | const |   51 |   100.00 | Using where; Backward index scan |
+----+-------+------+----------------------------------------+--------------------------+---------+-------+------+----------+----------------------------------+

Using filesort が消えました。

Backward index scan とは

idx_continent_population(Continent, Population) の順で昇順にソートされています。今回は ORDER BY Population DESC(降順)なので、インデックスを逆方向にたどることでソートなしに降順の結果が得られます。これが Backward index scan です。

インデックスの並び:ASC(小 → 大)
ORDER BY DESC   →  逆方向にたどるだけでOK(ソート不要)

MySQL 8.0以降で導入された最適化です。

結果の比較

単独インデックス 複合インデックス
key idx_continent idx_continent_population
Extra Using filesort Backward index scan
ソート 追加ソートあり インデックスで解決

まとめ

インデックスマージ

  • OR 条件で複数インデックスが sort_union で合成されることがある(type: index_merge
  • ただし複合インデックスの方が効率的なことが多い
  • UPDATE / DELETE でインデックスマージが起きるとロック範囲が広がる

ORDER BY LIMIT最適化

  • WHERE + ORDER BY の両方を複合インデックスにまとめると Using filesort が消える
  • ORDER BY DESC の場合は Backward index scan でインデックスを逆方向にたどって解決
  • Railsで .order(population: :desc).limit(10) のようなクエリに直結する

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?