5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

SQLiteエンジンを自作時にDBファイル構造を調べたのでメモる

Last updated at Posted at 2023-06-29

CodeCrafters の Build Your Own SqLite コースに Ruby で取り組んだ。(完成品のコードはこちら)

実装時に、公式ドキュメントであるDatabase File Formatを読み、SQLiteのデータベースの構造(各ページがどのようなバイト配列になっているか等)を色々調べる必要があった。この際に調べたことをメモる。

Step1. Print page size (-> 今回は 4096 bytes)

SQLiteのDBファイルの最初の100byteは、ヘッダーである。
ヘッダーの中の、Offset=16~17の2bytesに、pagesizeが収納されている。
👉 今回見ているsample.dbのページサイズは、4096 bytesだった。

Step2. Print number of tables

SQLiteのDBファイルは、ページ(今回だと4096bytes)が並んでいる。ページの中身は以下の通り。

image.png

DBファイルの中の1ページ目は、sqlite_schemaテーブル(ref)のルートノードに相当する。
sqlite_schemaテーブルの各レコードは、そのDB内に入っている1テーブルに相当する。よって、このテーブル内のレコード数を求めれば良い。

(なお、1ページ目の最初の100bytesはファイルヘッダーなので、1ページ目のみ他のページよりも100bytesほど使用可能容量が少ない。)

SQLiteには、2種類のtreeがある。

  • table B-tree
  • index B-tree

各node=pageのoffset=1の1byteを見ることで、それが(table-btree/index-btree, interior-node/leaf-node) のどれに当たるかを知ることができる。

image.png

hexdummp -C sample.db (-Cオプションにより、1byteずつのbig endianとして解釈)により、今回のDBファイルを見てみると、、

image.png

これにより、1ページ目は、0x0d = 13 = leaf table b-tree pageであるので、sqlite_schemaテーブルのデータは、1ページ目に収まっていることがわかる。

よって、このページ内のレコードの数 = セルの数(leafであるため)なので、このページの offset=3~4の値を見れば良い。実際の実装はこちら

Step3: Print table names

ページ1に乗っかっているsqlite_schemaテーブルの中のレコードの一覧を取得すれば良い。
今Stepでは、sqlite_schemaテーブルの中のレコード数が少ないため、ページ1に全てのレコードが乗っかった。従って、

  • leaf page内でレコードたちを読む。-> 今Stepで実装
  • interior pageのポインターより、その子のpageを辿っていく -> Step8で実装

table B-tree leaf page は以下のように構成される。

image.png

よって、やることとしては以下。実際の実装はこちら

  • header内のoffset 3-4を読んで、cell数=record数を知る。
  • records(array)を初期し、headerの直後であるoffset=8から、#cell回、以下を繰り返す。
    • 2bytesのcell pointerを読む。
    • そのoffset=に飛ぶ。これはcellの先頭である。
    • payload sizeを示すvarintを一回読む。
    • rowidを示すvarintを一回読む。
    • col_to_serial_type(hashmap)を初期化し、カラムの個数回以下を繰り返す。
      • varintを読みこむ。それぞれが、各カラムの値の型とバイト長を示す。
      • これをcol_to_serial_type[col]に保存する。
    • record(hashmap)を初期化し、カラムの個数回以下を繰り返す。
      • col_to_serial_type[col]に入っているcol_to_serial_typeを元に、カラムの値を求める。
      • これをrecord[col]に代入する。
    • records(array)にrecordをappendする。

Step4: Count rows in a table

これは簡単。Step3で、TableBTreeTraverser#cnt_recordsを実装したのでこれを以下の順に使うだけ。実装はこちら

  • sqlite_schemaの中のテーブル一覧の中で該当するテーブルのレコードに注目し、root page indexを得る。
  • そのページインデックスを引数に、TableBTreeTraverser#cnt_recordsを走らせる。これで、テーブル内のレコード数が得られる。

Step5: Read data from a single column

これも簡単。Step3で、TableBTreeTraverser#get_recordsを実装したのでこれを以下の順に使うだけ。実装はこちら

  • sqlite_schemaの中のテーブル一覧の中で該当するテーブルのレコードに注目し、root page indexを得る。
  • そのページインデックスを引数に、TableBTreeTraverser#cnt_recordsを走らせる。これでテーブル内のレコードの配列が得られる。
  • このレコードの配列を、SELECTで指定されたカラムの値にmappingしてやる。

Step6: Read data from multiple columns

これはStep5の最後でmappingのロジックを変えるだけ。

そのページインデックスを引数に、TableBTreeTraverser#cnt_recordsを走らせる。これでテーブル内のレコードの配列が得られる。
このレコードの配列を、SELECTで指定されたカラムの値にmappingしてやる。

このmapping先を複数カラムにする。

Step7: Filter data with a WHERE clause

そのページインデックスを引数に、TableBTreeTraverser#cnt_recordsを走らせる。これでテーブル内のレコードの配列が得られる。

これに、WHERE clauseの条件を満たすレコードのみの集合を取り出すようフィルタリングする。

Step8: Retrieve data using a full-table scan

Step3では、sqlite_schemaテーブルの中のレコード数が少ないため、ページ1に全てのレコードが乗っかった。従って、

  • leaf page内でレコードたちを読む。-> 今Stepで実装
  • interior pageのポインターより、その子のpageを辿っていく -> Step8で実装

interior pageには、その子であるページのインデックスを持つポインタが複数含まれている。
これを元にleaf pageまで辿っていくロジックを実装すれば良い。

interior pageの構成は以下。

image.png

実装はこちら

Step9: Retrieve data using an index

WHERE clause で indexが貼られたカラムをフィルタリングするクエリが投げられる。
よって、index treeを使ってスキャンするレコードを絞ることが求められた。

具体的なステップは以下。

  1. インデックスツリーをtraverseし、WHERE clauseで指定された条件を満たすrowidの配列を求める。実装
  2. テーブルツリーをtraverseし、rowidの配列に含まれるrowidに相当するレコードのみを取り出す。実装

なお、index treeの leaf が持っている rowidとは、元のテーブルにおけるPrimary Keyのことである。
テーブルツリーは、primary keyによりノードがソートされているので、2のツリーのtraverseも高速にできる。

image.png

image.png

Ref.

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?