良いInnoDBプライマリキーを選ぶことは、パフォーマンスチューニングの方向性にとても重要です。この記事では、あなたのワークロードに応じた最適なプライマリキーを選ぶための方法を紹介したいと思います。
Percona社のプリンシパルアーキテクトとしての私の責務の一つは、顧客のデータベースをチューニングすることです。パフォーマンスチューニングに関連する側面は多く存在し、それがこの仕事を複雑、かつ大変興味深いものにしています。この記事では、私は最も重要な側面の一つ、つまり良いInnoDBプライマリキーの選択に関して述べたいと思います。私がこれまで何度プライマリーキーの重要性を説明しなければいけなかったか、またどれほどこの議題に関して議論してきたかに、あなたは驚くかもしれません。この経験を通して、それ以上のことを考えない先入観を持った人々が、さらなるチューニングをやり遂げることができるようになりました。
良いプライマリキーを選ぶことは、InnoDBテーブルにとってすごく重要であり、パフォーマンス大きな影響を及ぼします。1TBに近いRAMを使用してもなお過負荷な状態のx1.16xlarge RDSインスタンスを運用している顧客と仕事をした場合、新しいプライマリキーを設定した後には、r4.4xlargeインスタンスでも非常に上手く動作するようになります。これは巨大なインパクトです。勿論プライマリキーは銀の弾丸ではなく、あなたは後のセクションで私が強調するような対策を立てることが必要です。また、チューニングはトレードオフであり、とりわけプライマリキーの場合ではその性質が色濃くなることを忘れないでください。あなたがパフォーマンスのために何かを得たとしたら、一方ではその対価を払わなければいけません。あなたのワークロードにとって何が最善なのかを考える必要があります。
InnoDBプライマリキーの何が特別なのか?
InnoDBはインデックスで構成されたストレージエンジンと呼ばれます。このストレージエンジンはデータ(テーブルの行)を保管するためにプライマリキーのBツリー構造を使用しています。このことは、プライマリキーがInnoDBでは必須であることを 意味しています。テーブルにプライマリキーが一つもなかった場合、InnoDBは見えない6バイトの自動採番カウンターをテーブルに追加して、この暗黙のカウンターをプライマリキーとして使用します。このInnoDBの暗黙プライマリキーにはいくつか問題があります。あなたはテーブルに対して明示的なプライマリキーを常に設定すべきです。まとめると、あなたはプライマリキーの値を使って全てのInnoDBテーブルのレコードにアクセスしているのです。
InnoDBのセカンダリインデックスも同じくBツリー構造です。検索キーはインデックスカラムで構成され、格納されている値は一致するレコードのプライマリキーです。セカンダリインデックスによる検索は、しばしばプライマリキーによる暗黙的な検索を伴います。こちらのドキュメントを読めば、InnoDBのファイルフォーマットに関する更なる情報を得ることができます。Jeremy Cole氏が作成したInnoDB Rubyというツールも、InnoDBの内部を学ぶためにはとても良い方法です。
Bツリーとは何か?
Bツリーとは、ブロックデバイス上での操作のために最適化されたデータ構造です。ブロックデバイス(or ディスク)では、特にディスクが回転するため、データアクセスのレイテンシがかなり重大です。ランダムな位置から1バイト読み取るのと、より大きな8KB or 16KBのデータをまとめて読み取るのではそれほど時間に差がありません。これがBツリー構造の基礎となる考え方です。InnoDBはデータを16KBのページとして扱います。
さらにBツリーについて簡単に説明しましょう。Bツリーはキーを中心に構成されたデータ構造です。キーはBツリー内部のデータを検索するために用いられます。Bツリーは通常複数の階層を持っています。データは最も下の層に保存されており、これが「リーフ」です。他の階層のページは「ノード」と呼ばれ、次の下層ページへのキーやポインタのみを含んでいます。
キーの特定の値に対するデータにアクセスするには、最上位のノードである「ルートノード」から開始し、含まれているキーと検索値を比較し、次の階層でアクセスするページを見つけます。このプロセスはあなたが最後の階層、すなわち「リーフ」ノードに到達するまで繰り返されます。本来は、あなたはBツリーの一つの階層につき1回のディスク読み取り操作が必要となります。実際には、常にメモリキャッシュが存在しており、ノード数は少なくかつ頻繁にアクセスされるため、キャッシュするのが容易です。
順序付けされたINSERTの例
以下の sysbench テーブルを考えてみましょう:
mysql> show create table sbtest1\G
*************************** 1. row ***************************
Table: sbtest1
Create Table: CREATE TABLE `sbtest1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`k` int(11) NOT NULL DEFAULT '0',
`c` char(120) NOT NULL DEFAULT '',
`pad` char(60) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `k_1` (`k`)
) ENGINE=InnoDB AUTO_INCREMENT=3000001 DEFAULT CHARSET=latin1
1 row in set (0.00 sec)
mysql> show table status like 'sbtest1'\G
*************************** 1. row ***************************
Name: sbtest1
Engine: InnoDB
Version: 10
Row_format: Dynamic
Rows: 2882954
Avg_row_length: 234
Data_length: 675282944
Max_data_length: 0
Index_length: 47775744
Data_free: 3145728
Auto_increment: 3000001
Create_time: 2018-07-13 18:27:09
Update_time: NULL
Check_time: NULL
Collation: latin1_swedish_ci
Checksum: NULL
Create_options:
Comment:
1 row in set (0.00 sec)
プライマリキーのBツリーの大きさは Data_length です。上の例では、一つだけBツリーのセカンダリキー(k_1)が存在し、そのサイズが "Index_length" で示されています。sysbench テーブルは id カラムが auto_increment のため、プライマリキーの順でレコードがINSERTされています。プライマリキーの順でINSERTした時、たとえinnodb_fill_factor変数が100に設定されている場合でも、最大15KB(16KBのうち)のデータをページに埋め込みます。これによって、最初のINSERTの後にページを分割する必要がある場合、事前に一部のレコードを拡張することができます。また、このページ内にはヘッダーとフッターも含まれています。もしページが溢れ、それ以上データの追加ができなくなったら、ページを2つに分割されます。同様に隣り合うページが使用率50%以下だった場合は、InnoDBがそれらをマージします。以下は、idの順に挿入されたsysbenchテーブルの例です。
mysql> select count(*), TABLE_NAME,INDEX_NAME, avg(NUMBER_RECORDS), avg(DATA_SIZE) from information_schema.INNODB_BUFFER_PAGE
-> WHERE TABLE_NAME='`sbtest`.`sbtest1`' group by TABLE_NAME,INDEX_NAME order by count(*) desc;
+----------+--------------------+------------+---------------------+----------------+
| count(*) | TABLE_NAME | INDEX_NAME | avg(NUMBER_RECORDS) | avg(DATA_SIZE) |
+----------+--------------------+------------+---------------------+----------------+
| 13643 | `sbtest`.`sbtest1` | PRIMARY | 75.0709 | 15035.8929 |
| 44 | `sbtest`.`sbtest1` | k_1 | 1150.3864 | 15182.0227 |
+----------+--------------------+------------+---------------------+----------------+
2 rows in set (0.09 sec)
mysql> select PAGE_NUMBER,NUMBER_RECORDS,DATA_SIZE,INDEX_NAME,TABLE_NAME from information_schema.INNODB_BUFFER_PAGE
-> WHERE TABLE_NAME='`sbtest`.`sbtest1`' order by PAGE_NUMBER limit 1;
+-------------+----------------+-----------+------------+--------------------+
| PAGE_NUMBER | NUMBER_RECORDS | DATA_SIZE | INDEX_NAME | TABLE_NAME |
+-------------+----------------+-----------+------------+--------------------+
| 3 | 35 | 455 | PRIMARY | `sbtest`.`sbtest1` |
+-------------+----------------+-----------+------------+--------------------+
1 row in set (0.04 sec)
mysql> select PAGE_NUMBER,NUMBER_RECORDS,DATA_SIZE,INDEX_NAME,TABLE_NAME from information_schema.INNODB_BUFFER_PAGE
-> WHERE TABLE_NAME='`sbtest`.`sbtest1`' order by NUMBER_RECORDS desc limit 3;
+-------------+----------------+-----------+------------+--------------------+
| PAGE_NUMBER | NUMBER_RECORDS | DATA_SIZE | INDEX_NAME | TABLE_NAME |
+-------------+----------------+-----------+------------+--------------------+
| 39 | 1203 | 15639 | PRIMARY | `sbtest`.`sbtest1` |
| 61 | 1203 | 15639 | PRIMARY | `sbtest`.`sbtest1` |
| 37 | 1203 | 15639 | PRIMARY | `sbtest`.`sbtest1` |
+-------------+----------------+-----------+------------+--------------------+
3 rows in set (0.03 sec)
テーブルはバッファプールには収まりませんが、クエリによって良い情報が得られます。プライマリキーのBツリーのページには平均75レコード存在し、15KB未満のデータを格納しています。k_1インデックスは、sysbenchによってランダムな順番で挿入されます。なぜ格納の効率が良いのでしょうか?それはレコードが挿入された後にsysbenchがインデックスを作成し、InnoDBがソートファイルを使用してインデックスを作成するからです。
InnoDBのBツリーの階層数は簡単に見積もることができます。上記の表には、約40000のリーフページ(3M / 75)が必要です。プライマリキーが4バイトの整数である場合、各ノードページには約1200個のポインタが保持されます。したがって、リーフノードの上の階層は約35ページあり、Bツリーの上にはルートノード(PAGE_NUMBER = 3)があります。私たちは合計3つの階層を持っていると言えます。
ランダムな挿入の例
もしあなたが鋭い観察者であれば、プライマリキーをランダムな順序で挿入することによる直接の結果を理解できたでしょう。ページは頻繁に分割され、充填率は平均で約65~75%ほどに過ぎません。あなたは information_schema から簡単に充填率を確認することができます。idカラムの値をランダムな順序で挿入するようsysbenchを修正し、300万レコードのテーブルを作成しました。結果として生成されるテーブルはもっと大きくなります。
mysql> show table status like 'sbtest1'\G
*************************** 1. row ***************************
Name: sbtest1
Engine: InnoDB
Version: 10
Row_format: Dynamic
Rows: 3137367
Avg_row_length: 346
Data_length: 1088405504
Max_data_length: 0
Index_length: 47775744
Data_free: 15728640
Auto_increment: NULL
Create_time: 2018-07-19 19:10:36
Update_time: 2018-07-19 19:09:01
Check_time: NULL
Collation: latin1_swedish_ci
Checksum: NULL
Create_options:
Comment:
1 row in set (0.00 sec)
idの順序で挿入されたプライマリキーのBツリーの大きさは644MBですが、ランダムな順序で挿入された場合のサイズは約1GBと、60%ほど大きくなります。明らかに、ページの充填率はより低くなっています。
mysql> select count(*), TABLE_NAME,INDEX_NAME, avg(NUMBER_RECORDS), avg(DATA_SIZE) from information_schema.INNODB_BUFFER_PAGE
-> WHERE TABLE_NAME='`sbtestrandom`.`sbtest1`'group by TABLE_NAME,INDEX_NAME order by count(*) desc;
+----------+--------------------------+------------+---------------------+----------------+
| count(*) | TABLE_NAME | INDEX_NAME | avg(NUMBER_RECORDS) | avg(DATA_SIZE) |
+----------+--------------------------+------------+---------------------+----------------+
| 4022 | `sbtestrandom`.`sbtest1` | PRIMARY | 66.4441 | 10901.5962 |
| 2499 | `sbtestrandom`.`sbtest1` | k_1 | 1201.5702 | 15624.4146 |
+----------+--------------------------+------------+---------------------+----------------+
2 rows in set (0.06 sec)
プライマリキーのページには、約10KBのデータ(~66%)しか格納されていません。レコードをランダムな順序で挿入することは、一般的または予期できる結果です。一部のワークロードによっては都合が悪いでしょう。しかしそれ以外の場合は、デメリットとしては大きくありません。
実用的な類推
どんなことが起きているのかをよりよく理解するためには、具体的なモデルや類推について考察することは常に良いことです。例えば、Percona Live のような大きなイベントに来訪した全ての参加者の名前と到着時間を紙に記載するよう頼まれているとしましょう。そのため、あなたは良いペンと紙の束を抱え、入口の近くに置かれたテーブルに座っています。人々が到着すると、名前と到着時間が順番に書き込まれていきます。40人分の名前で受付シートがいっぱいになったら、あなたはその紙を横にずらし、また新しい受付シートに書き始めます。これは速く、かつ効率的です。あなたが受付シートに触れるのは一度きりであり、いっぱいになった後にそれに触れることはありません。この類推は簡単です。紙のページはInnoDBのページを表しています。
上記のユースケースは、順序付けられたINSERTを表しています。これは書き込み処理には非常に効率的です。あなたが直面するであろう唯一の問題は、イベントの主催者が「"Mr. X"、または"Mrs. Y"は到着していますか?」と尋ねてきた時でしょう。あなたは名前を見つけるために受付シートを全て確認しなければいけません。これが順序付けられたINSERTの欠点であり、READ処理のコストが高くなる可能性があります。ただし、全てのREADが高コストになるわけではなく、非常にコストが低くなるものもあります。例えば「誰が最初に来た10人は誰ですか?」という質問はとても簡単です。アプリケーションの重要な要素としてINSERTの処理件数とレイテンシがある場合は、順序付けられたINSERT戦略が必要となります。一般的に、このことはREAD処理がユーザにとって問題にならないことを意味しています。READ処理がレポートバッチジョブによって実行されたものとして、ジョブが正常な時間内に完了する限りは、READ処理に関してあなたが問題視することはないでしょう。
では、ランダムな挿入の類推を考えてみましょう。イベントの翌日、主催者からの質問に飽き飽きしたあなたは、新しい戦略を決めます。名字の最初の1文字で名前をグループ化しようというものです。あなたのゴールは名前から簡単に検索できるようにすることです。そのため、あなたは26枚の受付シートを用意して、各シートの上部にアルファベットを1文字ずつ書きます(訳注:Aのシート、Bのシート、・・・、Zのシートを用意する)。最初の参加者が到着した時、紙束の中から目的のシートを探し、名前を記入したらまた正しい場所に戻すという作業により多くの時間を費やしてしまっていることに、あなたはすぐ理解できるでしょう。
翌日まで、あなたは必死に働きました。そして、一部のアルファベットのシートはまだ書かれた名前が少ないのに対し、来訪者が多いために前日よりも多くの枚数が必要となっているシートもあります。これらシートから名前で検索することはとても簡単です。ランダムな順序の挿入の主な欠点は、データを追加するときにデータベースのページを管理する際のオーバーヘッドです。データベースは非常に多くのディスクへの読み書きを行い、データセットのサイズも大きくなります。
あなたのワークロードのタイプを定める
最初のステップは、どのような種類のワークロードなのかを決定することです。INSERTが中心のワークロードの場合、上位のクエリは大きなテーブルへのINSERTであり、データベースは頻繁にディスクへ書き込みを行います。もしあなたがMySQLクライアント上で “show processlist;” を繰り返し実行したら、これらのINSERTが頻繁に表示されるでしょう。これは多くのデータをロギングするようなアプリケーションの典型です。多くのデータコレクターが存在し、それらが全てデータの挿入を待っています。待つ時間が長すぎると、一部のデータが失われる可能性があります。INSERTに関しては厳密なSLAがあり、一方でREADについては緩いSLAが決められている場合は、あなたのワークロードがINSERT中心であることは明確であり、主キーの順番でレコードを挿入する必要があります。
また、大きなテーブルに対しては適切なINSERTの比率を設定することもできますが、これらのINSERTはキューに入れられ、バッチ処理によって実行されます。INSERTが完了するを待つことはなく、サーバはINSERTの件数に容易に追いつくことができます。あなたのアプリケーションにとって重要なのは、INSERTではなく大きなテーブルに対して大量に実行される参照クエリです。既にクエリチューニングを済ませており、良いインデックスが用意されていれば、データベースは非常に高速にディスクからデータを読み取ることができます。
MySQLの processlist を確認すると、大きなテーブルに対して同じSELECTクエリが何度も実行されていることが分かります。唯一の採れる選択肢は、ディスクの読み取りを少なくするためにより多くのメモリを追加することのようですが、テーブルは急速に成長しており、メモリを永久に追加し続けることはできません。READが集中するタイプのワークロードについては、次のセクションで詳しく説明します。
INSERTが重い、または読み込みが重いワークロードがあると判断できない場合は、多大な負荷がかかっていないことがあります。
読み込み中心のワークロード
私はこれまで数年間コンサルティング業務をしてきた中で、主にオンラインゲームやソーシャルネットワーキングアプリケーションなど、多くの読み込み中心のワークロードを見てきました。中にはソーシャルネットワーキングの機能を備えたゲームもありました。ソーシャルネットワーキング機能とは、ゲームの進行に合わせてあなたの友人のスコアを見る機能などです。次の話をする前に、まず読み込みが非常に非効率的であることを確認する必要があります。読み取りが非効率的である場合、多くの参照クエリはフェッチしたレコード数と同等の異なるInnoDBページにアクセスしています。Percona Server for MySQLのスロークエリログでは、ログレベルに"InnoDB"が含まれている場合は、両方の量(※訳注:Rows examine と pages distin)をログに出力し、pt-query-digestツールで統計情報が確認できます。以下に例を示します(一部の行は削除しています)。
# Query 1: 2.62 QPS, 0.00x concurrency, ID 0x019AC6AF303E539E758259537C5258A2 at byte 19976
# This item is included in the report because it matches --limit.
# Scores: V/M = 0.00
# Time range: 2018-07-19T20:28:02 to 2018-07-19T20:28:23
# Attribute pct total min max avg 95% stddev median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count 48 55
# Exec time 76 93ms 637us 3ms 2ms 2ms 458us 2ms
# Lock time 100 10ms 72us 297us 182us 247us 47us 176us
# Rows sent 100 1.34k 16 36 25.04 31.70 4.22 24.84
# Rows examine 100 1.34k 16 36 25.04 31.70 4.22 24.84
# Rows affecte 0 0 0 0 0 0 0 0
# InnoDB:
# IO r bytes 0 0 0 0 0 0 0 0
# IO r ops 0 0 0 0 0 0 0 0
# IO r wait 0 0 0 0 0 0 0 0
# pages distin 100 1.36k 18 35 25.31 31.70 3.70 24.84
# EXPLAIN /*!50100 PARTITIONS*/
select * from friends where user_id = 1234\G
friendsテーブルの定義は以下の通りです:
CREATE TABLE `friends` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`friend_user_id` int(10) unsigned NOT NULL,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`active` tinyint(4) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_id_friend` (`user_id`,`friend_user_id`),
KEY `idx_friend` (`friend_user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=144002 DEFAULT CHARSET=latin1
私はこの簡単なテーブルをテストサーバ上に構築しました。テーブルデータはメモリに簡単に収まるので、ディスクの読み込みは発生しません。ここで重要なのは "page distin" と "Rows examine" の間の関係性です。ご覧のように、この比率は 1 に近くなっています。つまり、InnoDBは1ページ当たり2つ以上のレコードを取得することは殆どありません。指定された user_id の値の場合、該当するレコードは主キーBツリーの全体に分散しています。これを確認するために、以下サンプルクエリの出力を見てみます。
mysql> select * from friends where user_id = 1234 order by id limit 10;
+-------+---------+----------------+---------------------+--------+
| id | user_id | friend_user_id | created | active |
+-------+---------+----------------+---------------------+--------+
| 257 | 1234 | 43 | 2018-07-19 20:14:47 | 1 |
| 7400 | 1234 | 1503 | 2018-07-19 20:14:49 | 1 |
| 13361 | 1234 | 814 | 2018-07-19 20:15:46 | 1 |
| 13793 | 1234 | 668 | 2018-07-19 20:15:47 | 1 |
| 14486 | 1234 | 1588 | 2018-07-19 20:15:47 | 1 |
| 30752 | 1234 | 1938 | 2018-07-19 20:16:27 | 1 |
| 31502 | 1234 | 733 | 2018-07-19 20:16:28 | 1 |
| 32987 | 1234 | 1907 | 2018-07-19 20:16:29 | 1 |
| 35867 | 1234 | 1068 | 2018-07-19 20:16:30 | 1 |
| 41471 | 1234 | 751 | 2018-07-19 20:16:32 | 1 |
+-------+---------+----------------+---------------------+--------+
10 rows in set (0.00 sec)
上記レコードの多くは、idカラムが数千ほど離れています。レコードサイズは約30バイトほどと小さいですが、InnoDBページに含まれるレコードは500行以下です。アプリケーションが人気になるにつれて、ユーザ数は徐々に増えていき、テーブルサイズはユーザ数の2乗くらいのペースで大きくなります。テーブルサイズがInnoDBのバッファプール領域を超過したら、MySQLはディスクからデータを読み込み始めます。さらに悪いことに、何もキャッシュがされていない状態だと友達1人につき1回の読み込みIOPが発生します。これらのSELECTの割合が300件 / 秒で、かつすべてのユーザに平均100人の友達がいる場合、MySQLは毎秒30000ページにアクセスする必要があります。これでは明らかに長期に渡ってスケールさせることはできません。
私たちは全てのテーブルへのアクセス方法を決定する必要があります。そこで、私はpt-query-digestを使って返されるクエリフォームの数に制限をかけます。例えば以下のように仮定しましょう:
- user_id によるものが93%
- friend_id によるものが5%
- id によるものが2%
上記の割合はかなり一般的です。比率が高いアクセスパターンが存在する場合は、何かしらできることはあります。friendsテーブルは多対多テーブルの典型例です。InnoDBでは、以下のようなテーブルを定義する必要があります。
CREATE TABLE `friends` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(10) unsigned NOT NULL,
`friend_user_id` int(10) unsigned NOT NULL,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`active` tinyint(4) NOT NULL DEFAULT '1',
PRIMARY KEY (`user_id`,`friend_user_id`),
KEY `idx_friend` (`friend_user_id`),
KEY `idx_id` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=144002 DEFAULT CHARSET=latin1
ここでは、プライマリキーBツリー内のuser_idによってレコードが順序付けられてグループ化されますが、挿入順はランダムです。そうでない場合は、SELECT文へのメリットのためにINSERTは遅くなります。レコードをINSERTするためにInnoDBは、新しいページをどこに配置するかを確認するために1回のディスク読み込みを行い、データをディスクに保存するために1回のディスク書き込みを行う必要があります。これまでの類推では、紙束から受付シートを1枚取り出し、名前を書いて元に戻す部分に当たります。また、テーブルが大きくなってもInnoDBのページはいっぱいにならず、プライマリキーが大きいほどセカンダリインデックスも大きくなっていきます。ここで私たちはセカンダリインデックスも追加しましたので、バッファプール内のデータが少なくなりました。
バッファプール内にデータが少なくなると、パニックに陥ってしまうでしょうか?答えは「いいえ」で、それはInnoDBがディスクからページを読み込むとき、一致するレコードを1つだけでなく、数百行をまとめて取得するからです。読み込みのIOPSの量は、友人の数とSELECT文の割合をかけあわせたものではなくなります。それは最早、SELECT文の到達率の一要素に過ぎません。全てのテーブルをキャッシュするための十分なメモリを持っていないことの影響は、これで大幅に軽減されます。ストレージがSELECT文が発行される割合よりも多くの読み込みIOPSを実行できる限り、すべて正常であると言えます。変更されたテーブルを使用すると、pt-query-digestの出力結果は以下のようになります:
# Attribute pct total min max avg 95% stddev median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Rows examine 100 1.23k 16 34 23.72 30.19 4.19 22.53
# pages distin 100 111 2 5 2.09 1.96 0.44 1.96
新しいプライマリキーがあれば、30000回の読み込みIOPSの代わりに、約588回のIOPS(~300 * 1.96)を実行するだけで済みます。INSERTのコストは高くなりますが、発行される件数が 100件 / 秒 であれば、最も悪い場合で100回の読み込みIOPSおよび書き込みIOPSがかかることを意味します。
上記の戦略は、明確なアクセスパターンが存在する場合にうまく機能します。私の経験では通常は支配的なアクセスパターンが存在していることが多く、例えば以下のような例があります:
- ゲームのランキング (by user)
- ユーザー設定 (by user)
- メッセージ機能 (by from or to)
- ユーザの所有アイテム一覧 (by user)
- アイテムのお気に入り機能 (by item)
- アイテムのコメント (by item)
支配的なアクセスパターンが存在しない場合は、何ができるでしょう?選択肢の一つは、カバリングインデックスの使用です。カバリングインデックスは、必要とされるカラム全てを包括する必要があります。カラムの順序も重要で、最初のカラムはグループ化された値でなければいけません。もう一つの選択肢は、パーティションを使用してデータセット内のホットスポットをキャッシュしやすくすることです。私はこれらの戦略について、今後のブログ記事で議論したいと思います。
本記事では、READ集中型のワークロードを解決するための一般的な戦略を見てきました。この戦略は常に機能するわけではありません。共通のパターンでデータにアクセスしている必要があります。しかし、それがうまくいき、良いプライマリキーが選ばれると、あなたはその日のヒーローになれるでしょう!