これは、MySQLでデータベースクエリの効率を最大化するための2部構成のシリーズブログの第2部です。ここでパート1を読むことができます。
単一列、複合、接頭辞、およびカバーインデックスの使用
トラフィックが多いテーブルは、適切にインデックスを作成する必要があります。テーブルにインデックスを付けることは重要であるだけでなく、特定のテーブルに必要なクエリのタイプまたは取得のタイプを決定して分析する必要もあります。テーブルに必要なインデックスを決定する前に、特定のテーブルで必要なクエリまたはデータの取得のタイプを分析することを強くお勧めします。これらのタイプのインデックスと、それらを使用してクエリのパフォーマンスを最大化する方法を見ていきましょう。
InnoDテーブルには、最大64個のセカンダリインデックスを含めることができます。単一列インデックス(または全列インデックス)は、特定の列にのみ割り当てられるインデックスです。個別の値を含む特定の列へのインデックスを作成することは良い候補です。オプティマイザが適切なクエリプランを選択できるように、適切なインデックスには高いカーディナリティと統計が必要です。インデックスの分布を表示するには、次のようにSHOWINDEXES構文で確認できます。
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)
複合(複合)またはマルチパートインデックス
複合インデックス(一般に複合インデックスと呼ばれます)は、複数の列で構成されるマルチパートインデックスです。 MySQLでは、特定の複合インデックスに対して最大16列を制限できます。制限を超えると、次のようなエラーが返されます。
ERROR 1070 (42000): Too many key parts specified; max 16 parts allowed
複合インデックスはクエリを後押ししますが、データを取得する方法を完全に理解している必要があります。たとえば、DDLが...
のテーブル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
...これは複合インデックス`name`で構成されます。複合インデックスは、これらのキーが使用済みキーパーツとして参照されると、クエリのパフォーマンスを向上させます。たとえば、次を参照してください。
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は、クエリプランが複合インデックスでカバーされている目的の列を完全に選択したことを示しています。
ドキュメントによると、 "オプティマイザーは、比較演算子が=、<=>、またはIS NULLである限り、追加のキーパーツを使用して間隔を決定しようとします。演算子が>の場合、<、> =、<=、!=、<>、BETWEEN、またはLIKEの場合、オプティマイザーはそれを使用しますが、これ以上重要な部分は考慮しません。次の式では、オプティマイザーは最初の比較から=を使用します。>=も使用します。 2番目の比較からですが、それ以上の重要な部分は考慮せず、間隔の構築に3番目の比較を使用しません… " 。基本的に、これは、2つの列の複合インデックスがある場合でも、以下のサンプルクエリは両方のフィールドをカバーしないことを意味します。
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)
この場合(クエリが定数型や参照型ではなく範囲の範囲である場合)、複合インデックスの使用は避けてください。メモリとバッファを浪費するだけで、クエリのパフォーマンスが低下します。
プレフィックスインデックスは、インデックスとして参照される列を含むインデックスですが、その列に定義された開始長のみを取り、その部分(またはプレフィックスデータ)はバッファに格納される唯一の部分です。プレフィックスインデックスは、列の全長を取る必要がないため、バッファプールリソースとディスクスペースを減らすのに役立ちます。これはどういう意味ですか?例を見てみましょう。フルレングスインデックスとプレフィックスインデックスの影響を比較してみましょう。
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
users_accountテーブル用に合計36MiBのテーブルスペースを消費するフルレングスの複合インデックスを作成しました。ドロップしてからプレフィックスインデックスを追加しましょう。
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
プレフィックスインデックスを使用すると、最大28MiBしか保持できません。これは、フルレングスインデックスを使用する場合よりも8MiB未満です。それを聞くのは素晴らしいことですが、それがパフォーマンスが高く、必要なものを提供するという意味ではありません。
プレフィックスインデックスを追加する場合は、最初に、必要なデータ取得用のクエリの種類を特定する必要があります。プレフィックスインデックスを作成すると、バッファプールの効率が向上するため、クエリのパフォーマンスが向上しますが、その制限についても知っておく必要があります。たとえば、フルレングスインデックスとプレフィックスインデックスを使用した場合のパフォーマンスを比較してみましょう。
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)
結果は、実際には、カバーするインデックスを使用していることを示しています。つまり、 "using_index":trueであり、インデックスを適切に使用しています。つまり、Handler_read_keyがインクリメントされ、Handler_read_nextがインクリメントされるとインデックススキャンが実行されます。
では、同じアプローチのプレフィックスインデックスを使用してみましょう。
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は、インデックスを適切に使用していることを示していますが、注目すべきことに、フルレングスのインデックスと比較してコストのオーバーヘッドがあります。プレフィックスインデックスはフィールド値の全長をカバーしていないため、これは明白で説明可能です。プレフィックスインデックスの使用は、フルレングスインデックスの代替でも代替でもありません。また、プレフィックスインデックスを不適切に使用すると、結果が悪くなる可能性があります。したがって、取得する必要のあるクエリとデータの種類を決定する必要があります。
インデックスのカバーには、MySQLで特別な構文は必要ありません。 InnoDBのカバーするインデックスは、クエリで選択されたすべてのフィールドがインデックスでカバーされる場合を指します。テーブル内のデータを読み取るためにディスクを順次読み取る必要はありませんが、インデックス内のデータのみを使用するため、クエリが大幅に高速化されます。たとえば、以前のクエリ、つまり
select last_name from users_account where last_name='Namuag' and first_name='Maximus Aleksandre' \G
前述のように、はカバーインデックスです。データを保存し、インデックスを適切に作成する際に非常によく計画されたテーブルがある場合は、クエリがカバーインデックスを活用するように設計されていることを確認して、結果が得られるようにします。これにより、クエリの効率を最大化し、優れたパフォーマンスを実現できます。
多くの場合、組織は最初にgithubを利用して、大きなメリットを提供できるオープンソースソフトウェアを見つける傾向があります。クエリの最適化に役立つ簡単なアドバイザリについては、PerconaToolkitを活用できます。 MySQL DBAの場合、PerconaToolkitはスイスアーミーナイフのようなものです。
操作については、インデックスをどのように使用しているかを分析する必要があります。pt-index-usageを使用できます。
Pt-query-digestも利用可能であり、ログ、プロセスリスト、およびtcpdumpからMySQLクエリを分析できます。実際、不正なクエリの分析と検査に使用する必要がある最も重要なツールは、pt-query-digestです。このツールを使用して、類似したクエリを集約し、実行時間が最も長いクエリについてレポートします。
古いレコードをアーカイブするには、pt-archiverを使用できます。重複するインデックスがないかデータベースを検査し、pt-duplicate-key-checkerを活用します。 pt-deadlock-loggerを利用することもできます。デッドロックは、パフォーマンスが低く非効率的なクエリの原因ではなく、実装が不十分ですが、クエリの非効率性に影響を与えます。テーブルのメンテナンスが必要で、特定のテーブルに向かうデータベーストラフィックに影響を与えずにオンラインでインデックスを追加する必要がある場合は、pt-online-schema-changeを使用できます。または、gh-ostを使用することもできます。これは、スキーマの移行にも非常に役立ちます。
クエリのパフォーマンスと監視、アラームとアラート、クエリの最適化に役立つダッシュボードまたはメトリック、アドバイザーなど、多くの機能がバンドルされたエンタープライズ機能をお探しの場合、ClusterControlは次のツールになる可能性があります。君。 ClusterControlは、上位クエリ、実行中のクエリ、およびクエリの外れ値を表示する多くの機能を提供します。 ClusterControlを使用してクエリを監視するための標準となる方法をガイドするこのブログMySQLQueryPerformanceTuningをチェックしてください。
2シリーズのブログの最後の部分に到着しました。ここでは、クエリの劣化を引き起こす要因と、データベースクエリを最大化するためにそれを解決する方法について説明しました。また、あなたに利益をもたらし、あなたの問題を解決するのに役立ついくつかのツールを共有しました。