ORDER BY에 사용하려면 인덱스가 선택된 모든 열을 포함해야합니까?


15

결국 SO 가 BY를 사용하지 않는 이유는 무엇입니까?

상황은 3 개의 열과 10k 개의 행으로 구성된 MySQL의 간단한 InnoDB 테이블과 관련이 있습니다. 열 중 하나 인 정수가 색인되었고 OP는 해당 열에서 정렬 된 전체 테이블을 검색하려고했습니다.

SELECT * FROM person ORDER BY age

그는 EXPLAIN이 쿼리가 filesort(인덱스가 아닌) 로 해결되었음을 보여주는 출력을 첨부 하고 그 이유를 물었습니다.

에도 불구하고 힌트 FORCE INDEX FOR ORDER BY (age) 사용되는 인덱스의 원인 , 누군가가 대답 하는 것이 (다른 사람의 의견 / upvotes 지원과) 인덱스 만 선택한 열이 인덱스 모두 읽을 때 정렬을 위해 사용된다 (즉, 일반적으로 표시되는 바와 같이 Using index에서 Extra열 의 EXPLAIN출력). 나중에 인덱스를 탐색 한 다음 테이블에서 열을 가져 오면 임의 I / O가 발생하여 MySQL보다 a가 더 비싸다는 설명이 나왔습니다 filesort.

이것은 ORDER BY최적화 에 대한 매뉴얼 장을 마주하는 것으로 보입니다 ORDER BY. 인덱스 를 만족시키는 것이 추가 정렬을 수행하는 것보다 낫다 는 강한 인상을 전달할 뿐만 아니라 filesort퀵 정렬과 병합 정렬의 조합 이므로 하한이 있어야합니다. 인덱스를 순서대로 살펴보면서 테이블을 찾아야하는데 이는 완벽 해야합니다. ) 또한 "최적화"라고 언급하면서 다음과 같이 언급합니다.Ω(nlog n)O(n)

다음 쿼리는 색인을 사용하여 ORDER BY부품 을 해결합니다 .

SELECT * FROM t1
  ORDER BY key_part1,key_part2,... ;

내 독서에 따르면,이 상황에서는 정확히 그렇습니다 (그러나 명시 적 힌트없이 색인이 사용되지 않았습니다).

내 질문은 :

  • MySQL이 인덱스를 사용하도록 선택하려면 선택한 모든 열을 인덱스해야합니까?

    • 그렇다면이 문서는 어디에 기록되어 있습니까?

    • 그렇지 않다면 여기서 무슨 일이 있었습니까?

답변:


14

MySQL이 인덱스를 사용하도록 선택하려면 선택한 모든 열을 인덱스해야합니까?

인덱스를 사용할 가치가 있는지 여부를 결정하는 요소가 있기 때문에 이것은로드 된 질문입니다.

요인 # 1

주어진 인덱스의 주요 인구는 무엇입니까? 다시 말해, 인덱스에 기록 된 모든 튜플의 카디널리티 (고유 한 수)는 무엇입니까?

요인 # 2

어떤 스토리지 엔진을 사용하고 있습니까? 인덱스에서 필요한 모든 열에 액세스 할 수 있습니까?

무엇 향후 계획 ???

간단한 예를 들어 보겠습니다. 두 값을 보유하는 테이블 (남성과 여성)

색인 사용 테스트를 통해 이러한 테이블을 작성하십시오.

USE test
DROP TABLE IF EXISTS mf;
CREATE TABLE mf
(
    id int not null auto_increment,
    gender char(1),
    primary key (id),
    key (gender)
) ENGINE=InnODB;
INSERT INTO mf (gender) VALUES
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
ANALYZE TABLE mf;
EXPLAIN SELECT gender FROM mf WHERE gender='F';
EXPLAIN SELECT gender FROM mf WHERE gender='M';
EXPLAIN SELECT id FROM mf WHERE gender='F';
EXPLAIN SELECT id FROM mf WHERE gender='M';

테스트 InnoDB

mysql> USE test
Database changed
mysql> DROP TABLE IF EXISTS mf;
Query OK, 0 rows affected (0.00 sec)

mysql> CREATE TABLE mf
    -> (
    ->     id int not null auto_increment,
    ->     gender char(1),
    ->     primary key (id),
    ->     key (gender)
    -> ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.07 sec)

