シャーディングは異なるデータサブセットを異なるリソースに割り当て、レプリケーションは同じサブセットの複数のコピーを保持します。前者は容量と処理能力を拡張し、後者は冗長性と読み取りサービスを提供します。実際のシステムでは通常、両方が同時に使用されます。
シャーディングキーを選択する前に、最も一般的なクエリ条件、一緒に更新する必要があるデータ、データの増加分布、アクセスの偏りをリストアップしてください。優れたシャーディングキーは、一般的なリクエストを直接位置指定でき、頻繁に共同操作されるデータを一緒に配置しますが、通常、すべてのクエリを同時に最適化することはできません。
キー、論理パーティション、マシンの区別から始める
ビジネスキーはシャーディングルールによって論理パーティションにマッピングされ、ルーティングメタデータが論理パーティションをレプリカグループまたはマシンにマッピングします。1台のマシンが複数のパーティションをホストでき、1つのパーティションに複数のレプリカが存在する可能性があります。
したがって、「パーティションが100個ある」ことは「マシンが100台ある」こととは等しくありません。論理パーティションの数は移行の粒度とメタデータの規模を決定し、マシンの数と仕様は実際のリソースを決定します。この2つの層を分離することで、拡張時に一部のパーティションのみを移行でき、各ビジネスキーの意味を再定義する必要がなくなります。
範囲シャーディング:キーの順序を保持する
範囲シャーディングは、順序付けられたキーの境界に基づいて切り分けを行います。例えば [0,1000)、[1000,2000) などです。id BETWEEN 950 AND 1050 のようなクエリは2つのパーティションにアクセスします。より狭い範囲では1つのパーティションのみがアクセスされ、より広い範囲ではすべてがカバーされる可能性があります。利点は、境界に基づいて関連のないパーティションを切り捨てられることです(すべての範囲クエリが1つのパーティションのみをアクセスすることを保証するわけではありません)。
レコードが増加する時間順に書き込まれる場合、最新のデータは末尾のパーティションに集中し続ける可能性があります。分割はデータ範囲を狭めますが、新しい書き込みを分散させるとは限りません。後続のリクエストはすべて、新しく生成された右端のパーティションに集中する可能性があります。書き込みの並行性、タイムバケット、および他のキー次元と組み合わせて設計する必要があります。
論理的に隣接しているからといって、物理的な順序でのI/Oが行われるわけではありません。ページ構成、LSMのフラッシュ、圧縮、ストレージメディアが実際のI/Oを決定します。シャーディングは、どのリソースがどの範囲を担当するかを決定するだけです。
ハッシュシャーディング:キーの隣接関係を分散させる
ハッシュシャーディングは、まずシャーディングキーに対して決定論的なハッシュを実行し、バケットまたはトークン範囲を選択します。異なるキーは通常、より均一に分散されやすくなりますが、ハッシュは元の順序を壊します。ルーティングが hash(order_id) のみに依存している場合、注文時間に基づくクエリでは、通常、どのパーティションにアクセスすべきかを直接知ることはできません。
キーの数が均一であっても、負荷が均一であるとは限りません。1万件の一般的なキーでも、1つのホット商品キーのリクエスト数より少なくなる可能性があります。ハッシュは、単一のホットキーを自動的に分散させるわけではありません。DynamoDBの公式ガイドラインでも、キーの値の数だけでなく、実際のアクセスアクティビティに基づいてパーティションキーを設計することが強調されています。パーティションキー設計ガイドライン
複合キー:パーティション内で意味のある順序を保持する
注文システムを想定し、user_id をパーティションキーとし、(created_at, order_id) をパーティション内のソートキーとします。これにより、あるユーザーの直近20件の注文を照会する場合、そのユーザーが属するパーティションを特定し、有界範囲スキャンを実行できます。同じタイムスタンプ内では order_id によってレコードを区別します。
同一バッチの注文:シャーディングキーがクエリで誰を探すかを決定
ユーザーで注文を検索
user_id = 42
hash(user_id)
論理パーティションを特定
ユーザー42の注文
内部では時間でソート
全サイトの本日の注文を検索
user_idの条件なし
パーティションA
パーティションB
パーティションC
ローカルソートにより、単一ユーザーの時間範囲クエリが保持されます。グローバルな時間クエリには、ファンアウトまたは別のインデックスが必要です。
これはアクセスパターンの設計であり、特定のデータベースの構文を制限するものではありません。その境界は、大規模テナントまたは大規模ユーザーです。あるテナントのデータが単一の論理パーティションに存在する必要がある場合、そのパーティションの容量またはスループットが先に上限に達する可能性があります。月ごとのバケットやハッシュサフィックスを追加することで対応できますが、その代償として、複数のバケットをクエリして結果をマージする必要があります。
シャーディングを跨いで直近K件を取得:まずローカル候補を取得し、その後グローバルマージ
ユーザーデータが複数のバケットに分割されている場合、時間を降順で並べ替えて直近K件を取得するには、各バケットから条件を満たす上位K件を取得し、その後グローバルな上位K件を選択する必要があります。ローカルでのK+1件目のレコードは、すでに同じバケット内にK件のレコードが存在するため、グローバルな上位K件に入ることはできません。この推論は、各バケットが同じフィルタ条件と同一の全順序を使用していることを前提としています。
以下のタプルは (created_at, order_id) を表し、2つの列とも降順で、order_id はグローバルに一意です。すべてのデータは固定されており、各シャード内のレコードは事前にソートされています。
from heapq import merge
from itertools import islice
shards = [
[ ( 105 , 9 ) , ( 100 , 8 ) , ( 99 , 7 ) ] ,
[ ( 104 , 6 ) , ( 100 , 5 ) , ( 98 , 4 ) ] ,
[ ( 103 , 3 ) , ( 102 , 2 ) , ( 100 , 1 ) ] ,
[ ] ,
]
def page ( shards , k , before = None ) :
if k <= 0 :
raise ValueError ( " ページサイズは正でなければなりません" )
candidates = [ list ( islice ( ( row for row in shard
if before is None or row < before ) , k ) )
for shard in shards ]
return list ( islice ( merge ( * candidates , reverse = True ) , k ) )
assert page ( shards , 3 ) == [ ( 105 , 9 ) , ( 104 , 6 ) , ( 103 , 3 ) ]
collected , cursor = [ ] , None
while batch : = page ( shards , 2 , cursor ) :
collected . extend ( batch )
cursor = batch [ - 1 ]
assert collected == sorted ( [ row for shard in shards for row in shard ] , reverse = True )
assert len ( collected ) == len ( set ( collected ) ) == 9
ties = [ [ ( 100 , 8 ) , ( 100 , 5 ) , ( 100 , 1 ) ] ]
first = page ( ties , 2 )
assert first == [ ( 100 , 8 ) , ( 100 , 5 ) ]
assert page ( ties , 2 , first [ - 1 ] ) == [ ( 100 , 1 ) ]
assert [ row for row in ties [ 0 ] if row [ 0 ] < first [ - 1 ] [ 0 ] ] == [ ]
各シャードでローカル上位K件を取得し、同じ順序でマージ
シャードA
105/9, 100/8, 99/7
シャードB
104/6, 100/5, 98/4
シャードC
103/3, 102/2, 100/1
グローバル上位3件:105/9, 104/6, 103/3
ソートキー:時間 / グローバルに一意の注文ID
各バケットから最大K件の候補を取得します。同じタイムスタンプの場合、依然として一意の順序と完全なカーソルが必要です。
モデルは最大で P×K 件の候補(Pはシャード数)を収集し、その後マージして K 件を返します。実際のクエリでは、タイムアウト、部分的な結果、および各バケットからの応答の非同期性に対するルールを策定する必要があります。Pythonのフィルタリングはメモリ内スキャンですが、データベースの実装では、ソートインデックスを利用してカーソル位置から位置指定し、この関数を効率的なデータベースクエリプランとして使用すべきではありません。
order_id はページ付け順序の最後の区別次元です。created_at のみでソートするか、または時間のみをカーソルに格納する場合、同一ミリ秒の複数の注文がページを跨いで漏れる可能性があります。SQLの LIMIT でも、一意の順序を決定できる ORDER BY が必要です。PostgreSQL LIMIT/OFFSET ドキュメント を参照してください。
複合カーソルでも、ページを跨ぐスナップショットが自動的に提供されるわけではありません。最初のページ以降に挿入され、カーソルの前に並ぶレコードは、後のページには表示されません。元のレコードのソートキーが変更された場合、読み飛ばしや再読み取りが発生する可能性があります。プロダクトで「同一時刻の完全な結果をトラバースする」ことを要求する場合、利用可能なスナップショットまたは読み取りバージョンを固定し、スナップショット識別子、フィルタ条件、およびソールルールをカーソルにバインドする必要があります。最後のIDのみを保存するだけでは不十分です。
各シャードから何件取得すれば、グローバル上位K件が得られるか?
ローカル候補と最終的な返却値を分離することで、クエリファンアウトの実際のコストが見えます。3つのシャードがそれぞれK件を返す場合、合計で3K件の候補が収集されますが、最終的に必要なのはK件だけです。金色の結果は、同じ (time,id) の全順序に基づいて選択されなければなりません。時間のみを比較して同点の項目を任意に捨てることはできません。図中のレコードは固定されており、ページ付け中に新規作成および変更されたレコードについては、本文のスナップショット規約に従う必要があります。
図を準備しています 再読み込み
各シャードから何件取得すれば、グローバル上位K件が得られるか? ローカルでのK+1件目のレコードは、すでに同じシャード内にK件のレコードが存在するため、グローバルな上位K件に入ることはできません。候補集合は、同じフィルタ条件と決定された全順序を使用する必要があります。
{"id":"sharded-topk-merge","title":"各シャードから何件取得すれば、グローバル上位K件が得られるか?","summary":"ローカルでのK+1件目のレコードは、すでに同じシャード内にK件のレコードが存在するため、グローバルな上位K件に入ることはできません。候補集合は、同じフィルタ条件と決定された全順序を使用する必要があります。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"k\" data-i18n=\"k\"></label><output for=\"k\" id=\"k-value\"></output><input id=\"k\" type=\"range\" min=\"1\" max=\"4\" value=\"2\" step=\"1\"></div><div id=\"shards\" class=\"shards\"></div><div class=\"metrics\"><div><span data-i18n=\"candidates\"></span><strong id=\"candidates\"></strong></div><div><span data-i18n=\"output\"></span><strong id=\"output\"></strong></div></div><div id=\"global\" class=\"track\"></div><p class=\"hint\" data-i18n=\"hint\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n\n.shards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.shards .cell{margin:5px 0;min-height:42px}#global{min-height:58px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const shards=[[[10,9],[9,8],[7,3],[5,1]],[[10,10],[8,7],[7,4],[4,2]],[[10,11],[8,6],[7,5],[3,0]]],cmp=(a,b)=>b[0]-a[0]||b[1]-a[1];function draw(){const k=+$('#k').value;set('k-value',k);const candidates=shards.flatMap(a=>a.slice(0,k)),global=candidates.slice().sort(cmp).slice(0,k),winners=new Set(global.map(x=>x[1]));$('#shards').replaceChildren(...shards.map((s,i)=>{const box=document.createElement('div');const h=document.createElement('strong');h.textContent='S'+(i+1);box.append(h);s.forEach((r,j)=>{const e=document.createElement('div');e.className='cell '+(winners.has(r[1])?'gold':j<k?'on':'empty');e.textContent=r[0]+' · #'+r[1];box.append(e);});return box;}));set('candidates',candidates.length);set('output',global.length);cells('global',global.map(r=>({text:r[0]+' · #'+r[1],cls:'gold'})));$('#global').dataset.ids=global.map(x=>x[1]).join(',');}$('#k').oninput=draw;draw();","audio":false,"strings":{"candidates":"ローカル候補数","heading":"各シャードから何件取得すれば、グローバル上位K件が得られるか?","hint":"青色はローカル上位K件、金色は最終的に選ばれたもの。他のレコードは存在しますが、今回のマージには返却する必要はありません。","intro":"3つの固定データを (time,id) の降順で並べ替えます。Kを調整し、各シャードの候補とグローバル結果を見比べます。同じ時間の場合、idによって順序を決定します。","k":"グローバルK","output":"グローバル返却数","reset":"リセット","scope":"3つの不変シャード。各シャードに4件のデータ、idはグローバルに一意。スクロールのトップKのみを説明。ページを跨ぐ一貫性のあるスナップショット、タイムアウト、および部分的な結果は別途処理する必要があります。","scopeLabel":"モデルの範囲と規約"}}
ホットスポットは原因に応じて処理する
ホットスポットの種類 検討すべき対策 発生するコスト
1つのキーによる大量の読み取り キャッシュ、リクエストのマージ、可能であれば読み取りレプリカの使用 無効化と一貫性のポリシー
1つのカウンターによる大量の更新 複数のサブカウンター、読み取り時に集計 合計値の可視性と正確な制約が複雑になる
最新の時間範囲への継続的な書き込み タイムバケットと他の分散次元の組み合わせ 範囲クエリでファンアウトする可能性がある
他のテナントよりもはるかに大きなテナント テナント内での再シャーディング、または独立したリソース ルーティングとシャーディングを跨ぐ操作の増加
1つのパーティションに複数のホットキーが混在 パーティションの分割または移行 移行中のリソース消費
ソルティング(Salting)は、論理キーにバケット次元を追加することです。例えば (product_id, bucket_id) のようになります。バケットの選択方法、旧記録の検索方法、およびクエリがすべてのバケットをトラバえるかどうかを定義する必要があります。原子性のある更新が必要な同じレコードを異なる位置にランダムに書き込むと、ビジネスセマンティクスが変更されるため、一般的な拡張テクニックとしては使用できません。
例えば (日付, hash(order_id) % B) とすることで、日付とorder_idを知ることで重複チェックのバケットを計算できます。バケットが書き込みのたびにランダムに選択される場合、位置を別途保存するか、バケットをトラバースする必要があります。いずれの場合も、ある日のすべての注文をクエリするには、通常、すべてのB個のバケットにアクセスする必要があります。AWSの書き込みシャーディングの説明では、ランダムなサフィックスと計算可能なサフィックスのクエリコストを比較しています。Write sharding
クライアントがターゲットパーティションに直接接続することでプロキシオーバーヘッドを削減できますが、ターゲットパーティション自体のストレージまたは計算の上限を高めるわけではありません。
セカンダリインデックス:クエリ条件が変われば、維持する組織形式も増える
主テーブルが user_id でシャーディングされているが、クエリ条件が email であると仮定します。各シャードに独自のローカル email インデックスしかない場合、単一シャードでの検索は高速になりますが、呼び出し元は email がどのシャードにあるかを知ることができないため、依然としてファンアウトが必要になる可能性があります。
別の方法は、email で整理されたグローバルインデックスを維持し、email をレコードの主キーにマッピングすることです。この場合、まずインデックスを検索し、その後主表に戻して取得します。その代償として、1回のビジネス書き込みで2つの組織形式を維持する必要があります。これらがパーティションを跨ぐ場合、クロスパーティショントランザクション、非同期伝播、または他の一貫性プロトコルのいずれを採用するかを明確にする必要があります。
「ローカル」「グローバル」の具体的な定義は製品によって異なり、名前だけでインデックスの更新が原子性であるか、強一貫性読み取りをサポートするかどうかを推測することはできません。非同期インデックスは、一時的に新しいレコードを見逃すか、すでに削除されたレコードを指す可能性があります。アプリケーションは、必要なセマンティクスに従って処理する必要があります。インデックスを通じて「このemailは存在しない」ことを確認してから挿入しても、自動的にグローバル一意制約にはなりません。
「インデックスに存在しない」ことを一意性ロックとして使用できない理由
2つの登録リクエストが同じ email を使用しているが、user_id によって異なるシャードにルーティングされています。各主シャードでの単一挿入が原子性であっても、以下のインターリーブは合法です。
時間 リクエストA リクエストB
1 emailインデックスを検索し、記録が見つからない
2 同一インデックスを検索し、記録が見つからない
3 ユーザーAを書き込み
4 ユーザーBを書き込み
5 インデックスを非同期更新 インデックスを非同期更新
2つの主レコードはすでに同時に存在しており、後続のインデックスが最終的に1つまたは2つのエントリを保持しても、この事実を取り消すことはできません。問題はインデックスの伝播が遅いだけではありません。2回のクエリがともに強一貫性であっても、「存在しないことを確認する」と「作成権限を取得する」が2つの独立したステップに分かれている限り、2回の確認がどちらも最初の作成の前に発生する可能性があります。
1つの方向性は、正規化された email で位置指定された一意の場所で、作成権限を原子性で確保することです。または、カバー制約と主トランザクションを使用します。前者では、プレースホルダーの成功、主レコードの未完成時にどのように回復するかを定義する必要があり、永続的なプレースホルダーが残らないようにする必要があります。後者では、対応するクロスパーティション調整のコストを負担する必要があります。グローバル一意制約所谓のコストは、この共同判断を必要とするキーにあります。すべてのシャードでローカル一意インデックスを個別に構築して組み合わせることで解決できるものではありません。
シャーディングキーレビューのクイックチェック
主要なクエリにシャーディングキーが含まれているか?含まれていない場合、どの程度の数のパーティションにアクセスするか?
単一パーティションの容量、書き込み、ホットスポットが先に枯渇する可能性があるか?
頻繁に一緒に更新されるレコードを共置できるか?パーティションを跨ぐ場合、どのようなトランザクションセマンティクスが必要か?
シャーディングキーは変更されるか?変更される場合、それはインプレース更新か、レコードの移動を意味するか?
セカンダリインデックス、ページ付け、グローバルソートの追加コストは許容できるか?
ファンアウトは、テールレイテンシと障害処理コストを増幅します。並列クエリの応答は、通常、必要な結果の中で最も遅いもの待ちになります。部分的な結果を許可する場合、その完全性を明確にする必要があります。完全な結果を要求する場合、再試行、タイムアウト、および重複マージを考慮する必要があります。
続きを読む:一貫性ハッシュ は、メンバーが変更されたときにマッピングの再計算をどのように減らすかを説明します。動的再バランス は、パーティションがどのように移行するかを説明します。
参考文献
Keywords: sharding, partition key, range partitioning, hash partitioning, hot key, salting, secondary index