MySQL은 JOIN, WHERE 및 ORDER와 함께 인덱스를 사용하지 않습니다.
-
22-07-2019 - |
문제
다음과 같이 간단한 태그-레코드 구조와 유사한 두 개의 테이블이 있습니다(실제로는 훨씬 더 복잡하지만 이것이 문제의 본질입니다).
tag (A.a) | recordId (A.b)
1 | 1
2 | 1
2 | 2
3 | 2
....
그리고
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 seconds
(즉시), 반환 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'
다른 팁
select A.a, A.b, B.c from A join B on A.b = B.b where a = 44 order by 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 조건은 결과 테이블 열을 기반으로 합니다.
- 결과 테이블 열과 쿼리된 테이블에 별칭을 지정하여 둘 중 하나를 사용할 때 더 명확하게 하고 서버에서도 더 명확하게 만들 수 있기를 바랍니다.원래 쿼리의 두 위치에서 열을 참조하는 것을 무시했습니다.
귀하의 실제 데이터가 더 복잡하다는 것을 알고 있지만 문제가 그렇게 간단한 수준에 있기 때문에 귀하가 간단한 버전의 쿼리를 제공했다고 가정합니다.