Dit is het tweede deel van een blog in twee delen over het maximaliseren van de efficiëntie van databasequery's in MySQL. Je kunt deel één hier lezen.
Enkelkoloms, samengestelde, prefix en dekkende index gebruiken
Tabellen die vaak veel verkeer ontvangen, moeten correct worden geïndexeerd. Het is niet alleen belangrijk om uw tabel te indexeren, maar u moet ook bepalen en analyseren welke typen query's of typen opvragingen u nodig hebt voor de specifieke tabel. Het wordt sterk aanbevolen dat u analyseert welk type query's of het ophalen van gegevens u nodig heeft voor een specifieke tabel, voordat u beslist welke indexen vereist zijn voor de tabel. Laten we eens kijken naar dit soort indexen en hoe u ze kunt gebruiken om uw zoekopdrachtprestaties te maximaliseren.
Index met één kolom
InnoD-tabel kan maximaal 64 secundaire indexen bevatten. Een index met één kolom (of index met een volledige kolom) is een index die alleen aan een bepaalde kolom is toegewezen. Het maken van een index voor een bepaalde kolom die verschillende waarden bevat, is een goede kandidaat. Een goede index moet een hoge kardinaliteit en statistieken hebben, zodat de optimizer het juiste queryplan kan kiezen. Om de distributie van indexen te bekijken, kunt u de syntaxis SHOW INDEXES controleren, zoals hieronder:
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)
U kunt ook inspecteren met tabellen information_schema.index_statistics of mysql.innodb_index_stats.
Samengestelde (samengestelde) of meerdelige indexen
Een samengestelde index (gewoonlijk een samengestelde index genoemd) is een meerdelige index die uit meerdere kolommen bestaat. MySQL staat maximaal 16 kolommen toe die zijn begrensd voor een specifieke samengestelde index. Overschrijding van de limiet geeft een fout zoals hieronder:
ERROR 1070 (42000): Too many key parts specified; max 16 parts allowed
Een samengestelde index geeft een boost aan uw zoekopdrachten, maar het vereist dat u een zuiver begrip heeft van hoe u de gegevens ophaalt. Bijvoorbeeld een tabel met een DDL van...
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
...die bestaat uit de samengestelde index `naam`. De samengestelde index verbetert de queryprestaties zodra deze sleutels worden gebruikt als gebruikte sleutelonderdelen. Zie bijvoorbeeld het volgende:
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
De used_key_parts laten zien dat het zoekplan perfect de gewenste kolommen heeft geselecteerd die in onze samengestelde index worden behandeld.
Samengestelde indexering heeft ook zijn beperkingen. Bepaalde voorwaarden in de query kunnen niet alle kolommen als onderdeel van de sleutel opnemen.
In de documentatie staat:"Het optimalisatieprogramma probeert aanvullende sleutelonderdelen te gebruiken om het interval te bepalen zolang de vergelijkingsoperator =, <=> of IS NULL is. Als de operator> is , <,>=, <=, !=, <>, BETWEEN of LIKE, de optimizer gebruikt het maar houdt geen rekening met meer belangrijke onderdelen. Voor de volgende uitdrukking gebruikt de optimizer =uit de eerste vergelijking. Het gebruikt ook>=van de tweede vergelijking, maar houdt geen rekening met andere belangrijke onderdelen en gebruikt de derde vergelijking niet voor intervalconstructie..." . Dit betekent in feite dat, ongeacht of u een samengestelde index voor twee kolommen heeft, een voorbeeldquery hieronder niet beide velden dekt:
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)
In dit geval (en als uw zoekopdracht meer uit bereiken bestaat in plaats van constante of referentietypes), vermijd dan het gebruik van samengestelde indexen. Het verspilt gewoon je geheugen en buffer en het verhoogt de prestatievermindering van je zoekopdrachten.
Voorvoegselindexen
Prefix-indexen zijn indexen die kolommen bevatten waarnaar wordt verwezen als een index, maar die alleen de beginlengte hebben die voor die kolom is gedefinieerd, en dat gedeelte (of prefixgegevens) is het enige deel dat in de buffer is opgeslagen. Prefix-indexen kunnen u helpen uw bufferpoolbronnen en ook uw schijfruimte te verminderen, omdat deze niet de volledige lengte van de kolom hoeft te nemen. Wat betekent dit? Laten we een voorbeeld nemen. Laten we de impact vergelijken tussen de volledige index en de prefix-index.
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
We hebben een samengestelde index van volledige lengte gemaakt die in totaal 36 MiB tabelruimte in beslag neemt voor de user_account-tabel. Laten we het laten vallen en dan een prefix-index toevoegen.
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
Als je de prefix-index gebruikt, kan deze maximaal 28 MiB bevatten en dat is minder dan 8 MiB dan wanneer je de volledige index gebruikt. Dat is geweldig om te horen, maar het betekent niet dat het performant is en dient wat je nodig hebt.
Als u besluit een prefix-index toe te voegen, moet u eerst bepalen welk type query voor het ophalen van gegevens u nodig heeft. Door een prefix-index te maken, kunt u meer efficiëntie gebruiken met de bufferpool en dus helpt het bij uw queryprestaties, maar u moet ook de beperking ervan kennen. Laten we bijvoorbeeld de prestaties vergelijken bij het gebruik van een index van volledige lengte en een prefix-index.
Laten we een volledige index maken met behulp van een samengestelde index,
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)
Het resultaat laat zien dat het in feite een dekkende index gebruikt, d.w.z. "using_index":true en indexen correct gebruikt, d.w.z. Handler_read_key wordt verhoogd en voert een indexscan uit terwijl Handler_read_next wordt verhoogd.
Laten we nu proberen de prefix-index van dezelfde benadering te gebruiken,
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)
MySQL onthult dat het de index op de juiste manier gebruikt, maar het is opvallend dat er overheadkosten zijn in vergelijking met een volledige index. Dat is duidelijk en verklaarbaar, aangezien de prefixindex niet de hele lengte van de veldwaarden dekt. Het gebruik van een prefix-index is geen vervanging of alternatief voor indexering over de volledige lengte. Het kan ook slechte resultaten opleveren als de prefix-index onjuist wordt gebruikt. U moet dus bepalen welk type zoekopdracht en gegevens u moet ophalen.
Indexen afdekken
Het afdekken van indexen vereist geen speciale syntaxis in MySQL. Een dekkingsindex in InnoDB verwijst naar het geval waarin alle velden die in een zoekopdracht zijn geselecteerd, worden gedekt door een index. Het hoeft geen sequentiële lezing over de schijf uit te voeren om de gegevens in de tabel te lezen, maar gebruik alleen de gegevens in de index, wat de query aanzienlijk versnelt. Bijvoorbeeld, onze vraag eerder, d.w.z.
select last_name from users_account where last_name='Namuag' and first_name='Maximus Aleksandre' \G
Zoals eerder vermeld, is dit een dekkende index. Als u zeer goed geplande tabellen heeft bij het opslaan van uw gegevens en de juiste index hebt gemaakt, probeer dan zo mogelijk te maken dat uw zoekopdrachten zijn ontworpen om de dekkingsindex te benutten, zodat u het resultaat ten goede komt. Dit kan u helpen de efficiëntie van uw zoekopdrachten en het resultaat te maximaliseren voor geweldige prestaties.
Maak gebruik van tools die adviseurs bieden of prestatiecontrole opvragen
Organisaties hebben in eerste instantie vaak de neiging om eerst op github te gaan en open-sourcesoftware te vinden die grote voordelen kan bieden. Voor eenvoudige adviezen waarmee u uw zoekopdrachten kunt optimaliseren, kunt u de Percona Toolkit gebruiken. Voor een MySQL DBA is de Percona Toolkit als een Zwitsers zakmes.
Voor bewerkingen moet u analyseren hoe u uw indexen gebruikt, u kunt pt-index-usage gebruiken.
Pt-query-digest is ook beschikbaar en kan MySQL-query's analyseren uit logs, processlist en tcpdump. In feite is pt-query-digest de belangrijkste tool die u moet gebruiken voor het analyseren en inspecteren van slechte queries. Gebruik deze tool om vergelijkbare zoekopdrachten samen te voegen en te rapporteren over de zoekopdrachten die de meeste uitvoeringstijd kosten.
Voor het archiveren van oude records kunt u pt-archiver gebruiken. Inspecteer uw database op dubbele indexen en maak gebruik van pt-duplicate-key-checker. U kunt ook profiteren van pt-deadlock-logger. Hoewel deadlocks geen oorzaak zijn van een slecht presterende en inefficiënte query, maar van een slechte implementatie, heeft het toch invloed op de inefficiëntie van de query. Als u tabelonderhoud nodig heeft en u moet online indexen toevoegen zonder het databaseverkeer naar een bepaalde tabel te beïnvloeden, dan kunt u pt-online-schema-change gebruiken. Als alternatief kunt u gh-ost gebruiken, wat ook erg handig is voor schemamigraties.
Als u op zoek bent naar zakelijke functies, gebundeld met tal van functies van queryprestaties en -bewaking, alarmen en waarschuwingen, dashboards of statistieken die u helpen uw vragen te optimaliseren, en adviseurs, dan is ClusterControl wellicht de tool voor jij. ClusterControl biedt veel functies die u Top Queries, Running Queries en Query Outliers laten zien. Bekijk deze blog MySQL Query Performance Tuning, waarin u wordt uitgelegd hoe u op één lijn kunt komen met het bewaken van uw zoekopdrachten met ClusterControl.
Conclusie
Zoals je bent aangekomen bij het laatste deel van onze blog met twee series. We hebben hier de factoren besproken die degradatie van query's veroorzaken en hoe u deze kunt oplossen om uw databasequery's te maximaliseren. We hebben ook enkele tools gedeeld die u kunnen helpen en die u kunnen helpen bij het oplossen van uw problemen.