グラフDBの内部:隣接リストとインデックスフリー隣接
なぜグラフDBは多段のホップを速く辿れるのか、その正体が原理から腑に落ちます。物理ポインタで隣接を直結する index-free adjacency を、隣接行列・CSR と対比して掴めます。
- 1.ネイティブグラフDBは index-free adjacency を採り、各ノードのレコードに隣接エッジへの物理ポインタ(ファイルオフセット)を直接埋める。隣を辿るのにグローバルインデックスを引かないので、1ホップが定数時間で済む。
- 2.RDB のテーブルでグラフを表すと、JOIN ごとにエッジ表の B+Tree インデックスを引く必要があり、1ホップが O(log N) かかる。K ホップで log の重みが K 回乗り、深い探索ほどネイティブ方式と差が開く。
- 3.隣接行列は密グラフや行列演算向きで V×V のメモリを食い、CSR は静的な疎グラフを連続配列で最小に詰めて解析バッチに向く。可変・更新が多いトランザクショングラフには連結リスト型の隣接が向く。
グラフを「物理的に」どう置くか
グラフは頂点(ノード)と辺(エッジ)の集まりです。論理モデルは単純でも、これをディスクやメモリにどう並べるかでトラバーサル(隣を次々辿る探索)の速さが桁で変わります。グラフDB の心臓部は「あるノードの隣接エッジを、どれだけ速く列挙できるか」に尽きます。BFS・最短経路・パターンマッチングはすべて「隣を辿る」操作の繰り返しだからです。
物理レイアウトには大きく3系統あります。ノードのレコードに隣接エッジへのポインタを直接埋める隣接リスト型(ネイティブグラフDB が採る)、V×V の真偽値表で辺の有無を持つ隣接行列、疎な隣接リストを連続配列に圧縮する CSR(Compressed Sparse Row)です。それぞれ得意な形と操作が違います。
グラフDB は「グラフを物理レイアウトとしても採るネイティブ型」(Neo4j など)と「RDB やドキュメントDB の上にグラフAPIを被せた非ネイティブ型」に分かれます。違いの核心は本記事で扱うストレージの持ち方です。論理的に同じグラフでも、隣接を物理ポインタで直結するか、毎ホップでインデックスを引くかで、深い探索の計算量が原理的に変わります。
index-free adjacency――隣を引かずに辿る
ネイティブグラフDB の中核思想が index-free adjacency(インデックスフリー隣接)です。各ノードのレコードには、そのノードに接続するエッジレコードへの物理ポインタ(ファイル内のオフセット、固定長レコードなら ID×レコードサイズで算出可能)が格納されます。エッジレコードはさらに「次の隣接エッジ」「相手側ノード」への物理ポインタを持ち、隣接エッジが連結リストとして物理的に繋がります。
Node(A) ──first_edge──▶ Edge(A→B) ──next_edge──▶ Edge(A→C) ──next_edge──▶ ∅
│ to_node │ to_node
▼ ▼
Node(B) Node(C)
各ポインタはファイルオフセット。値を介さず、加算で次レコードに飛ぶ。
要点は、隣を辿る操作がグローバルインデックスの検索を一切伴わないことです。ポインタは「どこを読めばよいか」を直接指すので、ノード A の隣接を列挙する手間は A の次数(隣の数)に比例するだけで、グラフ全体のサイズ N には依存しません。1ホップが実質定数時間(隣1個あたり)で済み、これはヒープ上の行を TID という物理アドレスで直接指すのと同じ「値で引かずアドレスで飛ぶ」発想の、グラフ版です。
物理オフセットへ加算一発で飛べるのは、ノード・エッジが固定長レコードだからです。Neo4j がノード15バイト・関係34バイト(バージョンにより変動)といった固定長を採るのはこのためで、ID から位置を ID × レコードサイズ で計算できます。代償として可変長プロパティは別ストア(プロパティストア)に逃がし、ノード/エッジ本体は隣接ポインタだけの痩せた構造に保ちます。
RDB でグラフを表すと何が起きるか
同じグラフをリレーショナルDBで表すと、ノード表とエッジ表(from_id, to_id の2列)になります。「A の隣を辿る」は SELECT to_id FROM edges WHERE from_id = A で、from_id の B+Tree インデックスを引きます。これは1ホップあたり O(log N) です(N はエッジ総数)。
問題は深い探索で顕在化します。K ホップ辿るには JOIN を K 回重ね、各段で log N の重みが乗ります。ネイティブ方式が「次数の合計回ぶんポインタを辿るだけ」なのに対し、RDB 方式は「訪れるノードごとにインデックスを引き直す」ため、探索が深く広いほど log の係数が効いてきます。
| 観点 | index-free adjacency(ネイティブ) | エッジ表+インデックス(RDB) |
|---|---|---|
| 1ホップの隣接列挙 | 物理ポインタを辿るだけ。グラフ全体サイズ N に非依存 | from_id の B+Tree を検索。O(log N) |
| K ホップ探索 | 辿ったエッジ数に線形。log の係数が乗らない | 各ホップで log N を支払い、深さ K ぶん累積 |
| 局所性 | 隣接が物理的に繋がりやすく、関連レコードが近い | インデックス葉と行本体が散在しランダム I/O |
| 更新 | ポインタの繋ぎ替えが必要だが局所的 | 行 INSERT/DELETE とインデックス保守 |
| 向くワークロード | 深い・可変な経路探索、リアルタイム | 浅い結合、既存 RDB 資産との統合 |
index-free adjacency は「DB にインデックスが全く無い」という意味ではありません。起点ノードを ID やラベルで最初に見つけるときには通常のインデックスを使います。インデックスを使わないのはそこから先の隣接を辿る部分だけです。最初の1ノードを索引で特定し、あとはポインタチェイシングに切り替える――この役割分担が肝で、探索全体を毎ホップ索引引きにしてしまう RDB 方式との差はここにあります。
隣接行列と CSR――解析・行列演算の視点
トラバーサルではなくグラフ全体の解析(PageRank、連結成分、行列演算)では、別のレイアウトが効きます。
隣接行列は V×V のビット(または重み)表で、M[i][j] が辺 i→j の有無を表します。辺の存在判定が定数時間、行列積でホップ数の合成(2ホップ到達は M²)ができるのが利点ですが、メモリが頂点数の2乗に比例し、辺が疎なグラフ(実世界の大半)では大半が 0 で埋まり致命的に無駄です。密グラフや小規模グラフ、線形代数で解く GNN の隣接表現に向きます。
CSR(Compressed Sparse Row)は疎な隣接リストを3本の連続配列に圧縮します。row_ptr[i] がノード i の隣接の開始位置、col_idx が隣接先 ID を詰めた配列です。
ノード0: {1,2} ノード1: {2} ノード2: {0}
col_idx = [1, 2, 2, 0] # 隣接先をノード順に連結
row_ptr = [0, 2, 3, 4] # ノード i の隣接は col_idx[row_ptr[i]..row_ptr[i+1]]
ノード0の隣接 = col_idx[0..2] = {1, 2}
CSR は隣接が完全に連続配置されるためキャッシュ局所性が極めて高く、頂点数+辺数のメモリしか食いません。これは列指向ストレージが同型値を連続配置してスキャンを速める発想と同じで、グラフ解析エンジン(GraphBLAS、各種 GNN ライブラリ)が CSR/CSC を標準採用する理由です。
CSR の弱点は動的更新です。1本の辺を挿入すると col_idx の途中に挿入が必要で、後続要素のシフトと row_ptr の付け替えが連鎖し、最悪 O(辺数) かかります。だから CSR は「グラフを固めて一括解析する」静的バッチ向きで、頻繁に辺が増減するトランザクショナルなグラフ(ソーシャルグラフの即時更新など)には、ポインタ連結リスト型の index-free adjacency のほうが向きます。RUM 的に言えば、CSR は読み(Read)と容量(Memory)に振り、更新(Update)を犠牲にした極です。
トラバーサル性能の原理的差
3方式の性質を、操作ごとに整理します。深い経路探索なら index-free adjacency、辺の存在判定や行列演算なら隣接行列、静的疎グラフの全体スキャンなら CSR、と適材が分かれます。
| 操作 | index-free adjacency | 隣接行列 | CSR |
|---|---|---|---|
| 隣接の列挙 | 次数に線形(ポインタ追跡) | 1行 V 個を走査 O(V) | 次数に線形(連続配列) |
| 辺 i→j の有無 | i の隣接を走査 | M[i][j] を定数時間 | row 範囲を二分探索 |
| メモリ量 | ノード+エッジ+ポインタ | V の2乗(疎で無駄大) | 頂点+辺で最小 |
| 動的更新 | ポインタ繋ぎ替えで局所的 | ビット反転で容易 | 配列シフトで高コスト |
| 局所性/キャッシュ | 良(隣接が近接配置) | 行内は良 | 極めて良(完全連続) |
「グラフDB が深いホップで RDB より速い理由」は index-free adjacency で答えます。RDB は毎ホップ O(log N) のインデックス引き、ネイティブは物理ポインタ追跡で N 非依存、K ホップで log の係数が累積するかしないかが分岐点です。「index-free でもインデックスを使う場面は」は起点ノードの特定時、と即答できると強い。「CSR の弱点」は動的更新(配列シフト)、「隣接行列の弱点」は疎グラフでの V の2乗メモリ、と一言で言い分けられるようにしておきます。
まとめ
グラフのトラバーサル性能は、結局「隣接をどう物理的に持つか」で決まります。index-free adjacency はノードに隣接エッジへの物理ポインタを直結し、隣を辿るのにグローバルインデックスを引かないので1ホップが N 非依存の定数時間で済み、深い探索ほど RDB のエッジ表方式(毎ホップ O(log N))に差をつけます。
一方、グラフ全体を解析するなら隣接行列(密・行列演算向き、V の2乗メモリ)や CSR(疎・静的・キャッシュ最良だが更新に弱い)が効きます。物理ポインタで関連データを直結する発想はヒープと TIDやクラスタ化インデックスの物理整列とも地続きで、「値で引くか、アドレスで飛ぶか」というストレージ設計の普遍的な選択が、グラフでは探索の計算量そのものを左右します。
データベース Article
グラフDBの内部:隣接リストとインデックスフリー隣接を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
グラフDB
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 5
導入後に効く点
RDB のテーブルでグラフを表すと、JOIN ごとにエッジ表の B+Tree インデックスを引く必要があり、1ホップが O(log N) かかる。K ホップで log の重みが K 回乗り、深い探索ほどネイティブ方式と差が開く。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 5
判断チェックリスト
- 自社の用途が「グラフDB / 隣接リスト」に近いか確認する。
- 強みである「ネイティブグラフDBは index-free adjacency を採り、各ノードのレコードに隣接エッジへの物理ポインタ(ファイルオフセット)を直接埋める。隣を辿るのにグローバルインデックスを引かないので、1ホップが定数時間で済む。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。