Por que o MySQL ignora o índice, mesmo em vigor para esta ordem?

14

Eu corro um EXPLAIN:

mysql> explain select last_name from employees order by last_name;
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
| id | select_type | table     | type | possible_keys | key  | key_len | ref  | rows  | Extra          |
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
|  1 | SIMPLE      | employees | ALL  | NULL          | NULL | NULL    | NULL | 10031 | Using filesort |
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
1 row in set (0.00 sec)  

Os índices na minha tabela:

mysql> show index from employees;  
+-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+  
| Table     | Non_unique | Key_name      | Seq_in_index | Column_name   | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |  
+-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+  
| employees |          0 | PRIMARY       |            1 | subsidiary_id | A         |           6 |     NULL | NULL   |      | BTREE      |         |               |  
| employees |          0 | PRIMARY       |            2 | employee_id   | A         |       10031 |     NULL | NULL   |      | BTREE      |         |               |  
| employees |          1 | idx_last_name |            1 | last_name     | A         |       10031 |      700 | NULL   |      | BTREE      |         |               |  
| employees |          1 | date_of_birth |            1 | date_of_birth | A         |       10031 |     NULL | NULL   | YES  | BTREE      |         |               |  
| employees |          1 | date_of_birth |            2 | subsidiary_id | A         |       10031 |     NULL | NULL   |      | BTREE      |         |               |  
+-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+  
5 rows in set (0.02 sec)  

Há um índice em last_name, mas o otimizador não o utiliza.
Então eu faço:

mysql> explain select last_name from employees force index(idx_last_name) order by last_name;  
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
| id | select_type | table     | type | possible_keys | key  | key_len | ref  | rows  | Extra          |  
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
|  1 | SIMPLE      | employees | ALL  | NULL          | NULL | NULL    | NULL | 10031 | Using filesort |  
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
1 row in set (0.00 sec)  

Mas ainda assim o índice não é usado! O que eu estou fazendo errado aqui?
Isso tem a ver com o fato de que o índice é NON_UNIQUE? Entre o last_name éVARCHAR(1000)

Atualização solicitada por @RolandoMySQLDBA

mysql> SELECT COUNT(DISTINCT last_name) DistinctCount FROM employees;  
+---------------+  
| DistinctCount |  
+---------------+  
|         10000 |  
+---------------+  
1 row in set (0.05 sec)  


mysql> SELECT COUNT(1) FROM (SELECT COUNT(1) Count500,last_name FROM employees GROUP BY last_name HAVING COUNT(1) > 500) A;  
+----------+  
| COUNT(1) |  
+----------+  
|        0 |  
+----------+  
1 row in set (0.15 sec)  
Cratylus
fonte
Por favor, execute estas duas consultas: 1) SELECT COUNT(DISTINCT last_name) DistinctCount FROM employees;2) SELECT COUNT(1) FROM (SELECT COUNT(1) Count500,last_name FROM employees GROUP BY last_name HAVING COUNT(1) > 500) A;. Qual é o resultado de cada contagem?
RolandoMySQLDBA
@RolandoMySQLDBA: Atualizei o OP com as informações solicitadas.
Cratylus
Mais duas consultas, por favor: 1) SELECT COUNT(1) FullTableCount FROM employees;e 2) SELECT * FROM (SELECT COUNT(1) Count500,last_name FROM employees GROUP BY last_name HAVING COUNT(1) > 500) A LIMIT 10;.
RolandoMySQLDBA
Não importa, eu vejo a explicação com o que eu preciso.
RolandoMySQLDBA
2
@Cratylus você aceitou uma resposta errada, você deve aceitar o correto resposta de Michael-sqlbot
miracle173

Respostas:

6

PROBLEMA # 1

Olhe para a consulta

select last_name from employees order by last_name;

Não vejo uma cláusula WHERE significativa, nem o MySQL Query Optimizer. Não há incentivo para usar um índice.

PROBLEMA # 2

Olhe para a consulta

select last_name from employees force index(idx_last_name) order by last_name; 

Você forneceu um índice, mas o Query Opitmizer assumiu. Eu já vi esse comportamento antes ( como forçar um JOIN a usar um índice específico no MySQL? )

Por que isso deveria acontecer?

Sem uma WHEREcláusula, o Query Optimizer diz o seguinte para si mesmo:

  • Esta é uma tabela do InnoDB
  • É uma coluna indexada
  • O índice possui o row_id do gen_clust_index (também conhecido como Índice de Cluster)
  • Por que eu deveria olhar para o índice quando
    • não há WHEREcláusula?
    • Eu sempre teria que voltar para a mesa?
  • Como todas as linhas em uma tabela do InnoDB residem nos mesmos blocos de 16K que o gen_clust_index, farei uma varredura completa da tabela.

O Query Optimizer escolheu o caminho de menor resistência.

Você ficará um pouco chocado, mas aqui está: Você sabia que o Query Optimizer manipulará o MyISAM de maneira bastante diferente?

Você provavelmente está dizendo HUH ???? COMO ????

O MyISAM armazena os dados em um .MYDarquivo e todos os índices no .MYIarquivo.

