Por que o mysql está usando o índice errado para solicitar por consulta?

9

Aqui está minha tabela com ~ 10.000.000 linhas de dados

CREATE TABLE `votes` (
  `subject_name` varchar(32) COLLATE utf8_unicode_ci NOT NULL,
  `subject_id` int(11) NOT NULL,
  `voter_id` int(11) NOT NULL,
  `rate` int(11) NOT NULL,
  `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`subject_name`,`subject_id`,`voter_id`),
  KEY `IDX_518B7ACFEBB4B8AD` (`voter_id`),
  KEY `subject_timestamp` (`subject_name`,`subject_id`,`updated_at`),
  KEY `voter_timestamp` (`voter_id`,`updated_at`),
  CONSTRAINT `FK_518B7ACFEBB4B8AD` FOREIGN KEY (`voter_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

Aqui estão os índices de cardinalidades

insira a descrição da imagem aqui

Então, quando eu faço essa consulta:

SELECT SQL_NO_CACHE * FROM votes WHERE 
    voter_id = 1099 AND 
    rate = 1 AND 
    subject_name = 'medium'
ORDER BY updated_at DESC
LIMIT 20 OFFSET 100;

Eu estava esperando que ele usa índice, voter_timestamp mas o mysql escolhe usar isso:

explain select SQL_NO_CACHE * from votes  where subject_name = 'medium' and voter_id = 1001 and rate = 1 order by updated_at desc limit 20 offset 100;`

type:
    index_merge
possible_keys: 
    PRIMARY,IDX_518B7ACFEBB4B8AD,subject_timestamp,voter_timestamp
key:
    IDX_518B7ACFEBB4B8AD,PRIMARY
key_len:
    102,98
ref:
    NULL
rows:
    9255
filtered:
    10.00
Extra:
    Using intersect(IDX_518B7ACFEBB4B8AD,PRIMARY); Using where; Using filesort

E eu tenho 200-400ms de tempo de consulta.

Se eu forçá-lo a usar o índice correto, como:

SELECT SQL_NO_CACHE * FROM votes USE INDEX (voter_timestamp) WHERE 
    voter_id = 1099 AND 
    rate = 1 AND 
    subject_name = 'medium'
ORDER BY updated_at DESC
LIMIT 20 OFFSET 100;

Mysql pode retornar os resultados em 1-2ms

e aqui está a explicação:

type:
    ref
possible_keys:
    voter_timestamp
key:
    voter_timestamp
key_len:
    4
ref:
    const
rows:
    18714
filtered:
    1.00
Extra:
    Using where

Então, por que o mysql não escolheu o voter_timestampíndice para minha consulta original?

O que eu tinha tentado é analyze table votes, optimize table votes, queda nesse índice e adicioná-lo novamente, mas mysql ainda usa o índice errado. não entendo bem qual é o problema.

Fénix
fonte
11
@ ypercubeᵀᴹ Não acho necessário indexar todas as colunas na condição where, como você vê se forço a usar o índice (voter_id, updated_at), ele pode usá-lo e ser muito eficiente. Se eu remover a subject_name = "medium"parte ele também pode escolher o indicador direito, sem necessidade de índicerate
Phoenix
Ainda assim, o índice de 4 colunas será mais eficiente que o 2 (voter_id, updated_at). Outro índice seria (voter_id, subject_name, updated_at)ou (subject_name, voter_id, updated_at)(sem a taxa).
precisa saber é o seguinte
11
E sim, você está - em algum ponto - certo. Você não precisa do índice de 4 colunas. É apenas o melhor índice possível para esta consulta. As duas colunas (que você acha "certas") talvez sejam válidas para os dados e a distribuição que você possui atualmente. Com uma distribuição diferente, pode ser horrível. Exemplo: suponha que 99% das linhas tenham taxa> 1 e apenas 1% tenham taxa = 1. Você acha que usar o índice de duas colunas seria eficiente?
precisa saber é o seguinte
Teria que percorrer grande parte do índice e fazer milhares de pesquisas na tabela, apenas para encontrar essa taxa> 1 e rejeitar as linhas, até encontrar 120 que se encaixam nos critérios que não podem ser julgados pelo índice ( subject_name='medium' and rate=1)
ypercube # 4/16
ypercube, Phoenix - O MySQL não acessa o LIMITmesmo, a ORDER BYmenos que o índice satisfaça primeiro toda a filtragem. Ou seja, sem as quatro colunas completas, ele coletará todas as linhas relevantes, classificará todas e depois selecionará a LIMIT. Com o índice de 4 colunas, a consulta pode evitar a classificação e parar depois de ler apenas as LIMITlinhas.
Rick James

Respostas:

5

O MySQL está usando um modelo de custo relativamente simples (mais simples que outros RDBMS) para planejar consultas em que a filtragem do conjunto de dados tem uma prioridade bastante alta. Na sua primeira consulta com o índice de mesclagem, estima-se que a digitalização ~ 9000 linhas será necessária, enquanto a segunda com a dica do índice exigirá 18000. Minha aposta seria que isso pesa no cálculo o suficiente para mover a escala em direção à mesclagem . Você pode confirmar isso (ou encontrar outros motivos) ativando optimizer_trace, executando sua consulta e avaliando os resultados.

set global optimizer_trace='enabled=on';

-- run your query 

SELECT SQL_NO_CACHE * FROM votes WHERE 
    voter_id = 1099 AND 
    rate = 1 AND 
    subject_name = 'medium'
ORDER BY updated_at DESC
LIMIT 20 OFFSET 100;

select * from information_schema.`OPTIMIZER_TRACE`;

Uma observação sobre index_merge: na maioria dos casos, você verá que é bastante caro. Embora seja muito útil para cenários do tipo OLAP, pode não ser muito adequado para OLTP, porque a operação pode levar um tempo significativo da sua consulta e, como você pode ver às vezes, o plano de execução abaixo do ideal é realmente mais rápido.

Felizmente, o MySQL fornece opções para otimizador, para que você possa personalizá-lo como desejar.

Para toda a opção, você pode executar:

show global variables like 'optimizer_switch';

Para alterar um, você não precisa copiar e colar toda a string. Funciona como dict.update()em python.

 set global optimizer_switch='index_merge=off';

Se possível, eu também daria uma olhada na estrutura da sua tabela e melhoraria. Ter uma chave primária de ~ 100 bytes com muitas chaves secundárias não é realmente recomendado.

Você tem quatro chaves secundárias e algumas delas são supérfluas, por exemplo, (voter_id)índice é um subconjunto de(voter_id, updated_at)

Károly Nagy
fonte
A "intersecção de mesclagem de índice" raramente é usada pelo MySQL. Em talvez todos os casos, é significativamente melhor ter um índice com mais colunas. A "união de mesclagem de índice" às ​​vezes é útil; transformar OR- UNIONse geralmente é tão bom ou melhor.
Rick James
5

Para essa consulta, você precisa deste índice:

INDEX(voter_id, rate, subject_name, updated_at)

O updated_atdeve ser o último; os outros três podem estar em qualquer ordem. (os índices de três colunas do ypercube não são muito úteis, pois não finalizam as WHEREcolunas antes de atingi- ORDER BYlas.)

À medida que você adiciona esse índice, você provavelmente pode se livrar de todas as outras chaves secundárias:

KEY IDX_518B7ACFEBB4B8AD( voter_id), - O FK pode usar a minha chave de índice subject_timestamp( subject_name, subject_id, updated_at), - CHAVE principalmente redundante voter_timestamp( voter_id, updated_at), - pode ter sido sua tentativa

Com o índice de 4 colunas, você tem a chance de otimizar a "paginação" e evitar OFFSET. Veja este blog.

Em outro tópico ... Quando eu vejo X_namee X_id, presumo que a "normalização" está acontecendo. Eu esperaria ver essas duas colunas em uma tabela, praticamente sem mais nada. Eu não esperaria ver os dois em alguma outra tabela.

(voter_id, updated_at)não passará, voter_idpois não terminou a filtragem (the WHERE). Então, como um outro índice é menor, ele é escolhido. O meu possui 3 colunas para cuidar da filtragem e, em seguida, a coluna para ORDER BY.

Rick James
fonte