Esta é a segunda parte de um blog em série de duas partes para Maximizar a eficiência da consulta de banco de dados no MySQL. Você pode ler a parte um aqui.
Usando coluna única, composto, prefixo e índice de cobertura
As tabelas que recebem frequentemente alto tráfego devem ser indexadas corretamente. Não é apenas importante indexar sua tabela, mas você também precisa determinar e analisar quais são os tipos de consultas ou tipos de recuperação necessários para a tabela específica. É altamente recomendável que você analise que tipo de consultas ou recuperação de dados você precisa em uma tabela específica antes de decidir quais índices são necessários para a tabela. Vamos examinar esses tipos de índices e como você pode usá-los para maximizar o desempenho de sua consulta.
Índice de coluna única
A tabela InnoD pode conter no máximo 64 índices secundários. Um índice de coluna única (ou índice de coluna inteira) é um índice atribuído apenas a uma coluna específica. Criar um índice para uma coluna específica que contém valores distintos é um bom candidato. Um bom índice deve ter uma alta cardinalidade e estatísticas para que o otimizador possa escolher o plano de consulta correto. Para visualizar a distribuição dos índices, você pode verificar com a sintaxe SHOW INDEXES como abaixo:
root[test]#> SHOW INDEXES FROM users_account\G
*************************** 1. row ***************************
Table: users_account
Non_unique: 0
Key_name: PRIMARY
Seq_in_index: 1
Column_name: id
Collation: A
Cardinality: 131232
Sub_part: NULL
Packed: NULL
Null:
Index_type: BTREE
Comment:
Index_comment:
*************************** 2. row ***************************
Table: users_account
Non_unique: 1
Key_name: name
Seq_in_index: 1
Column_name: last_name
Collation: A
Cardinality: 8995
Sub_part: NULL
Packed: NULL
Null:
Index_type: BTREE
Comment:
Index_comment:
*************************** 3. row ***************************
Table: users_account
Non_unique: 1
Key_name: name
Seq_in_index: 2
Column_name: first_name
Collation: A
Cardinality: 131232
Sub_part: NULL
Packed: NULL
Null:
Index_type: BTREE
Comment:
Index_comment:
3 rows in set (0.00 sec)
Você também pode inspecionar com as tabelas information_schema.index_statistics ou mysql.innodb_index_stats.
Índices compostos (compostos) ou de várias partes
Um índice composto (comumente chamado de índice composto) é um índice de várias partes composto por várias colunas. O MySQL permite até 16 colunas limitadas para um índice composto específico. Exceder o limite retorna um erro como abaixo:
ERROR 1070 (42000): Too many key parts specified; max 16 parts allowed
Um índice composto fornece um impulso para suas consultas, mas exige que você tenha um entendimento puro de como está recuperando os dados. Por exemplo, uma tabela com um DDL de...
CREATE TABLE `user_account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`last_name` char(30) NOT NULL,
`first_name` char(30) NOT NULL,
`dob` date DEFAULT NULL,
`zip` varchar(10) DEFAULT NULL,
`city` varchar(100) DEFAULT NULL,
`state` varchar(100) DEFAULT NULL,
`country` varchar(50) NOT NULL,
`tel` varchar(16) DEFAULT NULL
PRIMARY KEY (`id`),
KEY `name` (`last_name`,`first_name`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
...que consiste no índice composto `name`. O índice composto melhora o desempenho da consulta uma vez que essas chaves são referenciadas como partes de chave usadas. Por exemplo, veja o seguinte:
root[test]#> explain format=json select * from users_account where last_name='Namuag' and first_name='Maximus'\G
*************************** 1. row ***************************
EXPLAIN: {
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "1.20"
},
"table": {
"table_name": "users_account",
"access_type": "ref",
"possible_keys": [
"name"
],
"key": "name",
"used_key_parts": [
"last_name",
"first_name"
],
"key_length": "60",
"ref": [
"const",
"const"
],
"rows_examined_per_scan": 1,
"rows_produced_per_join": 1,
"filtered": "100.00",
"cost_info": {
"read_cost": "1.00",
"eval_cost": "0.20",
"prefix_cost": "1.20",
"data_read_per_join": "352"
},
"used_columns": [
"id",
"last_name",
"first_name",
"dob",
"zip",
"city",
"state",
"country",
"tel"
]
}
}
}
1 row in set, 1 warning (0.00 sec
Os used_key_parts mostram que o plano de consulta selecionou perfeitamente nossas colunas desejadas cobertas em nosso índice composto.
A indexação composta também tem suas limitações. Certas condições na consulta não podem fazer com que todas as colunas façam parte da chave.
A documentação diz, "O otimizador tenta usar partes de chave adicionais para determinar o intervalo desde que o operador de comparação seja =, <=> ou IS NULL. Se o operador for> , <,>=, <=, !=, <>, BETWEEN ou LIKE, o otimizador o usa, mas não considera mais partes principais. Para a expressão a seguir, o otimizador usa =da primeira comparação. Ele também usa>=da segunda comparação, mas não considera outras partes principais e não usa a terceira comparação para construção de intervalo..." . Basicamente, isso significa que, independentemente de você ter um índice composto para duas colunas, um exemplo de consulta abaixo não abrange os dois campos:
root[test]#> explain format=json select * from users_account where last_name>='Zu' and first_name='Maximus'\G
*************************** 1. row ***************************
EXPLAIN: {
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "34.61"
},
"table": {
"table_name": "users_account",
"access_type": "range",
"possible_keys": [
"name"
],
"key": "name",
"used_key_parts": [
"last_name"
],
"key_length": "60",
"rows_examined_per_scan": 24,
"rows_produced_per_join": 2,
"filtered": "10.00",
"index_condition": "((`test`.`users_account`.`first_name` = 'Maximus') and (`test`.`users_account`.`last_name` >= 'Zu'))",
"cost_info": {
"read_cost": "34.13",
"eval_cost": "0.48",
"prefix_cost": "34.61",
"data_read_per_join": "844"
},
"used_columns": [
"id",
"last_name",
"first_name",
"dob",
"zip",
"city",
"state",
"country",
"tel"
]
}
}
}
1 row in set, 1 warning (0.00 sec)
Nesse caso (e se sua consulta for mais de intervalos em vez de tipos constantes ou de referência), evite usar índices compostos. Apenas desperdiça sua memória e buffer e aumenta a degradação do desempenho de suas consultas.
Índices de prefixo
Os índices de prefixo são índices que contêm colunas referenciadas como um índice, mas apenas assumem o comprimento inicial definido para essa coluna, e essa parte (ou dados de prefixo) é a única parte armazenada no buffer. Os índices de prefixo podem ajudar a diminuir os recursos do pool de buffers e também o espaço em disco, pois não precisa ocupar o comprimento total da coluna. O que isso significa? Vamos dar um exemplo. Vamos comparar o impacto entre o índice de comprimento total versus o índice de prefixo.
root[test]#> create index name on users_account(last_name, first_name);
Query OK, 0 rows affected (0.42 sec)
Records: 0 Duplicates: 0 Warnings: 0
root[test]#> \! du -hs /var/lib/mysql/test/users_account.*
12K /var/lib/mysql/test/users_account.frm
36M /var/lib/mysql/test/users_account.ibd
Criamos um índice composto completo que consome um total de 36MiB de espaço de tabela para a tabela users_account. Vamos eliminá-lo e, em seguida, adicionar um índice de prefixo.
root[test]#> drop index name on users_account;
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0
root[test]#> alter table users_account engine=innodb;
Query OK, 0 rows affected (0.63 sec)
Records: 0 Duplicates: 0 Warnings: 0
root[test]#> \! du -hs /var/lib/mysql/test/users_account.*
12K /var/lib/mysql/test/users_account.frm
24M /var/lib/mysql/test/users_account.ibd
root[test]#> create index name on users_account(last_name(5), first_name(5));
Query OK, 0 rows affected (0.42 sec)
Records: 0 Duplicates: 0 Warnings: 0
root[test]#> \! du -hs /var/lib/mysql/test/users_account.*
12K /var/lib/mysql/test/users_account.frm
28M /var/lib/mysql/test/users_account.ibd
Usando o índice de prefixo, ele suporta apenas 28MiB e isso é menos de 8MiB do que usando o índice de tamanho completo. É ótimo ouvir isso, mas não significa que seja eficiente e atenda ao que você precisa.
Se você decidir adicionar um índice de prefixo, você deve identificar primeiro que tipo de consulta para recuperação de dados você precisa. A criação de um índice de prefixo ajuda a utilizar mais eficiência com o pool de buffers e, portanto, ajuda no desempenho de sua consulta, mas você também precisa conhecer sua limitação. Por exemplo, vamos comparar o desempenho ao usar um índice completo e um índice de prefixo.
Vamos criar um índice completo usando um índice composto,
root[test]#> create index name on users_account(last_name, first_name);
Query OK, 0 rows affected (0.45 sec)
Records: 0 Duplicates: 0 Warnings: 0
root[test]#> EXPLAIN format=json select last_name from users_account where last_name='Namuag' and first_name='Maximus Aleksandre' \G
*************************** 1. row ***************************
EXPLAIN: {
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "1.61"
},
"table": {
"table_name": "users_account",
"access_type": "ref",
"possible_keys": [
"name"
],
"key": "name",
"used_key_parts": [
"last_name",
"first_name"
],
"key_length": "60",
"ref": [
"const",
"const"
],
"rows_examined_per_scan": 3,
"rows_produced_per_join": 3,
"filtered": "100.00",
"using_index": true,
"cost_info": {
"read_cost": "1.02",
"eval_cost": "0.60",
"prefix_cost": "1.62",
"data_read_per_join": "1K"
},
"used_columns": [
"last_name",
"first_name"
]
}
}
}
1 row in set, 1 warning (0.00 sec)
root[test]#> flush status;
Query OK, 0 rows affected (0.02 sec)
root[test]#> pager cat -> /dev/null; select last_name from users_account where last_name='Namuag' and first_name='Maximus Aleksandre' \G
PAGER set to 'cat -> /dev/null'
3 rows in set (0.00 sec)
root[test]#> nopager; show status like 'Handler_read%';
PAGER set to stdout
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Handler_read_first | 0 |
| Handler_read_key | 1 |
| Handler_read_last | 0 |
| Handler_read_next | 3 |
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 0 |
+-----------------------+-------+
7 rows in set (0.00 sec)
O resultado revela que está, de fato, usando um índice de cobertura, ou seja, "using_index":true e usa índices corretamente, ou seja, Handler_read_key é incrementado e faz uma varredura de índice conforme Handler_read_next é incrementado.
Agora, vamos tentar usar o índice de prefixo da mesma abordagem,
root[test]#> create index name on users_account(last_name(5), first_name(5));
Query OK, 0 rows affected (0.22 sec)
Records: 0 Duplicates: 0 Warnings: 0
root[test]#> EXPLAIN format=json select last_name from users_account where last_name='Namuag' and first_name='Maximus Aleksandre' \G
*************************** 1. row ***************************
EXPLAIN: {
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "3.60"
},
"table": {
"table_name": "users_account",
"access_type": "ref",
"possible_keys": [
"name"
],
"key": "name",
"used_key_parts": [
"last_name",
"first_name"
],
"key_length": "10",
"ref": [
"const",
"const"
],
"rows_examined_per_scan": 3,
"rows_produced_per_join": 3,
"filtered": "100.00",
"cost_info": {
"read_cost": "3.00",
"eval_cost": "0.60",
"prefix_cost": "3.60",
"data_read_per_join": "1K"
},
"used_columns": [
"last_name",
"first_name"
],
"attached_condition": "((`test`.`users_account`.`first_name` = 'Maximus Aleksandre') and (`test`.`users_account`.`last_name` = 'Namuag'))"
}
}
}
1 row in set, 1 warning (0.00 sec)
root[test]#> flush status;
Query OK, 0 rows affected (0.01 sec)
root[test]#> pager cat -> /dev/null; select last_name from users_account where last_name='Namuag' and first_name='Maximus Aleksandre' \G
PAGER set to 'cat -> /dev/null'
3 rows in set (0.00 sec)
root[test]#> nopager; show status like 'Handler_read%';
PAGER set to stdout
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Handler_read_first | 0 |
| Handler_read_key | 1 |
| Handler_read_last | 0 |
| Handler_read_next | 3 |
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 0 |
+-----------------------+-------+
7 rows in set (0.00 sec)
O MySQL revela que ele usa o índice corretamente, mas visivelmente, há uma sobrecarga de custo em comparação com um índice completo. Isso é óbvio e explicável, pois o índice do prefixo não cobre todo o comprimento dos valores do campo. O uso de um índice de prefixo não é uma substituição, nem uma alternativa, da indexação completa. Também pode criar resultados ruins ao usar o índice de prefixo de forma inadequada. Portanto, você precisa determinar que tipo de consulta e dados você precisa recuperar.
Cobertura de índices
A cobertura de índices não requer nenhuma sintaxe especial no MySQL. Um índice de cobertura no InnoDB refere-se ao caso em que todos os campos selecionados em uma consulta são cobertos por um índice. Ele não precisa fazer uma leitura sequencial no disco para ler os dados na tabela, mas apenas usar os dados no índice, agilizando significativamente a consulta. Por exemplo, nossa consulta anterior, ou seja,
select last_name from users_account where last_name='Namuag' and first_name='Maximus Aleksandre' \G
Como mencionado anteriormente, é um índice de cobertura. Quando você tiver tabelas muito bem planejadas ao armazenar seus dados e criar o índice corretamente, tente fazer com que suas consultas sejam projetadas para alavancar o índice de cobertura para que você se beneficie do resultado. Isso pode ajudá-lo a maximizar a eficiência de suas consultas e resultar em um ótimo desempenho.
Aproveite ferramentas que oferecem consultores ou monitoramento de desempenho de consultas
As organizações geralmente tendem a usar primeiro o github e encontrar software de código aberto que pode oferecer grandes benefícios. Para conselhos simples que ajudam a otimizar suas consultas, você pode aproveitar o Percona Toolkit. Para um DBA MySQL, o Percona Toolkit é como um canivete suíço.
Para operações, você precisa analisar como está usando seus índices, você pode usar pt-index-usage.
Pt-query-digest também está disponível e pode analisar consultas MySQL de logs, processlist e tcpdump. Na verdade, a ferramenta mais importante que você deve usar para analisar e inspecionar consultas ruins é o pt-query-digest. Use essa ferramenta para agregar consultas semelhantes e relatar aquelas que consomem mais tempo de execução.
Para arquivar registros antigos, você pode usar pt-archiver. Inspecionando seu banco de dados para índices duplicados, aproveite o pt-duplicate-key-checker. Você também pode aproveitar o pt-deadlock-logger. Embora os deadlocks não sejam a causa de uma consulta de baixo desempenho e ineficiente, mas de uma implementação ruim, ainda assim, isso afeta a ineficiência da consulta. Se você precisar de manutenção de tabela e precisar adicionar índices online sem afetar o tráfego do banco de dados que vai para uma tabela específica, poderá usar pt-online-schema-change. Como alternativa, você pode usar gh-ost, que também é muito útil para migrações de esquema.
Se você estiver procurando por recursos corporativos, agrupados com muitos recursos de desempenho e monitoramento de consultas, alarmes e alertas, painéis ou métricas que ajudam a otimizar suas consultas e consultores, o ClusterControl pode ser a ferramenta para tu. O ClusterControl oferece muitos recursos que mostram as principais consultas, consultas em execução e consultas atípicas. Confira este blog MySQL Query Performance Tuning, que o orienta como estar no mesmo nível para monitorar suas consultas com o ClusterControl.
Conclusão
Como você chegou à parte final do nosso blog de duas séries. Cobrimos aqui os fatores que causam a degradação das consultas e como resolvê-los para maximizar suas consultas ao banco de dados. Também compartilhamos algumas ferramentas que podem beneficiá-lo e ajudar a resolver seus problemas.