MySQLはJOIN、WHERE、ORDERでインデックスを使用していません
-
22-07-2019 - |
質問
次のように、単純なタグレコード構造に似た2つのテーブルがあります(実際にはもっと複雑ですが、これが問題の本質です):
tag (A.a) | recordId (A.b)
1 | 1
2 | 1
2 | 2
3 | 2
....
and
recordId (B.b) | recordData (B.c)
1 | 123
2 | 666
3 | 1246
問題は、特定のタグで順序付けられたレコードを取得することです。それを行う明白な方法は、単純な結合と(PK)(A.a、A.b)、(A.b)、(PK)(B.b)、(B.b、B.c)のインデックスを使用することです:
select A.a, A.b, B.c from A join B on A.b = B.b where a = 44 order by c;
しかし、これはファイルソートの不快な結果をもたらします:
+----+-------------+-------+------+---------------+---------+---------+-----------+------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+---------+---------+-----------+------+----------------------------------------------+
| 1 | SIMPLE | A | ref | PRIMARY,b | PRIMARY | 4 | const | 94 | Using index; Using temporary; Using filesort |
| 1 | SIMPLE | B | ref | PRIMARY,b | b | 4 | booli.A.b | 1 | Using index |
+----+-------------+-------+------+---------------+---------+---------+-----------+------+----------------------------------------------+
巨大で非常に冗長な「マテリアライズドビュー」の使用かなりまともなパフォーマンスを得ることができますが、これはビジネスロジックの複雑化を犠牲にしますが、特にAテーブルとBテーブルは既にMV:sであるため(また、他のクエリに必要であり、同じクエリを作成するため) UNIONを使用)。
create temporary table C engine=innodb as (select A.a, A.b, B.c from A join B on A.b = B.b);
explain select a, b, c from C where a = 44 order by c;
状況をさらに複雑にしているのは、範囲フィルターなどのBテーブルに条件があることです。
select A.a, A.b, B.c from A join B on A.b = B.b where a = 44 AND B.c > 678 order by c;
しかし、ファイルソートの問題が解決した場合、これを処理できると確信しています。
上記のコードブロック3の単純な結合が並べ替えにインデックスを使用しない理由を知っていますか?新しいMVを作成せずに何らかの方法で問題を回避できるかどうか
以下は、テストに使用している完全なSQLリストです。
DROP TABLE IF EXISTS A;
DROP TABLE IF EXISTS B;
DROP TABLE IF EXISTS C;
CREATE TEMPORARY TABLE A (a INT NOT NULL, b INT NOT NULL, PRIMARY KEY(a, b), INDEX idx_A_b (b)) ENGINE=INNODB;
CREATE TEMPORARY TABLE B (b INT NOT NULL, c INT NOT NULL, d VARCHAR(5000) NOT NULL DEFAULT '', PRIMARY KEY(b), INDEX idx_B_c (c), INDEX idx_B_b (b, c)) ENGINE=INNODB;
DELIMITER $
CREATE PROCEDURE prc_filler(cnt INT)
BEGIN
DECLARE _cnt INT;
SET _cnt = 1;
WHILE _cnt <= cnt DO
INSERT IGNORE INTO A SELECT RAND()*100, RAND()*10000;
INSERT IGNORE INTO B SELECT RAND()*10000, RAND()*1000, '';
SET _cnt = _cnt + 1;
END WHILE;
END
$
DELIMITER ;
START TRANSACTION;
CALL prc_filler(100000);
COMMIT;
DROP PROCEDURE prc_filler;
CREATE TEMPORARY TABLE C ENGINE=INNODB AS (SELECT A.a, A.b, B.c FROM A JOIN B ON A.b = B.b);
ALTER TABLE C ADD (PRIMARY KEY(a, b), INDEX idx_C_a_c (a, c));
EXPLAIN EXTENDED SELECT A.a, A.b, B.c FROM A JOIN B ON A.b = B.b WHERE A.a = 44;
EXPLAIN EXTENDED SELECT A.a, A.b, B.c FROM A JOIN B ON A.b = B.b WHERE 1 ORDER BY B.c;
EXPLAIN EXTENDED SELECT A.a, A.b, B.c FROM A JOIN B ON A.b = B.b where A.a = 44 ORDER BY B.c;
EXPLAIN EXTENDED SELECT a, b, c FROM C WHERE a = 44 ORDER BY c;
-- Added after Quassnois comments
EXPLAIN EXTENDED SELECT A.a, A.b, B.c FROM B FORCE INDEX (idx_B_c) JOIN A ON A.b = B.b WHERE A.a = 44 ORDER BY B.c;
EXPLAIN EXTENDED SELECT A.a, A.b, B.c FROM A JOIN B ON A.b = B.b WHERE A.a = 44 ORDER BY B.c LIMIT 10;
EXPLAIN EXTENDED SELECT A.a, A.b, B.c FROM B FORCE INDEX (idx_B_c) JOIN A ON A.b = B.b WHERE A.a = 44 ORDER BY B.c LIMIT 10;
解決
スクリプトを使用してこのクエリを再現しようとすると:
SELECT A.a, A.b, B.c
FROM A
JOIN B
ON A.b = B.b
WHERE a = 44
ORDER BY
c
、 0.0043秒
(瞬時)に完了し、 930
行を返し、この計画を生成します:
1, 'SIMPLE', 'A', 'ref', 'PRIMARY', 'PRIMARY', '4', 'const', 1610, 'Using index; Using temporary; Using filesort'
1, 'SIMPLE', 'B', 'eq_ref', 'PRIMARY', 'PRIMARY', '4', 'test.A.b', 1, ''
このようなクエリには非常に効率的です。
このようなクエリでは、フィルタリングとソートの両方に単一のインデックスを使用できません。
詳細な説明については、私のブログのこの記事を参照してください。
クエリがほとんどのレコードを返さないと予想される場合は、フィルタリングに A
のインデックスを使用し、filesortを使用してソートする必要があります(上記のクエリと同様)。
多数のレコード(および LIMIT
)が返されると予想される場合は、ソートにインデックスを使用してからフィルタリングする必要があります:
CREATE INDEX ix_a_b ON a (b);
CREATE INDEX ix_b_c ON b (c)
SELECT *
FROM B FORCE INDEX (ix_b_c)
JOIN A
ON A.b = B.b
ORDER BY
b.c
LIMIT 10;
1, 'SIMPLE', 'B', 'index', '', 'ix_b_c', '4', '', 2, 'Using index'
1, 'SIMPLE', 'A', 'ref', 'ix_a_b', 'ix_a_b', '4', 'test.B.b', 4, 'Using index'
他のヒント
AからA.a、A.b、B.cを選択し、A.b = B.bのBに参加します。ここで、a = 44はcによる順序です。
列のエイリアスを作成すると、役立ちますか?例:
SELECT
T1.a AS colA,
T2.b AS colB,
T2.c AS colC
FROM A AS T1
JOIN B AS T2
ON (T1.b = T2.b)
WHERE
T1.a = 44
ORDER BY colC;
行った変更は次のとおりです。
- 結合条件を括弧で囲んでいます
- 結合条件と条件がテーブル列に基づいている場所
- ORDER BY条件は、結果のテーブル列に基づいています
- 結果テーブルの列とクエリされたテーブルのエイリアスを作成して、(うまくいけば)どちらかを使用しているときにわかりやすくします(サーバーにとってもわかりやすくします。元の2つの場所で列を参照することを怠ります)クエリ)。
実際のデータはより複雑ですが、問題はその単純なレベルにあるため、単純なバージョンのクエリを提供したと思います。