Dette er anden del af en blog i to dele om at maksimere effektiviteten af databaseforespørgsler i MySQL. Du kan læse første del her.
Brug af enkeltkolonne, sammensat, præfiks og dækkende indeks
Tabeller, der ofte modtager høj trafik, skal indekseres korrekt. Det er ikke kun vigtigt at indeksere din tabel, men du skal også bestemme og analysere, hvilke typer af forespørgsler eller typer af hentning, du har brug for til den specifikke tabel. Det anbefales kraftigt, at du analyserer, hvilken type forespørgsler eller hentning af data, du har brug for på en specifik tabel, før du beslutter dig for, hvilke indekser der kræves til tabellen. Lad os gennemgå disse typer indekser, og hvordan du kan bruge dem til at maksimere din forespørgselsydeevne.
Enkeltkolonneindeks
InnoD-tabel kan maksimalt indeholde 64 sekundære indekser. Et enkeltkolonneindeks (eller fuldkolonneindeks) er et indeks, der kun er tildelt en bestemt kolonne. At oprette et indeks til en bestemt kolonne, der indeholder forskellige værdier, er en god kandidat. Et godt indeks skal have en høj kardinalitet og statistik, så optimizeren kan vælge den rigtige forespørgselsplan. For at se fordelingen af indekser, kan du tjekke med VIS INDEKSER syntaks ligesom nedenfor:
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)
Du kan også inspicere med tabeller information_schema.index_statistics eller mysql.innodb_index_stats.
Sammensatte (sammensatte) eller flerdelte indekser
Et sammensat indeks (almindeligvis kaldet et sammensat indeks) er et flerdelt indeks sammensat af flere kolonner. MySQL tillader op til 16 kolonner afgrænset for et specifikt sammensat indeks. Overskridelse af grænsen returnerer en fejl som nedenfor:
ERROR 1070 (42000): Too many key parts specified; max 16 parts allowed
Et sammensat indeks giver et boost til dine forespørgsler, men det kræver, at du skal have en ren forståelse af, hvordan du henter dataene. For eksempel en tabel med en DDL på...
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
...som består af sammensat indeks `navn`. Det sammensatte indeks forbedrer forespørgselsydeevnen, når disse nøgler er reference som brugte nøgledele. Se f.eks. følgende:
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 viser, at forespørgselsplanen perfekt har valgt vores ønskede kolonner, der er dækket af vores sammensatte indeks.
Kompositindeksering har også sine begrænsninger. Visse betingelser i forespørgslen kan ikke tage alle kolonner med i nøglen.
Dokumentationen siger, "Optimeringsværktøjet forsøger at bruge yderligere nøgledele til at bestemme intervallet, så længe sammenligningsoperatoren er =, <=> eller ER NULL. Hvis operatoren er> , <,>=, <=, !=, <>, BETWEEN eller LIKE, optimeringsværktøjet bruger det, men tager ikke hensyn til flere nøgledele. For det følgende udtryk bruger optimeringsværktøjet =fra den første sammenligning. Den bruger også>=fra den anden sammenligning, men tager ikke hensyn til yderligere nøgledele og bruger ikke den tredje sammenligning til intervalkonstruktion..." . Grundlæggende betyder dette, at uanset om du har et sammensat indeks for to kolonner, dækker en eksempelforespørgsel nedenfor ikke begge felter:
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)
I dette tilfælde (og hvis din forespørgsel er mere af intervaller i stedet for konstante eller referencetyper), så undgå at bruge sammensatte indekser. Det spilder bare din hukommelse og buffer, og det øger ydeevneforringelsen af dine forespørgsler.
Præfiksindekser
Præfiksindekser er indekser, der indeholder kolonner, der refereres til som et indeks, men kun tager startlængden defineret for den kolonne, og den del (eller præfiksdata) er den eneste del, der er gemt i bufferen. Præfiksindekser kan hjælpe med at mindske dine bufferpuljeressourcer og også din diskplads, da det ikke behøver at tage hele kolonnens længde. Hvad betyder det? Lad os tage et eksempel. Lad os sammenligne virkningen mellem fuldlængdeindeks versus præfiksindeks.
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
Vi har oprettet et sammensat indeks i fuld længde, som bruger i alt 36 MiB tablespace til user_account-tabellen. Lad os droppe det og derefter tilføje et præfiksindeks.
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
Ved brug af præfiksindekset kan det kun holde til 28MiB, og det er mindre end 8MiB end at bruge fuldlængdeindeks. Det er dejligt at høre, men det betyder ikke, at det er effektivt og tjener det, du har brug for.
Hvis du beslutter dig for at tilføje et præfiksindeks, skal du først identificere, hvilken type forespørgsel til datahentning du har brug for. Oprettelse af et præfiksindeks hjælper dig med at udnytte mere effektivitet med bufferpuljen, og det hjælper derfor med din forespørgselsydeevne, men du skal også kende dens begrænsning. Lad os f.eks. sammenligne ydeevnen, når du bruger et indeks i fuld længde og et præfiksindeks.
Lad os oprette et indeks i fuld længde ved hjælp af et sammensat indeks,
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)
Resultatet afslører, at det faktisk bruger et dækkende indeks, dvs. "using_index":sand og bruger indekser korrekt, dvs. Handler_read_key øges og foretager en indeksscanning, når Handler_read_next øges.
Lad os nu prøve at bruge præfiksindeks med samme tilgang,
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 afslører, at den bruger indeks korrekt, men det er bemærkelsesværdigt, at der er en omkostningsomkostning sammenlignet med et indeks i fuld længde. Det er indlysende og forklarligt, da præfiksindekset ikke dækker hele længden af feltværdierne. Brug af et præfiksindeks er ikke en erstatning eller et alternativ til indeksering i fuld længde. Det kan også skabe dårlige resultater, når præfiksindekset bruges uhensigtsmæssigt. Så du skal bestemme, hvilken type forespørgsel og data, du skal hente.
Dækkende indekser
Dækning af indekser kræver ikke nogen speciel syntaks i MySQL. Et dækkende indeks i InnoDB refererer til det tilfælde, hvor alle valgte felter i en forespørgsel er dækket af et indeks. Det behøver ikke at foretage en sekventiel læsning over disken for at læse dataene i tabellen, men kun bruge dataene i indekset, hvilket fremskynder forespørgslen markant. For eksempel vores forespørgsel tidligere, dvs.
select last_name from users_account where last_name='Namuag' and first_name='Maximus Aleksandre' \G
Er som tidligere nævnt et dækkende indeks. Når du har en meget veltilrettelagt tabell, når du har lagret dine data og oprettet indeks korrekt, så prøv så muligt at gøre, at dine forespørgsler er designet til at udnytte dækkende indeks, så du vil gavne resultatet. Dette kan hjælpe dig med at maksimere effektiviteten af dine forespørgsler og resultere i en fantastisk ydeevne.
Udnyt værktøjer, der tilbyder rådgivere eller overvågning af forespørgselsydelse
Organisationer har ofte i starten en tendens til at gå først på github og finde open source-software, der kan tilbyde store fordele. For enkle råd, der hjælper dig med at optimere dine forespørgsler, kan du udnytte Percona Toolkit. For en MySQL DBA er Percona Toolkit som en schweizisk hærkniv.
For operationer skal du analysere, hvordan du bruger dine indekser, du kan bruge pt-index-usage.
Pt-query-digest er også tilgængelig, og den kan analysere MySQL-forespørgsler fra logfiler, processlist og tcpdump. Faktisk er det vigtigste værktøj, du skal bruge til at analysere og inspicere dårlige forespørgsler, pt-query-digest. Brug dette værktøj til at samle lignende forespørgsler sammen og rapportere om dem, der bruger mest eksekveringstid.
Til arkivering af gamle poster kan du bruge pt-archiver. Når du inspicerer din database for duplikerede indekser, kan du bruge pt-duplicate-key-checker. Du kan også drage fordel af pt-deadlock-logger. Selvom dødvande ikke er en årsag til en underpræsterende og ineffektiv forespørgsel, men en dårlig implementering, påvirker det alligevel forespørgslens ineffektivitet. Hvis du har brug for tabelvedligeholdelse og kræver, at du tilføjer indekser online uden at påvirke databasetrafikken, der går til en bestemt tabel, så kan du bruge pt-online-schema-change. Alternativt kan du bruge gh-ost, som også er meget nyttigt til skemamigreringer.
Hvis du leder efter virksomhedsfunktioner, bundtet med masser af funktioner fra forespørgselsydeevne og overvågning, alarmer og advarsler, dashboards eller målinger, der hjælper dig med at optimere dine forespørgsler og rådgivere, kan ClusterControl være værktøjet til du. ClusterControl tilbyder mange funktioner, der viser dig Topforespørgsler, Løbende forespørgsler og Forespørgselsudliggere. Tjek denne blog MySQL Query Performance Tuning, som guider dig, hvordan du er på niveau med at overvåge dine forespørgsler med ClusterControl.
Konklusion
Som du er nået til den afsluttende del af vores blog i to serier. Vi dækkede her de faktorer, der forårsager forespørgselsforringelse, og hvordan man løser det for at maksimere dine databaseforespørgsler. Vi delte også nogle værktøjer, der kan gavne dig og hjælpe med at løse dine problemer.