TL

フラクショナルカスケーディングと分数インデックス

要素の間に新しい順序キーを無限に挿入でき、リストの並べ替えや協調編集で全行を振り直さずに済む。順序維持文字列キーの数理と実装の落とし穴を原理から理解できます。

応用分数インデックス順序維持協調編集データ構造ソートキー最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.分数インデックスは、隣り合う2つの順序キーの「中間」を必ず生成できる稠密順序を文字列で実現し、要素間に無限挿入を可能にする。
  • 2.鍵は基数bのなかで桁を伸ばしながら厳密に a と b の間を取る生成法と、辞書順比較がそのまま数値順序になる桁揃えの設計にある。
  • 3.協調編集では同時挿入でキーが衝突しうるため、末尾に一意なサイトIDを付けて全順序にし、CRDTやリスト順序の安定したソートキーとして使う。

何を解こうとしているのか

リストの並びをデータベースに保存する素朴な方法は、各行に整数の position(0,1,2,…)を持たせることです。しかしこの方式は、ある要素を 1 番と 2 番の あいだ に入れたいだけで破綻します。整数 1 と 2 の間には整数がないため、後続の要素すべての番号を振り直す(リナンバリング)か、行をロックして再計算するしかありません。1 件の挿入が O(n) 件の更新を生み、協調編集のように複数クライアントが同時に並べ替える環境では、その一括更新どうしが衝突します。

分数インデックス(fractional indexing) は、順序キーを整数ではなく 稠密な順序集合 の値として持つことでこれを解きます。稠密とは「異なる任意の2要素 a と b(a が b 未満)の間に、必ず別の要素が存在する」性質です。有理数や実数はこの性質を持つので、1 と 2 の間には 1.5、その間には 1.25…と無限に値が取れます。要素を間に挿入したいなら、両隣のキーの中間を1つ計算して新しいキーにするだけでよく、他の行には一切触れません。挿入は O(1) の書き込みになります。

名前の由来:分数とフラクショナルカスケーディング

「分数(fractional)」は、キーを 0 と 1 の間の分数として表す発想に由来します。一方、計算幾何学の フラクショナルカスケーディング は、複数のソート済みリストを横断する二分探索を高速化する別技法で、サンプル要素を隣のリストへ「カスケード」させて橋渡しします。どちらも「整列済み列の間を効率よく行き来する」着想を共有しますが、本稿が扱うのは前者の順序キー生成です。両者は名前が近いだけで適用場面が異なる点に注意してください。

なぜ整数ではなく文字列なのか

理屈の上では有理数をそのまま使えますが、実装では問題が起きます。中間を取り続けると分母が指数的に増え、浮動小数点では精度が枯渇し、可変長整数では比較コストが膨らみます。そこで実用実装は 基数 b の桁列(文字列) でキーを表し、辞書順(lexicographic)比較がそのまま順序比較になる ように設計します。これなら任意精度を桁の追加だけで確保でき、データベースの文字列インデックス(B+Tree など)にそのまま載せて高速に範囲検索・整列できます。

要点は2つです。第一に、キーを「0.(桁列)」という基数 b の小数とみなすこと。第二に、辞書順と数値順を一致させる桁揃え を守ることです。たとえば基数 10 で 525 を辞書順で比べると、先頭の 25 より小さいので 25 が前になり、これは小数 0.5 と 0.25 の数値順(0.25 が前)と一致します。つまり「小数点以下の桁列」として左詰めで比較する限り、辞書順 = 数値順が成り立ちます。実装では基数を大きく取り(base64 など)、文字集合自体を昇順に並べておけば、文字どうしのバイト比較が桁の大小比較に一致します。

中間キーの生成アルゴリズム

中心となるのは「ab の厳密な中間にある最短の桁列を返す」関数 midpoint(a, b) です。ab 未満であることを前提に、両端を基数 b の小数として桁ごとに比較していきます。

midpoint(a, b):   # a, b は小数点以下の桁列、a < b(辞書順)
  i = 0
  # 共通する先頭桁はそのままコピーする
  while a[i] == b[i]:        # b 側が尽きたら b は無限に 0 が続くとみなす
    append a[i] to result
    i++
  # 食い違う最初の桁を見る
  da = digit(a, i)           # a が尽きたら 0
  db = digit(b, i)
  if db - da >= 2:
    # 桁の間に隙間があるので、その中央の1桁で確定
    append floor((da + db) / 2) to result
    return result
  else:
    # 隣接桁(差が1)。a 側の桁を採用し、a の続きと最大桁の中間へ降りる
    append da to result
    return result + midpoint(rest_of(a, i+1), "")  # b 側は最大桁の上限とみなす

ポイントは食い違った桁の差です。差が 2 以上なら、その2桁の 中央の1桁 を置くだけで a より大きく b より小さい値が確定し、桁を伸ばさずに済みます。差が 1 しかない(隣接している)ときは、その桁では間に入れないので a 側の桁を採り、1桁深く降りて さらに中間を探します。再帰の各段で必ず探索範囲が a 寄りに狭まり、いずれ差 2 以上の隙間か桁の尽きに到達して停止します。生成されるキーは平均してごく短く、最悪でも入力キー長 + 数桁に収まります。

両端が無い場合(先頭・末尾への挿入)

