Detta är den andra delen av en tvådelad serieblogg för att maximera databasfrågeeffektivitet i MySQL. Du kan läsa del ett här.
Använda enkolumn, sammansatt, prefix och täckande index
Tabeller som ofta får hög trafik måste indexeras korrekt. Det är inte bara viktigt att indexera din tabell, utan du måste också bestämma och analysera vilka typer av frågor eller typer av hämtning som du behöver för den specifika tabellen. Det rekommenderas starkt att du analyserar vilken typ av frågor eller hämtning av data du behöver på en specifik tabell innan du bestämmer dig för vilka index som krävs för tabellen. Låt oss gå igenom dessa typer av index och hur du kan använda dem för att maximera din frågeprestanda.
Enkolumnsindex
InnoD-tabellen kan innehålla maximalt 64 sekundära index. Ett enkolumnsindex (eller fullkolumnsindex) är ett index som endast tilldelas en viss kolumn. Att skapa ett index till en viss kolumn som innehåller distinkta värden är en bra kandidat. Ett bra index måste ha en hög kardinalitet och statistik så att optimeraren kan välja rätt frågeplan. För att se fördelningen av index, kan du kontrollera med SHOW INDEXS syntax precis som nedan:
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 också inspektera med tabeller information_schema.index_statistics eller mysql.innodb_index_stats.
Sammansatta (sammansatta) eller flerdelade index
Ett sammansatt index (vanligtvis kallat ett sammansatt index) är ett flerdelat index som består av flera kolumner. MySQL tillåter upp till 16 kolumner avgränsade för ett specifikt sammansatt index. Om gränsen överskrids returneras ett fel som nedan:
ERROR 1070 (42000): Too many key parts specified; max 16 parts allowed
Ett sammansatt index ger en boost till dina frågor, men det kräver att du måste ha en ren förståelse för hur du hämtar data. Till exempel en tabell 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 av sammansatt index `namn`. Det sammansatta indexet förbättrar frågeprestanda när dessa nycklar är referens som använda nyckeldelar. Se till exempel följande:
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
used_key_parts visar att frågeplanen har perfekt valt våra önskade kolumner som täcks av vårt sammansatta index.
Kompositindexering har också sina begränsningar. Vissa villkor i frågan kan inte ta alla kolumner som en del av nyckeln.
Dokumentationen säger, "Optimeraren försöker använda ytterligare nyckeldelar för att bestämma intervallet så länge som jämförelseoperatorn är =, <=> eller ÄR NULL. Om operatorn är> , <,>=, <=, !=, <>, BETWEEN eller LIKE, optimeraren använder det men tar inte hänsyn till fler nyckeldelar. För följande uttryck använder optimeraren =från den första jämförelsen. Den använder också>=från den andra jämförelsen men tar inte hänsyn till ytterligare nyckeldelar och använder inte den tredje jämförelsen för intervallkonstruktion..." . I grund och botten betyder detta att oavsett om du har ett sammansatt index för två kolumner, täcker inte en exempelfråga nedan båda fälten:
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 det här fallet (och om din fråga är mer av intervall istället för konstanta eller referenstyper) så undvik att använda sammansatta index. Det slösar bara ditt minne och buffert och det ökar prestandaförsämringen av dina frågor.
Prefixindex
Prefixindex är index som innehåller kolumner som refereras till som ett index, men som bara tar startlängden som definierats för den kolumnen, och den delen (eller prefixdata) är den enda delen som lagras i bufferten. Prefixindex kan hjälpa till att minska dina buffertpoolsresurser och även ditt diskutrymme eftersom det inte behöver ta hela kolumnen. Vad betyder detta? Låt oss ta ett exempel. Låt oss jämföra effekten mellan fullängdsindex och prefixindex.
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 skapade ett sammansatt index i full längd som förbrukar totalt 36 MiB tabellutrymme för tabellen user_account. Låt oss släppa det och sedan lägga till ett prefixindex.
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
Med prefixindexet rymmer det bara 28MB och det är mindre än 8MiB än att använda fullängdsindex. Det är fantastiskt att höra, men det betyder inte att det är prestanda och tjänar det du behöver.
Om du bestämmer dig för att lägga till ett prefixindex måste du först identifiera vilken typ av fråga för datahämtning du behöver. Att skapa ett prefixindex hjälper dig att använda mer effektivitet med buffertpoolen och så hjälper det med din frågeprestanda men du måste också känna till dess begränsning. Låt oss till exempel jämföra prestandan när du använder ett fullängdsindex och ett prefixindex.
Låt oss skapa ett index i full längd med ett sammansatt 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)
Resultatet avslöjar att det faktiskt använder ett täckande index, dvs. "using_index":sant och använder index på rätt sätt, dvs. Handler_read_key ökas och gör en indexskanning när Handler_read_next ökas.
Låt oss nu försöka använda prefixindex för samma metod,
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 avslöjar att den använder index på rätt sätt, men märkbart att det finns en kostnadsöverbelastning jämfört med ett fullängdsindex. Det är uppenbart och förklarligt, eftersom prefixindexet inte täcker hela längden av fältvärdena. Att använda ett prefixindex är inte en ersättning eller ett alternativ till fullängdsindexering. Det kan också skapa dåliga resultat när prefixindexet används på ett olämpligt sätt. Så du måste bestämma vilken typ av fråga och data du behöver hämta.
Täckande index
Att täcka index kräver ingen speciell syntax i MySQL. Ett täckande index i InnoDB avser fallet när alla fält som valts i en fråga täcks av ett index. Den behöver inte göra en sekventiell läsning över disken för att läsa data i tabellen utan bara använda data i indexet, vilket avsevärt snabbar upp frågan. Till exempel vår fråga tidigare, dvs.
select last_name from users_account where last_name='Namuag' and first_name='Maximus Aleksandre' \G
Som nämnts tidigare, är ett täckande index. När du har en mycket välplanerad tabell över att lagra dina data och skapat index på rätt sätt, försök att göra så möjligt att dina frågor är utformade för att utnyttja täckande index så att du kommer att gynna resultatet. Detta kan hjälpa dig att maximera effektiviteten i dina frågor och resultatet till en fantastisk prestanda.
Utnyttja verktyg som erbjuder rådgivare eller frågeprestandaövervakning
Organisationer tenderar ofta till en början att gå först på github och hitta programvara med öppen källkod som kan erbjuda stora fördelar. För enkla råd som hjälper dig att optimera dina frågor kan du använda Percona Toolkit. För en MySQL DBA är Percona Toolkit som en schweizisk armékniv.
För operationer måste du analysera hur du använder dina index, du kan använda pt-index-användning.
Pt-query-digest är också tillgängligt och det kan analysera MySQL-frågor från loggar, processlist och tcpdump. Faktum är att det viktigaste verktyget som du måste använda för att analysera och inspektera dåliga frågor är pt-query-digest. Använd det här verktyget för att sammanställa liknande frågor och rapportera om de som tar mest körningstid.
För att arkivera gamla poster kan du använda pt-archiver. Inspektera din databas för dubbletter av index, utnyttja pt-duplicate-key-checker. Du kan också dra nytta av pt-deadlock-logger. Även om dödlägen inte är en orsak till en underpresterande och ineffektiv fråga utan en dålig implementering, påverkar det ändå frågans ineffektivitet. Om du behöver tabellunderhåll och kräver att du lägger till index online utan att påverka databastrafiken som går till en viss tabell, kan du använda pt-online-schema-change. Alternativt kan du använda gh-ost, som också är mycket användbart för schemamigreringar.
Om du letar efter företagsfunktioner, tillsammans med massor av funktioner från frågeprestanda och övervakning, larm och varningar, instrumentpaneler eller mätvärden som hjälper dig att optimera dina frågor och rådgivare, kan ClusterControl vara verktyget för du. ClusterControl erbjuder många funktioner som visar dig de bästa frågorna, pågående frågorna och utvikande frågor. Kolla in den här bloggen MySQL Query Performance Tuning som guidar dig hur du kan vara i nivå med att övervaka dina frågor med ClusterControl.
Slutsats
När du har kommit till slutet av vår blogg i två serier. Vi täckte här faktorerna som orsakar frågeförsämring och hur man löser det för att maximera dina databasfrågor. Vi delade också med oss av några verktyg som kan vara till nytta för dig och hjälpa dig att lösa dina problem.