mysql> INSERT INTO mf (gender) VALUES
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
Query OK, 40 rows affected (0.06 sec)
Records: 40  Duplicates: 0  Warnings: 0

mysql> ANALYZE TABLE mf;
+---------+---------+----------+----------+
| Table   | Op      | Msg_type | Msg_text |
+---------+---------+----------+----------+
| test.mf | analyze | status   | OK       |
+---------+---------+----------+----------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT gender FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |    3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT gender FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |   37 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT id FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |    3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT id FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |   37 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql>

테스트 MyISAM

mysql> USE test
Database changed
mysql> DROP TABLE IF EXISTS mf;
Query OK, 0 rows affected (0.00 sec)

mysql> CREATE TABLE mf
    -> (
    ->     id int not null auto_increment,
    ->     gender char(1),
    ->     primary key (id),
    ->     key (gender)
    -> ) ENGINE=MyISAM;
Query OK, 0 rows affected (0.05 sec)

mysql> INSERT INTO mf (gender) VALUES
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
    -> ('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
Query OK, 40 rows affected (0.00 sec)
Records: 40  Duplicates: 0  Warnings: 0

mysql> ANALYZE TABLE mf;
+---------+---------+----------+----------+
| Table   | Op      | Msg_type | Msg_text |
+---------+---------+----------+----------+
| test.mf | analyze | status   | OK       |
+---------+---------+----------+----------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT gender FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |    3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT gender FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |   36 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT id FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref   | rows | Extra       |
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
|  1 | SIMPLE      | mf    | ref  | gender        | gender | 2       | const |    3 | Using where |
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT id FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | mf    | ALL  | gender        | NULL | NULL    | NULL |   40 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)

mysql>

InnoDB 분석

데이터가 InnoDB로로드 될 때 네 가지 EXPLAIN계획 모두 gender인덱스를 사용했습니다 . 세 번째 및 네 번째 EXPLAIN계획 gender은 요청 된 데이터가이지만 색인을 사용했습니다 id. 왜? idPRIMARY KEY및에있는 모든 보조 인덱스에 PRIMARY KEY( gen_clust_index 를 통해) 참조 포인터 가 있기 때문 입니다 .

MyISAM 분석

데이터가 MyISAM으로로드 될 때 처음 세 EXPLAIN계획은 gender인덱스를 사용했습니다 . 네 번째 EXPLAIN계획에서 Query Optimizer는 인덱스를 전혀 사용하지 않기로 결정했습니다. 대신 전체 테이블 스캔을 선택했습니다. 왜?

DBMS와 상관없이 Query Optimizer는 매우 간단한 룰에서 작동합니다. 색인을 조회 수행에 사용할 후보로 선별하고 Query Optimizer가 총 조회 수의 5 % 이상을 조회해야한다고 계산합니다. 테이블의 행 :

  • 검색에 필요한 모든 열이 선택된 색인에 있으면 전체 색인 스캔이 수행됩니다.
  • 그렇지 않으면 전체 테이블 스캔

결론