リストの 先頭 に挿入するときは左隣が存在しないので、下限 a を「空(= 最小)」とみなして midpoint("", b) を呼びます。末尾 なら右隣がないので上限 b を「最大」とみなします。実装では基数の最小文字・最大文字を番兵として扱うか、b が空のとき桁を1つ足して中央に置く分岐を用意します。空リストへの最初の挿入は、基数のちょうど真ん中の1桁(base64 なら中央付近の文字)を返すのが定石で、以後の前後挿入に余白を残せます。

協調編集での全順序化

単独クライアントなら midpoint だけで足りますが、同時挿入 が入ると話が変わります。2 つのクライアントが同じ隙間(同じ ab の間)にそれぞれ要素を入れると、両者が まったく同じ中間キー を計算してしまい、キーが衝突します。順序キーは要素を一意に整列させる責務を負うので、衝突は順序の不定(どちらが先か決まらない)を招きます。

解決は、分数キーの 末尾に一意なサイトID(クライアントIDやアクター識別子)を連結 して全順序(total order)にすることです。比較は「分数部を先に比べ、等しければサイトIDで決定的に破る」。これにより同一隙間への並行挿入でも、全レプリカが同じ規則で同じ順序に収束します。これは CRDT のリスト型(RGA や LSEQ 系)が内部で用いるのと同じ発想で、分数インデックスは「位置を表す不変な識別子」をキー1本で表現する軽量な実装手段になります。

観点整数 position分数インデックス(文字列)
間への挿入後続を全リナンバリング(O(n))中間キーを1つ生成(O(1) 書き込み)
同時挿入一括更新どうしが衝突しやすいサイトID付与で全順序に収束
キー長固定(小さい)挿入を繰り返すと漸増しうる
並べ替え範囲の番号を振り直す移動先の両隣で中間キーを再生成
索引適性数値索引に最適辞書順索引にそのまま載る

実装上の落とし穴

分数インデックスは挿入が局所的で速い反面、キー長が単調に伸びうる 弱点があります。最悪のパターンは「常に同じ要素の直後(または直前)に挿入し続ける」ケースで、毎回 1 桁ずつ深く降りるため、キー長が挿入回数に比例して伸びます。長くなったキーは索引の肥大とソートコスト増を招くため、実務では一定長を超えたら オフピークにバックグラウンドで全キーをリバランス(振り直し) する運用を併用します。

辞書順を壊す3つの定番ミス

辞書順 = 数値順の前提が崩れるとソートが静かに狂います。第一に、文字集合の順序とバイト順序の不一致:使う基数の文字をコード順に昇順整列しておかないと比較が逆転します。第二に、末尾のゼロ相当文字:基数の最小文字でキーを終えると「a より小さい a の延長」を作れず中間生成が詰まるため、最小文字を末尾に置かない規約にします。第三に、可変長を数値の整数部として比較する誤り:あくまで「0.(桁列)」の小数として左詰め比較する設計を守ること。これらは単体テストで隣接キーの順序不変条件(生成キーが両隣の厳密な間にある)を検査して防ぎます。

試験・面接での頻出ポイント

「分数インデックスが解く問題は」には 間への挿入で後続を全リナンバリングせず O(1) 書き込みにする と答えます。「なぜ文字列で持つか」は 任意精度を桁追加で確保し、辞書順=数値順により文字列索引にそのまま載るから。「midpoint の停止理由は」は 食い違う桁の差が 2 以上なら中央1桁で確定、差 1 なら1桁降りて範囲が必ず狭まるから。「協調編集での衝突対策は」は 末尾にサイトIDを連結して全順序化 と整理しておくと安全です。

まとめ

まとめ

分数インデックスは、順序キーを整数ではなく 稠密な順序集合(実装上は基数 b の桁列=文字列)として持つことで、隣り合う2キーの 厳密な中間midpoint で生成し、要素間への挿入を他行に触れず O(1) 書き込みで実現します。鍵は 辞書順比較がそのまま数値順序になる桁揃え と、食い違う桁の差に応じて中央1桁で確定するか1桁降りる生成法です。協調編集では同時挿入でキーが衝突しうるため、末尾に サイトID を連結して全順序化し、CRDT のリスト順序や安定したソートキーとして使います。挿入が局所的で速い反面、片側挿入の連続でキー長が伸びうるため、長くなったキーの 定期リバランス を運用に組み込み、クラスタ化/非クラスタ化インデックスなど辞書順索引の上で範囲検索・整列に活かすのが定石です。

データベース Article

フラクショナルカスケーディングと分数インデックスを実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

分数インデックス

比較で見る軸

難易度: advanced / カテゴリ: データベース / タグ数: 5

導入後に効く点

鍵は基数bのなかで桁を伸ばしながら厳密に a と b の間を取る生成法と、辞書順比較がそのまま数値順序になる桁揃えの設計にある。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
データベース
タグ数
5

判断チェックリスト

  • 自社の用途が「分数インデックス / 順序維持」に近いか確認する。
  • 強みである「分数インデックスは、隣り合う2つの順序キーの「中間」を必ず生成できる稠密順序を文字列で実現し、要素間に無限挿入を可能にする。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

分数インデックス順序維持協調編集データ構造ソートキー分数インデックス順序維持協調編集