Transactional write の調査: SQLite wal 編

今回は、SQLite の wal モードについて調べた。

こちらもドキュメントが豊富で、以下の2つが参考になる。

wal モードは、redo log のみを使って永続化を行っている。 書き込みの時に以下のような動作を行う。

  • wal に frame を書き込む。COMMIT 時にはそれを示す値をヘッダフィールドに書く。
  • COMMIT 時に frame 数が規定値(デフォルトでは1000)を超えるか、自分以外のコネクションがいない時にデータベースを閉じる時に checkpoint を行う。

wal のレコードである frame は frame 固有のヘッダと、書き込むページデータからなる。 frame ヘッダには wal ヘッダに含まれる salt と、 wal ヘッダと frame ヘッダと自分以下の全 frame からなる checksum が書かれている。

frame は salt や checksum が正しいもののみ使用可能とみなされる。 wal は連続する frame を再生する必要があるので、連続した frame が全て壊れていないことを保証する必要がある。 そのため各 frame は自身の checksum だけでなく、これまでの frame を含めた checksum をつけているのだと思われる。

checkpoint 作業は wal に含まれる frame を適用することになるが、wal モードでは undo は行わない。 そのため、並行して走っている reader が見ているページを上書きしてはならない。 どこまで適用可能かという値は wal index に read mark として保存されている。

checkpoint が終われば wal header の checkpoint sequence や salt を更新する。 この更新で古い frame が適用されることを防ぐ。 また、他のコネクションがなければ wal の肥大化を防ぐために wal をリセットする。

wal モードでは checkpoint が起こるまでデータベースファイルのページが更新されない。 そのため reader は最新のページを wal から取得する必要があるが、当然スキャンを行うのはパフォーマンスが良くない。 これを解決するために、wal index が存在している。

wal index は標準の実装では memory mapped file を使っている。 index はクラッシュ時には再構築されるため、構造はドキュメントには詳しいことは記載されていない。

wal.c には WalIndexHdrWalCkptInfo 構造体が定義されていてなんとなく使い方がわかる。 index はハッシュテーブルを使っていて、キーに素数をかけてスロット数で割るという単純なものを使っている。 衝突時は前回の結果をインクリメントして同じアルゴリズムを適用する。