A mesma consulta produzirá um plano EXPLAIN diferente porque o índice reside em um arquivo diferente dos dados. Por quê ? Aqui está o porquê:

  • Os dados necessários ( last_namecoluna) já estão ordenados no.MYI
  • Na pior das hipóteses, você terá uma verificação completa do índice
  • Você acessará apenas a coluna last_namedo índice
  • Você não precisa filtrar indesejados
  • Você não acionará a criação do arquivo temporário para classificação

Como pode ter tanta certeza disso? Eu testei essa teoria de trabalho sobre como o uso de um armazenamento diferente irá gerar um plano EXPLAIN diferente (às vezes um melhor): Um índice deve cobrir todas as colunas selecionadas para ser usado no ORDER BY?

RolandoMySQLDBA
fonte
1
-1 @Rolando esta resposta não é menos precisa que a resposta correta de Michael-sqlbot, mas está errada, por exemplo, o manual diz: "O MySQL usa índices para estas operações: (...) Para classificar ou agrupar uma tabela se a classificação ou o agrupamento é feito no prefixo mais à esquerda de um índice utilizável (...) ". Além disso, algumas das outras declarações de sua postagem são discutíveis. Eu recomendo que você exclua esta resposta ou refaça-a.
miracle173
Esta resposta não está correta. Um índice ainda pode ser usado, mesmo se não houver cláusula WHERE, se evitar a classificação.
oysteing
19

Na verdade, o problema aqui é que isso parece um índice de prefixo. Não vejo a definição da tabela na pergunta, mas sub_part= 700? Você não indexou a coluna inteira, portanto, o índice não pode ser usado para classificação e também não é útil como índice de cobertura. Só poderia ser usado para encontrar as linhas que "podem" corresponder a WHEREe a camada do servidor (acima do mecanismo de armazenamento) precisaria filtrar ainda mais as linhas correspondentes. Você realmente precisa de 1000 caracteres para um sobrenome?


atualização para ilustrar: Eu tenho uma tabela de teste de tabela com um pouco mais de 500 linhas, cada uma com o nome de domínio de um site em uma coluna domain_name VARCHAR(254) NOT NULLe sem índices.

mysql> alter table keydemo add key(domain_name);
Query OK, 0 rows affected (0.17 sec)
Records: 0  Duplicates: 0  Warnings: 0

Com a coluna completa indexada, a consulta usa o índice:

mysql> explain select domain_name from keydemo order by domain_name;
+----+-------------+---------+-------+---------------+-------------+---------+------+------+-------------+
| id | select_type | table   | type  | possible_keys | key         | key_len | ref  | rows | Extra       |
+----+-------------+---------+-------+---------------+-------------+---------+------+------+-------------+
|  1 | SIMPLE      | keydemo | index | NULL          | domain_name | 764     | NULL |  541 | Using index |
+----+-------------+---------+-------+---------------+-------------+---------+------+------+-------------+
1 row in set (0.01 sec)

Então, agora, vou largar esse índice e apenas indexar os 200 primeiros caracteres de domain_name.