적절한 커버링 인덱스가 없거나 주어진 튜플의 주요 모집단이 테이블의 5 % 이상인 경우 6 가지 일이 발생해야합니다.

  1. 쿼리를 프로파일 링해야한다는 것을 깨달으십시오
  2. 모든 찾기 WHERE, GROUP BY그 쿼리에서, 그리고 ORDER BY` 절을
  3. 이 순서대로 인덱스를 공식화
    • WHERE 정적 값을 가진 절 열
    • GROUP BY
    • ORDER BY
  4. 전체 테이블 스캔 피하기 (현명한 WHERE절이 없는 쿼리 )
  5. 잘못된 키 채우기를 피하십시오 (또는 적어도 해당 잘못된 키 채우기를 캐시).
  6. 테이블에 가장 적합한 MySQL 스토리지 엔진 ( InnoDB 또는 MyISAM ) 결정

나는 과거 에이 5 %의 경험 법칙에 대해 썼습니다.

업데이트 2012-11-14 13:05 EDT

나는 당신의 질문과 원래의 SO 포스트를 되돌아 보았습니다 . 그런 다음 Analysis for InnoDB이전에 언급 한 내용에 대해 생각했습니다 . person테이블 과 일치 합니다. 왜?

테이블 mfperson

  • 스토리지 엔진은 InnoDB
  • 기본 키 id
  • 테이블 액세스는 보조 인덱스에 의한 것입니다
  • 테이블이 MyISAM이라면, 완전히 다른 EXPLAIN계획을 보게 될 것입니다

이제 SO 질문의 쿼리를 살펴보십시오 select * from person order by age\G. WHERE절이 없으므로 명시 적으로 전체 테이블 스캔을 요구했습니다 . 테이블의 기본 정렬 순서는 idauto_increment 및 gen_clust_index (일명 Clustered Index)로 인해 (PRIMARY KEY)이며 내부 rowid로 정렬됩니다 . 인덱스로 주문할 때 InnoDB 보조 인덱스에는 각 인덱스 항목에 행 ID가 첨부되어 있습니다. 이렇게하면 매번 전체 행 액세스가 필요합니다.

ORDER BYInnoDB 인덱스 구성 방법에 대한 이러한 사실을 무시하면 InnoDB 테이블에서 설정하는 것은 다소 어려운 작업이 될 수 있습니다.

SO 쿼리로 돌아가서 명시 적으로 전체 테이블 스캔을 요구 했기 때문에 MySQL Query Optimizer는 IMHO가 올바른 일을했습니다 (적어도 저항이 가장 적은 경로를 선택했습니다). InnoDB 및 SO 쿼리의 경우 filesort각 보조 인덱스 항목에 대해 gen_clust_index를 통해 전체 인덱스 스캔 및 행 조회를 수행하는 것보다 전체 테이블 스캔을 수행 한 다음 일부를 수행하는 것이 훨씬 쉽습니다 .

EXPLAIN 계획을 무시하기 때문에 Index Hints 사용을 옹호하지 않습니다. 그럼에도 불구하고 InnoDB보다 데이터를 더 잘 알고 있다면, 특히 WHERE절이 없는 쿼리를 사용하여 인덱스 힌트를 사용해야 합니다.

업데이트 2012-11-14 14:21 EDT

책에 따르면 MySQL 내부 이해하기

여기에 이미지 설명을 입력하십시오

문단 7은 다음과 같이 말합니다.

데이터는 clustered index 라는 특수 구조에 저장 되는데, 이는 키 값으로 작동하는 기본 키와 데이터 부분의 실제 레코드 (포인터가 아닌)가있는 B- 트리입니다. 따라서 각 InnoDB 테이블에는 기본 키가 있어야합니다. 하나를 제공하지 않으면 일반적으로 사용자에게 표시되지 않는 특수 행 ID 열이 기본 키 역할을하도록 추가됩니다. 보조 키는 레코드를 식별하는 기본 키의 값을 저장합니다. B- 트리 코드는 innobase / btr / btr0btr.c에 있습니다.

이것이 내가 이전에 언급 한 이유 입니다. 각 보조 인덱스 항목에 대해 gen_clust_index를 통해 전체 인덱스 스캔 및 행 조회를 수행하는 것보다 전체 테이블 스캔을 수행 한 다음 일부 파일 정렬을 수행하는 것이 훨씬 쉽습니다 . InnoDB는 매번 이중 인덱스 조회를 수행 할 것 입니다. 그것은 잔인한 것처럼 들리지만 사실입니다. WHERE절의 부족을 다시 고려하십시오 . 이것 자체는 전체 테이블 스캔을 수행하기위한 MySQL Query Optimizer의 힌트입니다.


롤란도, 철저하고 자세한 답변을 주셔서 감사합니다. 그러나 인덱스 선택과 관련이없는 것으로 보입니다 FOR ORDER BY(이 질문의 특정 경우). 이 경우 스토리지 엔진이 문제였습니다 InnoDB(원래 SO 질문은 10k 행이 8 개의 항목에 상당히 균일하게 분포되어 있으며 카디널리티도 문제가되지 않아야 함을 나타냅니다). 슬프게도, 이것이 이것이 질문에 대답한다고 생각하지 않습니다.
eggyal

첫 번째 부분도 첫 번째 본능 이었기 때문에 이것은 흥미 롭습니다 (좋은 카디널리티가 없었기 때문에 mysql은 전체 스캔을 선택했습니다). 그러나 내가 읽을수록 그 규칙은 최적화에 의한 순서에 적용되지 않는 것 같습니다. innodb 클러스터형 인덱스의 기본 키로 주문 하시겠습니까? 이 게시물 은 기본 키가 끝에 추가되었음을 나타내므로 정렬이 인덱스의 명시 적 열에 여전히 있지 않습니까? 한마디로, 나는 여전히 혼란에 빠져 있습니다!
데릭 다우니

1
filesort선택은 하나의 단순한 이유로 쿼리 최적화 프로그램에 따라 결정되었다 : 그것은 당신이 가지고있는 데이터의 예지 없다. 인덱스 힌트를 사용하기로 선택한 경우 (문제 # 2를 기준으로) 만족스러운 러닝 타임을 제공한다면 반드시 가십시오. 내가 제공 한 답변은 MySQL Query Optimizer가 얼마나 기 질적이며 행동 강의를 제안 할 수 있는지를 보여주는 학문적 운동이었습니다.
RolandoMySQLDBA

1
나는이 게시물과 다른 게시물을 읽고 다시 읽었으며, 우리가 모든 것을 선택하기 때문에 (커버링 인덱스가 아닌) 기본 키의 innodb 순서와 관련이 있다는 것에 동의 할 수 있습니다. ORDER BY 최적화 문서 페이지에서이 InnoDB 고유의 이상한 점에 대한 언급이 없다는 것이 놀랍습니다. 어쨌든, +1 Rolando로
데릭 다우니

1
@eggyal 이번 주에 작성되었습니다. 동일한 EXPLAIN 계획에 유의하십시오. 데이터 세트가 메모리에 맞지 않으면 전체 스캔 시간이 더 오래 걸립니다.
데릭 다우니

0

SO에 대한 다른 질문에 대한 Denis의 대답 에서 (허가 됨) 적응

모든 레코드 (또는 거의 모든)가 쿼리에 의해 페치되므로 일반적으로 인덱스가없는 것이 좋습니다. 그 이유는 실제로 인덱스를 읽는 데 비용이 들기 때문입니다.

전체 테이블을 진행할 때 순차적으로 테이블을 읽고 행을 메모리에 정렬하는 것이 가장 저렴한 계획 일 수 있습니다. 몇 개의 행만 필요하고 대부분 where 절과 일치하면 가장 작은 인덱스로 이동하면 트릭을 수행합니다.

이유를 이해하려면 관련된 디스크 I / O를 상상하십시오.

인덱스없이 전체 테이블을 원한다고 가정하십시오. 이렇게하려면 테이블 끝에 도달 할 때까지 data_page1, data_page2, data_page3 등을 읽고 관련된 여러 디스크 페이지를 순서대로 방문하십시오. 그런 다음 정렬하고 돌아옵니다.

인덱스가없는 상위 5 개 행을 원하면 상위 5 개 행을 힙 정렬하는 동안 이전과 같이 전체 테이블을 순차적으로 읽습니다. 분명히, 그것은 소수의 행에 대한 많은 읽기와 정렬입니다.

이제 전체 테이블에 인덱스가 필요하다고 가정하십시오. 이를 위해 index_page1, index_page2 등을 순차적으로 읽습니다. 그러면 data_page3, data_page1, data_page3, data_page2 등을 완전히 임의의 순서로 정렬합니다 (정렬 된 행이 데이터에 표시되는 순서). 관련된 IO는 전체 엉망을 순차적으로 읽고 잡기 가방을 메모리에 정렬하는 것이 더 저렴합니다.

반대로 인덱스 된 테이블의 상위 5 개 행을 원한다면 인덱스를 사용하는 것이 올바른 전략이됩니다. 최악의 시나리오에서는 5 개의 데이터 페이지를 메모리에로드하고 계속 진행합니다.

훌륭한 SQL 쿼리 플래너 인 btw는 데이터의 조각화 방식에 따라 인덱스 사용 여부를 결정합니다. 순서대로 행을 가져 오는 것이 테이블에서 앞뒤로 확대되는 것을 의미하는 경우, 좋은 계획자는 인덱스를 사용할 가치가 없다고 결정할 수 있습니다. 반대로, 동일한 인덱스를 사용하여 테이블을 클러스터링하면 행이 순서대로 정렬되어 사용 가능성이 높아집니다.

다른 테이블과 같은 쿼리에 가입한다면 다음, 다른 테이블은 작은 인덱스를 사용할 수있는 조항이 그것을 결정할 수 플래너로 태그 된 모든 행 ID를 가져 실제로 더 나은 예에 어디 매우 선택적있다 foo, 해시 테이블을 조인하고 메모리에서 힙 정렬하십시오.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.