Ini adalah bagian kedua dari blog seri dua bagian untuk Memaksimalkan Efisiensi Kueri Database Di MySQL. Anda dapat membaca bagian satu di sini.
Menggunakan Single-Column, Composite, Prefix, dan Covering Index
Tabel yang sering menerima lalu lintas tinggi harus diindeks dengan benar. Tidak hanya penting untuk mengindeks tabel Anda, tetapi Anda juga perlu menentukan dan menganalisis jenis kueri atau jenis pengambilan apa yang Anda perlukan untuk tabel tertentu. Sangat disarankan agar Anda menganalisis jenis kueri atau pengambilan data apa yang Anda perlukan pada tabel tertentu sebelum Anda memutuskan indeks apa yang diperlukan untuk tabel tersebut. Mari kita bahas jenis indeks ini dan bagaimana Anda dapat menggunakannya untuk memaksimalkan kinerja kueri Anda.
Indeks Kolom Tunggal
Tabel InnoD dapat berisi maksimal 64 indeks sekunder. Indeks kolom tunggal (atau indeks kolom penuh) adalah indeks yang ditetapkan hanya untuk kolom tertentu. Membuat indeks ke kolom tertentu yang berisi nilai berbeda adalah kandidat yang baik. Indeks yang baik harus memiliki kardinalitas dan statistik yang tinggi sehingga pengoptimal dapat memilih rencana kueri yang tepat. Untuk melihat distribusi indeks, Anda dapat memeriksa dengan sintaks SHOW INDEXES seperti di bawah ini:
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)
Anda dapat memeriksa juga dengan tabel information_schema.index_statistics atau mysql.innodb_index_stats.
Gabungan (Komposit) atau Indeks Multi-Bagian
Indeks gabungan (biasa disebut indeks komposit) adalah indeks multi-bagian yang terdiri dari beberapa kolom. MySQL memungkinkan hingga 16 kolom dibatasi untuk indeks komposit tertentu. Melebihi batas mengembalikan kesalahan seperti di bawah ini:
ERROR 1070 (42000): Too many key parts specified; max 16 parts allowed
Indeks komposit memberikan dorongan untuk kueri Anda, tetapi Anda harus memiliki pemahaman yang murni tentang cara Anda mengambil data. Misalnya, tabel dengan DDL sebesar...
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
...yang terdiri dari indeks komposit `nama`. Indeks komposit meningkatkan kinerja kueri setelah kunci ini menjadi referensi sebagai bagian kunci yang digunakan. Misalnya, lihat berikut ini:
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 menunjukkan bahwa rencana kueri telah dengan sempurna memilih kolom yang diinginkan yang tercakup dalam indeks komposit kami.
Pengindeksan komposit juga memiliki keterbatasan. Kondisi tertentu dalam kueri tidak dapat mengambil semua kolom bagian dari kunci.
Dokumentasi mengatakan, "Pengoptimal mencoba menggunakan bagian kunci tambahan untuk menentukan interval selama operator perbandingan =, <=>, atau IS NULL. Jika operatornya> , <,>=, <=, !=, <>, BETWEEN, atau LIKE, pengoptimal menggunakannya tetapi tidak mempertimbangkan bagian penting lagi. Untuk ekspresi berikut, pengoptimal menggunakan =dari perbandingan pertama. Ini juga menggunakan>=dari perbandingan kedua tetapi tidak mempertimbangkan bagian penting lebih lanjut dan tidak menggunakan perbandingan ketiga untuk konstruksi interval..." . Pada dasarnya, ini berarti bahwa terlepas dari Anda memiliki indeks gabungan untuk dua kolom, contoh kueri di bawah ini tidak mencakup kedua bidang:
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)
Dalam kasus ini (dan jika kueri Anda lebih banyak rentang daripada konstan atau tipe referensi), maka hindari menggunakan indeks komposit. Ini hanya membuang memori dan buffer Anda dan meningkatkan penurunan performa kueri Anda.
Indeks Awalan
Indeks awalan adalah indeks yang berisi kolom yang direferensikan sebagai indeks, tetapi hanya mengambil panjang awal yang ditentukan untuk kolom itu, dan bagian itu (atau data awalan) adalah satu-satunya bagian yang disimpan dalam buffer. Indeks awalan dapat membantu mengurangi sumber daya buffer pool Anda dan juga ruang disk Anda karena tidak perlu mengambil panjang penuh kolom. Apa artinya ini? Mari kita ambil contoh. Mari kita bandingkan dampak antara indeks panjang penuh versus indeks awalan.
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
Kami membuat indeks komposit full-length yang menghabiskan total 36MiB tablespace untuk tabel users_account. Mari kita lepaskan lalu tambahkan indeks awalan.
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
Menggunakan indeks awalan, ini hanya menampung hingga 28MiB dan itu kurang dari 8MiB daripada menggunakan indeks full-length. Itu bagus untuk didengar, tetapi itu tidak berarti bahwa itu berkinerja baik dan melayani apa yang Anda butuhkan.
Jika Anda memutuskan untuk menambahkan indeks awalan, Anda harus mengidentifikasi terlebih dahulu jenis kueri untuk pengambilan data yang Anda perlukan. Membuat indeks awalan membantu Anda memanfaatkan lebih banyak efisiensi dengan kumpulan buffer sehingga membantu kinerja kueri Anda, tetapi Anda juga perlu mengetahui batasannya. Misalnya, mari kita bandingkan kinerjanya saat menggunakan indeks full-length dan indeks awalan.
Mari kita buat indeks panjang penuh menggunakan indeks komposit,
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)
Hasilnya mengungkapkan bahwa sebenarnya, menggunakan indeks penutup yaitu "using_index":true dan menggunakan indeks dengan benar, yaitu Handler_read_key bertambah dan melakukan pemindaian indeks saat Handler_read_next bertambah.
Sekarang, mari kita coba menggunakan indeks awalan dengan pendekatan yang sama,
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 mengungkapkan bahwa ia menggunakan indeks dengan benar, tetapi ada biaya overhead dibandingkan dengan indeks full-length. Itu jelas dan dapat dijelaskan, karena indeks awalan tidak mencakup seluruh panjang nilai bidang. Menggunakan indeks awalan bukanlah pengganti, atau alternatif, pengindeksan full-length. Itu juga dapat membuat hasil yang buruk saat menggunakan indeks awalan secara tidak tepat. Jadi, Anda perlu menentukan jenis kueri dan data apa yang perlu diambil.
Meliputi Indeks
Meliputi Indeks tidak memerlukan sintaks khusus di MySQL. Indeks penutup di InnoDB mengacu pada kasus ketika semua bidang yang dipilih dalam kueri dicakup oleh indeks. Tidak perlu melakukan pembacaan berurutan melalui disk untuk membaca data dalam tabel tetapi hanya menggunakan data dalam indeks, secara signifikan mempercepat kueri. Misalnya, kueri kami sebelumnya yaitu
select last_name from users_account where last_name='Namuag' and first_name='Maximus Aleksandre' \G
Seperti yang disebutkan sebelumnya, adalah indeks penutup. Ketika Anda memiliki tabel yang sangat terencana saat menyimpan data Anda dan membuat indeks dengan benar, cobalah untuk membuat kueri Anda dirancang untuk memanfaatkan indeks penutup sehingga Anda akan mendapatkan keuntungan dari hasilnya. Ini dapat membantu Anda memaksimalkan efisiensi kueri dan menghasilkan kinerja yang hebat.
Manfaatkan Alat yang Menawarkan Penasihat atau Pemantauan Kinerja Kueri
Organisasi sering kali pada awalnya cenderung menjadi yang pertama di github dan menemukan perangkat lunak sumber terbuka yang dapat menawarkan manfaat besar. Untuk saran sederhana yang membantu Anda mengoptimalkan kueri, Anda dapat memanfaatkan Percona Toolkit. Untuk DBA MySQL, Percona Toolkit seperti pisau tentara swiss.
Untuk operasi, Anda perlu menganalisis bagaimana Anda menggunakan indeks Anda, Anda dapat menggunakan pt-index-usage.
Pt-query-digest juga tersedia dan dapat menganalisis kueri MySQL dari log, daftar proses, dan tcpdump. Faktanya, alat terpenting yang harus Anda gunakan untuk menganalisis dan memeriksa kueri buruk adalah pt-query-digest. Gunakan alat ini untuk menggabungkan kueri serupa dan melaporkan kueri yang menghabiskan waktu eksekusi paling banyak.
Untuk mengarsipkan catatan lama, Anda dapat menggunakan pt-archiver. Memeriksa database Anda untuk indeks duplikat, manfaatkan pt-duplicate-key-checker. Anda mungkin juga memanfaatkan pt-deadlock-logger. Meskipun kebuntuan bukanlah penyebab kueri yang berkinerja buruk dan tidak efisien tetapi implementasi yang buruk, namun hal itu berdampak pada ketidakefisienan kueri. Jika Anda memerlukan pemeliharaan tabel dan mengharuskan Anda untuk menambahkan indeks secara online tanpa mempengaruhi lalu lintas database yang menuju ke tabel tertentu, maka Anda dapat menggunakan pt-online-schema-change. Atau, Anda dapat menggunakan gh-ost, yang juga sangat berguna untuk migrasi skema.
Jika Anda mencari fitur perusahaan, yang dibundel dengan banyak fitur dari kinerja dan pemantauan kueri, alarm dan peringatan, dasbor atau metrik yang membantu Anda mengoptimalkan kueri, dan penasihat, ClusterControl mungkin merupakan alat untuk Anda. ClusterControl menawarkan banyak fitur yang menunjukkan kepada Anda Kueri Teratas, Kueri Berjalan, dan Pencilan Kueri. Lihat blog ini Penyesuaian Kinerja Kueri MySQL yang memandu Anda agar setara dalam memantau kueri Anda dengan ClusterControl.
Kesimpulan
Saat Anda tiba di bagian akhir blog dua seri kami. Kami membahas di sini faktor-faktor yang menyebabkan penurunan kueri dan cara mengatasinya untuk memaksimalkan kueri basis data Anda. Kami juga membagikan beberapa alat yang dapat bermanfaat bagi Anda dan membantu memecahkan masalah Anda.