TiDB Index について - How TiDB Index works and tuning? -
はじめに
TiDB は分散型アーキテクチャでありデータの実体は Key-Value Store である TiKV(RocksDB) です。従って、テーブルデータもインデックスデータも MySQL のデータの持ち方と全く異なり、加えて Index データの探索方法が全く異なります。今回は Index データの実体とデータの探索方法を理解し、Tuning に役立てるという趣旨で解説をしていきます。
TL;DR
TiDB は基本的に下記の順番でデータ探索が効率的に行われます。内部動作は後述しますが、TiDB のチューニングポイントをざっくり理解したい場合には、なるべく下記の順番でインデックス設計とSQL設計をすれば初手としては問題ないと考えています。
- Primary Index によるデータ検索
- Unique Index によるデータ検索
- Non-Unique Index によるデータ探索
- Index なしのデータ探索
TiDB の各 Index 構造と実行計画について
ここから実際のテーブルを例として内部構造と実行計画を確認していきます。今回下記のような payment テーブルをベースに説明していきます。
CREATE TABLE `payment` (
`id` smallint NOT NULL,
`receipt_id` smallint NOT NULL,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`) /*T![clustered_index] CLUSTERED */,
UNIQUE KEY `idx_receipt` (`receipt_id`),
KEY `idx_name` (`name`)
)
用意したテーブルデータは下記になります。
id | receipt_id | name |
---|---|---|
1 | 423 | Tom |
2 | 311 | Tom |
3 | 98 | Jack |
1. Primary Index によるデータ検索
TiDB の世界において一般的な int の Primary Key の場合、Primary Key = rowID となり、テーブルデータは下記のように TiKV に格納されています。テーブルデータは下記のようなルールで Key-Value で格納されます。
Key: tablePrefix{tableID}_recordPrefixSep{rowID}
Value: [col1, col2, col3, col4]
今回のデータは Key-Value で下記のように格納されています。tableID=1 と各行の rowID を以て各々の行が格納されています。
# tableID=1, rowID=PrimaryKey=1
Key: t1_r1
Value: [1, 423, 'Tom']
# tableID=1, rowID=PrimaryKey=2
Key: t1_r2
Value: [2, 311, 'Tom']
# tableID=1, rowID=PrimaryKey=3
Key: t1_r3
Value: [3, 98, 'Jack']
Primary Key ベースの検索をした場合、「Point_Get_1」となりシンプルな実行計画になります。今回 Primary Key である id = rowID = 2 で検索しているので、tablePrefix{tableID}_recordPrefixSep{rowID} = t1_r2 を一意で検索することが可能になります。このような実行計画を「Point_Get_1」と呼び、TiDB では最も高速な実行計画になります。
mysql> explain select * from payment where id = 2;
+-------------+---------+------+---------------+---------------+
| id | estRows | task | access object | operator info |
+-------------+---------+------+---------------+---------------+
| Point_Get_1 | 1.00 | root | table:payment | handle:2 |
+-------------+---------+------+---------------+---------------+
2. Unique Index によるデータ検索
Unique Index データは下記のように Key-Value で格納されます。Suffixの「indexedColumnsValue」は Unique Index が貼られた列の実際の値であり、この値はテーブル内で必ず一意になります。従って、tableID+indexID+indexedColumnsValue の情報があれば、対象のテーブルの1行を一意に特定することが可能なる、よって値には rowID を格納しています。
Key: tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue
Value: rowID
今回のデータでは、Unique Index のデータが下記のように格納されています。今回、indexedColumnsValue には receipt_id の実際の値が Suffix として付与されます。
# Key: tableID=1, indexIDy=1, receipt_id=423
# Value: rowID=PrimaryKey=1
Key: t1_i1_423
Value: 1
# Key: tableID=1, indexID=1, receipt_id=311
# Value: rowID=PrimaryKey=2
Key: t1_i1_311
Value: 2
# Key: tableID=1, indexID=1, receipt_id=98
# Value: rowID=PrimaryKey=3
Key: t1_i1_98
Value: 3
Unique Key である receipt_id で検索した場合には、こちらも「Point_Get_1」となりシンプルな実行計画になります。内部的な動作としては Unique Index で rowID を特定し、その rowID を以てテーブルデータを検索するという動作になります。実行計画的には「Point_Get_1」であり Primary Key による検索と似ていますが、Unique Key の場合には、①Index 検索による rowID 取得、②rowID によるテーブルデータの検索、で TiKV へのアクセスが2回になります。但し、今回のようなシンプルなケースでは Primary Key とほぼ同等に高速です。
mysql> explain select * from payment where receipt_id = 432;
+-------------+---------+------+----------------------------------------------+---------------+
| id | estRows | task | access object | operator info |
+-------------+---------+------+----------------------------------------------+---------------+
| Point_Get_1 | 1.00 | root | table:payment, index:idx_receipt(receipt_id) | |
+-------------+---------+------+----------------------------------------------+---------------+
3. Non-Unique Index によるデータ探索
Non-Unique Index データは下記のように Key-Value で格納されます。Unique Index と比較して、rowID の suffix が追加されています。これは TiDB の Index は対象テーブルの各行と1対1で紐づく構造なのですが、Non-Unique Index の indexedColumnsValue は一意であることが保証されません。(今回のデータでは複数行に Tom が存在しています)。従って、rowID の suffix を持ち対象行を一意に特定している、という構造になります。
Key: tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue_rowID
Value: null
また、Value には null が格納されています。null が入っている Index に意味があるのか?と思うのかもしれませんが、問題ありません。
Non-Unique Index 検索時は t1_i2_Tom の Index Key の前方一致で対象 Index を検索します。その後、前方一致で Hit した Index Key に1対1で紐づく各行を取得して結果を返します。この検索方法で Value を参照する必要はないため、Null で問題ないといった挙動になります。
文章のみだとわかりにくいと思うので下記に図示しておきます。
前提の説明が長くなりましたが、今回のデータでの Non-Unique Index のデータは下記のように格納されています。
# Key: tableID=1, indexID=2, receipt_id=423, rowID=1
Key: t1_i2_Tom_1
Value: null
# Key: tableID=1, indexID=2, receipt_id=311, rowID=2
Key: t1_i2_Tom_2
Value: null
# Key: tableID=1, indexID=2, receipt_id=311, rowID=3
Key: t1_i2_Jack_3
Value: null
Non-Unique Key である name で検索した場合、まず「IndexRangeScan_5」で上述の Index Key の前方一致の scan が実行されます。その後、合致する Index Key に含まれる rowID を使用して、対象のテーブルデータを取得するという動作になります。
この動作は Index Key を range scan するという動作上、相対的には Primary Key, Unique Key 検索よりも遅くはなりますが、特に問題ないレベルでは高速です。但し、意図せず Non-Uqniue になっている場合には、Unique Key に変更することをお勧めします。
mysql> explain select * from payment where name = 'Tom';
+-------------------------------+---------+-----------+-------------------------------------+-----------------------------------------------------+
| id | estRows | task | access object | operator info |
+-------------------------------+---------+-----------+-------------------------------------+-----------------------------------------------------+
| IndexLookUp_7 | 0.00 | root | | |
| ├─IndexRangeScan_5(Build) | 0.00 | cop[tikv] | table:payment, index:idx_name(name) | range:["Tom","Tom"], keep order:false, stats:pseudo |
| └─TableRowIDScan_6(Probe) | 0.00 | cop[tikv] | table:payment | keep order:false, stats:pseudo |
+-------------------------------+---------+-----------+-------------------------------------+-----------------------------------------------------+
4. Index なしのデータ探索
こちらはいうまでもなく「TableFullScan」になります。
mysql> explain select * from payment;
+-----------------------+---------+-----------+---------------+--------------------------------+
| id | estRows | task | access object | operator info |
+-----------------------+---------+-----------+---------------+--------------------------------+
| TableReader_5 | 3.00 | root | | data:TableFullScan_4 |
| └─TableFullScan_4 | 3.00 | cop[tikv] | table:payment | keep order:false, stats:pseudo |
+-----------------------+---------+-----------+---------------+--------------------------------+
まとめ
TiDB は MySQL 互換で Index も同様に作成することができますが、内部的な構造は MySQL のそれとは全く異なります。但し、考え方的にはシンプルで Primary Key, Unique Key での検索を心がけることです。この2つは速度的にはほぼほぼ変わらない印象です。次点で Non-Unique Index 利用になりますが、Non-Unique Index 利用の場合には Latency も数 msec のオーダーで変わってくる、かつ Thoughput にも影響を与える可能性があるため、可能な限り Unique Index 利用をお勧めします。
Discussion