mysql> alter table keydemo drop key domain_name;
Query OK, 0 rows affected (0.11 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> alter table keydemo add key(domain_name(200));
Query OK, 0 rows affected (0.08 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> explain select domain_name from keydemo order by domain_name;
+----+-------------+---------+------+---------------+------+---------+------+------+----------------+
| id | select_type | table   | type | possible_keys | key  | key_len | ref  | rows | Extra          |
+----+-------------+---------+------+---------------+------+---------+------+------+----------------+
|  1 | SIMPLE      | keydemo | ALL  | NULL          | NULL | NULL    | NULL |  541 | Using filesort |
+----+-------------+---------+------+---------------+------+---------+------+------+----------------+
1 row in set (0.00 sec)

mysql>

Voila.

Observe também que o índice, com 200 caracteres, é maior que o valor mais longo da coluna ...

mysql> select max(length(domain_name)) from keydemo;
+--------------------------+
| max(length(domain_name)) |
+--------------------------+
|                       43 |
+--------------------------+
1 row in set (0.04 sec)

... mas isso não faz nenhuma diferença. Um índice declarado com um comprimento de prefixo só pode ser usado para pesquisas, não para classificação e não como um índice de cobertura, pois não contém o valor completo da coluna, por definição.

Além disso, as consultas acima foram executadas em uma tabela InnoDB, mas executá-las em uma tabela MyISAM produz resultados praticamente idênticos. A única diferença nesse caso é que a contagem de InnoDB para rowsestá um pouco desligada (541) enquanto o MyISAM mostra o número exato de linhas (563), o que é um comportamento normal, pois os dois mecanismos de armazenamento manipulam mergulhos de índice de maneira muito diferente.

Eu ainda afirmaria que a coluna last_name provavelmente é maior que o necessário, mas ainda é possível indexar a coluna inteira, se você estiver usando o InnoDB e executando o MySQL 5.5 ou 5.6:

Por padrão, uma chave de índice para um índice de coluna única pode ter até 767 bytes. O mesmo limite de comprimento se aplica a qualquer prefixo da chave de índice. Consulte a Seção 13.1.13, “ CREATE INDEXSintaxe”. Por exemplo, você pode atingir esse limite com um índice de prefixo de coluna com mais de 255 caracteres em uma TEXTou VARCHARcoluna, assumindo um UTF-8conjunto de caracteres e o máximo de 3 bytes para cada caractere. Quando a innodb_large_prefixopção de configuração está ativada, esse limite de tamanho é aumentado para 3072 bytes, para InnoDBtabelas que usam os formatos de linha DYNAMICe COMPRESSED.

- http://dev.mysql.com/doc/refman/5.5/en/innodb-restrictions.html

Michael - sqlbot
fonte
Ponto de vista interessante. A coluna é varchar(1000), mas isto é para além do máximo permitido para o índice que é ~ 750
Cratylus
8
Essa resposta deve ser a aceita.
precisa saber é o seguinte
1
@ypercube Esta resposta é mais precisa que a minha. +1 no seu comentário e +1 nesta resposta. Que isso deva ser aceito no meu.
RolandoMySQLDBA
1
@ Timo, essa é uma pergunta interessante ... que eu sugeriria postar como uma nova pergunta, aqui, talvez com um link para esta resposta, para o contexto. Postar a saída completa de EXPLAIN SELECT ..., bem como SHOW CREATE TABLE ...e SELECT @@VERSION;desde alterações ao otimizador entre as versões podem ser relevantes.
Michael - sqlbot
1
Até agora, posso relatar que (pelo menos para 5,7) um índice de prefixo não ajuda na indexação nula, como pedi no meu comentário acima.
Timo
2

Eu respondi porque um comentário não suporta a formatação e o RolandoMySQL DBA falou sobre gen_clust_index e innodb. E isso é muito importante em uma tabela baseada em innodb. Isso vai além do conhecimento normal do DBA, porque você precisa poder analisar o código C.

SEMPRE SEMPRE crie uma CHAVE PRIMÁRIA ou ÚNICA se estiver usando o Innodb. Se você não usar, o innodb usará seu próprio ROW_ID gerado, o que pode causar mais mal do que bem.

Vou tentar explicar fácil, porque a prova é baseada no código C.

/**********************************************************************//**
Returns a new row id.
@return the new id */
UNIV_INLINE
row_id_t
dict_sys_get_new_row_id(void)
/*=========================*/
{
    row_id_t    id;

    mutex_enter(&(dict_sys->mutex));

    id = dict_sys->row_id;

    if (0 == (id % DICT_HDR_ROW_ID_WRITE_MARGIN)) {
          dict_hdr_flush_row_id();
    }

    dict_sys->row_id++;
    mutex_exit(&(dict_sys->mutex));
    return(id);
}

Primeiro problema

mutex_enter (& (dict_sys-> mutex));

Essa linha garante que apenas um encadeamento possa acessar dict_sys-> mutex ao mesmo tempo. E se o valor já tiver sido mutexado ... sim, um encadeamento tem que esperar para que você obtenha algo como um recurso aleatório agradável como bloqueio de encadeamento ou se você tiver mais tabelas sem sua própria PRIMARY KEY ou UNIQUE KEY, então você teria um recurso interessante com innodb ' table locking ' não é este o motivo pelo qual o MyISAM foi substituído pelo InnoDB, devido ao recurso interessante chamado bloqueio baseado em registro / linha.

Segundo problema

(0 == (id% DICT_HDR_ROW_ID_WRITE_MARGIN))

os cálculos do módulo (%) são lentos, não são bons se você estiver inserindo em lote porque precisa ser recalculado toda vez ... e porque DICT_HDR_ROW_ID_WRITE_MARGIN (valor 256) é uma potência de dois, isso pode ser feito muito mais rápido.

(0 == (id & (DICT_HDR_ROW_ID_WRITE_MARGIN - 1))))

Nota lateral: se o compilador C foi configurado para otimizar e é um bom otimizador, o otimizador C corrigirá o código "pesado" na versão mais leve

o lema da história sempre crie sua própria PRIMARY KEY ou verifique se você possui um índice ÚNICO ao criar uma tabela desde o início

Raymond Nijland
fonte
Adicione replicação baseada em linha e o fato de que os IDs de linha não são consistentes entre os servidores, e o argumento de Raymond sobre sempre criar uma chave primária é ainda mais importante.
Por favor, não sugira que UNIQUEseja suficiente - ele também precisa incluir apenas colunas que não sejam NULL para que o índice exclusivo seja promovido ao PK.
Rick James
"cálculos do módulo (%) são lentos" - Mais importante é o percentual do tempo INSERTgasto em uma função nesta função. Eu suspeito que é insignificante. Contrastar o esforço para colunas pá em volta, fazer operações BTREE, incluindo um bloco-split ocasional, vários semáforos na buffer_pool, coisas mudança tampão, etc.
Rick James
@RickJames verdadeiros a sobrecarga pode ser o número mas muitos muito pequenos números pequenos também adicionar-se (ainda seria um micro optimization) .. Além do primeiro problema é a maioria de problema algum
Raymond Nijland