Let's 'C#で数値計算' with Mol & Dsl

Let's C#で数値計算: LightDB(軽量・高速な簡易データベース)

Mol は CPU に最適化されたインテル® MKLを 利用して、.Net 上での巨大行列や膨大な計測データ等を計算対象にしています。 LightDB クラスはそのような巨大データを効率的に扱うための軽量・高速な簡易型データベースです。 例題で示すような数十Gバイト以上のファイルサイズでも高速動作します。

多くのデータベースシステム(DBMS)では検索を高速に実行するため、データ本体とは別に、別途インデックスを作成します。
一般にインデックスは B-Tree という特別な構造(平衡多分木)を持ったファイルになります(詳細は B-Tree 等で検索してください)。
LightDB は多くの DBMS が採用している B-Tree 構造を持ったインデックス操作を .Net クラスとして組み込んだものです。

DBMS のインデックスはキーフィールドの値と、そのキーを含むレコードへの位置情報を管理します。
LightDB ではキーフィールドとデータレコードを一体化して管理します(別途管理することもできます)。

プログラミング手順

LightDB の詳細は後述するとして、プログラミングは大体以下のような手順になります。

[新規ファイルにキーとデータ(レコード)を追加]

     //
     // LightDB インスタンス作成:
     //    キー部分は int[] で配列サイズ 1 
     //    データ部分は double[] で配列サイズ 2 
     //
     LightDB<int, double> db = new LightDB<int, double>(1, 2);

     // データベースファイル "sample.ldb" を新規にオープン
     db.OpenNew("sample.ldb");

     // キーとデータをセットしてデータベースファイルに追加
     int    [] key  = new int   [1];
     double [] data = new double[2];
     while(....) 
     {
        // データベースに追加するキーとデータのセット
        key[0] = キー
        data[0] = データ1
        data[1] = データ2
        // キーとデータをデータベースファイルに追加する。
        db.AddRecord(key,data);
     }

     // ファイルのクローズ
     db.Close();

[既存ファイルからキーを指定してデータを検索]

     //
     // LightDB インスタンスの作成は、前例と同じ。
     //
     LightDB<int, double> db = new LightDB<int, double>(1, 2);

     // 既存データベースファイル "sample.ldb" をオープン
     db.Open("sample.ldb");

     // キーを指定して対応するデータを検索
     int    [] key  = new int   [1];
     double [] data = new double[2];
     while(....) 
     {
        // データベースから検索するキーのセット
        key[0] = キー
        // キー(key)を指定して対応するデータ(data)をデータベースファイルから読み込む。
        if(db.ReadRecord(key,data)==0)
        {
            // 読み込めた場合の処理
            データ1 = data[0];
            データ2 = data[1];
             ..............
        }
     }

     // ファイルのクローズ
     ldb.Close();

簡単例: BigMatrix と BigVector

プログラミング手順は前記した通りですが、ここでは、若干異なるインデクサを利用した 行列(BigMatrix)の具体例と実行結果を以下に示します。
※ LightDB はジェネリッククラスです。

	// メモリー上に作成できないような巨大行列を扱う例
        public class BigMatrix : LightDB<int, double>
        {
            public BigMatrix(string dbPath)
                : base(2, 1)
            {
                Open(dbPath);
            }
        };

	// BigVector は大抵の場合一次元配列で代用可能ですが、行列と同様に以下のように定義できます。
        public class BigVector : LightDB<int, double>
        {
            public BigVector(string dbPath)
                : base(1, 1)
            {
                Open(dbPath);
            }
        };

継承せずに、 LightDB A = new LightDB<int,double>(2,1); のように直接インスタンスを作成してもかまいません。 <int,double>(2,1) はキーフィールドが int でデータフィールドが double の配列で、サイズはそれぞれ 2 と 1 を意味します。 つまり、2個のキーフィールド [i,j] で1個の double 値 A[i,j] を検索する、連想記憶配列を作成することになります。 連想記憶配列を作成すれば、後は通常の行列やベクトルとまったく同じように使用可能です。 違いは配列サイズの制限が無いこと、[i,j] で検索できなかった場合は値 0 が返ることなどです。

 ※ 例はインデクサ(A[i,j]とあたかも配列のようにアクセスする方法)によるレコードアクセスですが、他にメソッドを使用したさまざまな機能が提供されています。
 ※ データベースファイル名(と拡張子)は任意です。

        public void Test()
	{
            int n = 50000;

            BigMatrix  A = new BigMatrix("A.ldb"); // 行列、A[i,j] 形式でアクセスする連想配列データベース。
            double []  b = new double[n];
            double []  c = new double[n];

            // データのセット(書込み、値は計算時間に無関係なので適当…)
            DateTime tm = DateTime.Now;
            for(int i=0;i < n;++i)
            {
                b[i] = n - i;
                for (int j = 0; j < n; ++j)
                {
                    A[i, j] = i + j;  // 配列要素値は "A.ldb" に保存されます。
                }
            }
            Console.WriteLine("書き込み時間=" + (DateTime.Now-tm));
            // 掛け算の計算(読み込み)
            tm = DateTime.Now;
            for (int i = 0; i < n; ++i)
            {
                double s = 0;;
                for (int j = 0; j < n; ++j)
                {
                    s += A[i, j] * b[j];  // A[i,j] 要素は "A.ldb" から読み込まれます。
                }
                c[i] = s;
            }
            Console.WriteLine("読み込み時間=" + (DateTime.Now - tm));

            A.Verify(false);
            A.Close();
	}
実際の計算結果は以下のようになります(Windows 7、Intel CORE i7-2600S 2.8GHz 8GBメモリ)。
  ※勿論、HDDの性能やウィルスソフトの関係などで結果は異なってきます。
  ※特にウィルスソフトの場合、ウィルス検出機能をON/OFFにすることでI/O時間が大幅に異なることがあります。
書き込み時間=01:32:04.1101026
読み込み時間=01:17:06.7549277

LightDb データベースファイルの情報
 LightDB version    = 1
 全ページ数         = 31250004
 ページバイトサイズ = 1952
 先頭削除ページ     = 0
 ファイルサイズ     = 61000007808
 キータイプ         = 6[SINT]
 キーバイトサイズ   = 8
 データタイプ      = 10[DOUBLE]
 データバイトサイズ = 8
 ルートページ       = 538089
 最大アイテム数     = 80

内部メモリー(キャッシュ)の検査

LightDB ファイル構造の検査
 有効レコード数  = 2500000000
 有効ページ数    = 31250003
 削除ページ数    = 0
 B-Tree の高さ   = 5
 レコード占有率  = 99.999990%
データーベースファイル(A.ldb)の大きさは約 60GB という巨大なサイズで、計算時間が約1時間半をどう捉えるかは 利用者次第と思います。しかし「Intel CORE i7-2600S 2.8GHz 8GBメモリ」のパソコンでは double で 50000 x 50000(要素数25億個) の2次元配列を扱うことはできません(32&64bitアプリケーション)。

1レコードの平均読み込み時間は ((60+17)*60+6)/(50000*50000) => 1.8504e-6 秒ということになります。

書込み時間と読み込み時間が同程度になっていますが、 例えばn = 10000;程度で計測すると、書込み時間が約3分で読み込み時間は約2分弱となります。

上記テストは 32bit アプリケーションで実行しましたが、メモリー自体の負荷は少ないので 64bit アプリケーションで実行しても 結果に大きな違いは出てきません。

結果の「レコード占有率 = 99.999990%」とは、各ページが格納しているアイテム(レコード)がほぼ100%であることを 意味します(後述の「LightDB ファイルの構造」を参照してください)。つまり、データベースに隙間が殆どないということです。

また「B-Tree の高さ = 5」は、キーの検索に必要なディスクアクセスが最大でも 5 回で済むことを意味します。

[BigMatrix使用例: 連立方程式の解法]

あくまでも比較用としてですが、LU 分解による連立方程式の解法ソースコードを用意しました(例題に追加してください)。
基本的に搭載メモリーで扱える行列なら LuSolver クラス等(圧倒的高速性能を有しています)を使用すべきです。 BigMatrix を使用する唯一の選択肢は、搭載メモリーで扱えない、巨大行列のケースです。 しかし、そのような巨大行列を現実的な時間(と精度)内で解くことに BigMatrix が最適であるとは言い難いのも事実です。


データの検証と全体スキャン

上記例のデータを全て(昇順に)スキャン(Scan())して検証するのは以下のように簡単です。
        public void Assert(bool f)
        {
            if (f) return;
            StackFrame CallStack = new StackFrame(1, true);
            string SourceFile = CallStack.GetFileName();
            int SourceLine = CallStack.GetFileLineNumber();
            MessageBox.Show("Assertion failed" + " File: " + SourceFile + " Line: " + SourceLine.ToString());

        }

        public bool EQ(double a, double b)
        {
            double eps = double.Epsilon * 2;
            if (Math.Abs(a) <= eps && Math.Abs(b) <= eps) return true;
            return (Math.Abs(a - b) / (Math.Abs(a) + Math.Abs(b))) <= eps;
        }

	public void Scan()
	{
            long n = 50000;
 
            BigMatrix A = new BigMatrix("A.ldb");

            DateTime tm = DateTime.Now;

            // データの検証
            int[]    ij  = new int[2];
            double[] Aij = new double[1];

            Assert(A.GetMinimumRecord(ij, Aij) == 0);
            long nr = 0;
            do
            {
                nr++;
                Assert(EQ(Aij[0], (double)(ij[0] + ij[1])));
            } while (A.GetNextRecord(ij, Aij) == 0);
            Assert(nr == n * n);
            Console.WriteLine("検証時間 = " + (DateTime.Now - tm));
            A.Dispose();
	}
結果は以下のようになります。

検証時間 = 00:40:31.6074709

因みに、1レコードに対する平均アクセス時間は (40*60+31)/(50000*50000) => 9.72E-7 秒になります。

LightDB のレコード構成

LightDB が扱うレコードは先頭にキーフィールドがあり、次にデータフィールド(共に配列)が続く単純な構造です。

レコード例: LightDB A = new LightDB<int,double>(n,m);
キーフィールド(要素数 n 個の int 配列)データフィールド(要素数 m 個の double 配列)
キーフィールドやデータフィールドには以下のタイプ型(の配列)を指定することができます(つまり、レコードはキーとデータの 2種類のデータ型から成ります)。
System名 別名 MOL_TYPE_LIGHT_DB名(値)意味
System.Byte byte UBYTE(1) 符号なし 8bit 整数
System.SByte sbyte SBYTE(2) 符号付き 8bit 整数
System.UInt16 ushort USHORT(3) 符号なし 16bit 整数
System.Int16 short SSHORT(4) 符号付き 16bit 整数
System.UInt32 uint UINT(5) 符号なし 32bit 整数
System.Int32 int SINT(6) 符号付き 32bit 整数
System.UInt64 ulong ULONG(7) 符号なし 64bit 整数
System.Int64 long SLONG(8) 符号付き 64bit 整数
System.Single float FLOAT(9) 単精度 32bit 実数
System.Double double DOUBLE(10) 倍精度 64bit 実数
System.Numerics.Complex Complex COMPLEX(11) 倍精度 (64x2)bit 複素数
System.String string STRING(12) ユニコード文字列
ここで「MOL_TYPE_LIGHT_DB名(値)」は LightDB のファイル内で使用される識別子名とその値で、プログラミング上は特に気にする必要はありません。
また、複素数の LightDB 内での扱いは、実数部と虚数部が繰り返す単純な「実数」配列となります。
同様に文字列は「符号なし 16bit 整数」配列として扱われます。

上記の例で n や m は、それぞれキーとデータの最大配列要素数を指定しています(ただし、文字列の場合は最大文字列長です)。 それに対して、実際の I/O 操作で追加するキーやデータの配列サイズが n や m に一致しない場合、以下の操作が適用されます。

キーの大小関係とレコード

データベース(ファイル)に追加されるレコードはキーの大小関係に応じて、常に B-Tree の構造を保つように、並べ替えられます(ソートされると考えても可です)。 キーは配列ですので先頭要素から順に比較され、不一致が生じたときの結果で大小関係が決まります。配列の最後まで一致した場合に2つのキーは同じと判定されます。 従って、データフィールドが長くなると並び替えサイズが大きくなるのでレコードの追加や削除の速度が遅くなります。 一般の DBMS はこの速度低下を防ぐために、データファイルとインデックス(B-Tree 構造を持つキーファイル)の2つで情報を管理します。 インデックスにはキーとキーに対応するデータレコード位置情報が格納されます。 LightDB でも、このような管理方法は、以下のように実現できます(キーフィールドは int、データレコードの位置とサイズは long と仮定します)。
    LightDB ix = new LightDB<int,long>(2,2);
    ix.Open("index.idx");                                         // インデックス "index.idx" のオープン
   FileStream df = new FileStream("data.dat",FileMode.Truncate); // データファイル "data.dat" のオープン
    int  [] Key  = new int [2];
    long [] Data = new long[2];  // データレコードの位置情報
    while(...)
    {
         … Key のセット      …
         … データレコードのセット …
         Data[0] = df.Position; // データレコードのファイル上の位置
         Data[1] = size;        // データレコードのサイズ
         df.Write(データレコードのバイト配列,0,size);
         ix.AddRecord(Key,Data);  // インデックスにキー(Key)とデータの位置情報(Data)を追加
    }
LightDB を使用するには、上記のような2つのファイルを管理するより、キーとデータを(連結したレコードで)一括して一つのファイルで管理するのが一般的です。 (レコードサイズが大きい場合は DBMS を使用して、計算に必要な部分を DBMS より抽出してから LightDB ファイルとして管理することが考えられます。)

LightDB 機能概要

以下、LightDB の主な機能一覧です。
名前説明
LightDB<K, D>(int ks, int ds)LightDBコンストラクター。K と D はキーとデータのタイプ。 ks と ds はキーとデータの最大配列サイズ。
static void PrintInfo(string stDbPath)LightDB ファイル情報をコンソール出力。
void OpenNew(string stDbPath)新規ファイルのオープン。ファイルが存在すればエラー。
void OpenRenew(string stDbPath)新規ファイルのオープン。既存ファイルは破壊されます。
void void Open(string stDbPath[, bool readOnly])ファイルのオープン。ファイルがなければ作成されます。readOnly=true なら書込み禁止になります。
void Flush()キャッシュされていてディスクに書き戻す必要のあるページを全てディスクに書き込みます。
void Close()オープンしたファイルのクローズ。
int AddRecord(K[] Key, D[] Data)Key と Data のレコード追加。
int ReadRecord(K[] Key, D[] Data)指定した Key に対応するデータ(Data)の読み込み。
int SetupRecord(K[] Key, D[] Data)指定した Key が無ければレコード追加、あれば Data のみを更新。
int ChangeRecord(K[] Key, D[] Data)指定した Key に対応するデータの更新。
int DeleteRecord(K[] Key)指定した Key に対応するレコードの削除。
int GetMinimumRecord(K[] Key, D[] Data)最少キーと対応するデータの読み込み。
int GetMaximumRecord(K[] Key, D[] Data)最大キーと対応するデータの読み込み。
int GetNextRecord(K[] Key, D[] Data)前回処理されたキーの次に大きいキーとデータの読み込み。
int GetPrevRecord(K[] Key, D[] Data)前回処理されたキーの次に小さいキーとデータの読み込み。
int RereadRecord(K[] Key, D[] Data)前回処理されたキーとデータの読み込み。
int FieldIndexインデクサ(やGetField()・SetField()メソッド)を利用してアクセスするデータ配列要素のインデックス(初期値は 0)。
D GetField(K[] Key)FieldIndex で指定されるデータ配列(データ配列を Data とすると D = Data[FieldIndex] )要素の取得。
int SetField(K[] Key, D d)FieldIndex で指定されるデータ配列要素の設定(データ配列を Data とすると Data[FieldIndex] = d )。
void Verify(bool detailed_output)オープンされている LightDB ファイルの検証。結果はコンソールに出力。
int UserAreaSizeヘッダ領域にユーザ固有の情報を保存できる領域のバイトサイズ。
byte[] UserAreaヘッダ領域のユーザ固有情報。
D this[K k] インデクサ。キー(の先頭要素)で検索したデータ配列の要素(Data[FieldIndex])。
D this[K k1,K k2] インデクサ。キー(の先頭要素と2番目の要素)で検索したデータ配列の要素(Data[FieldIndex])。
各メソッドの戻り値(がある場合)はゼロなら正常終了。ゼロ以外は(同じ値のキーが存在して、レコード追加に失敗した場合などの致命的ではない)エラーを示します。 戻り値の値と意味は以下の通りです。

プログラミングティップス

以下、LightDB を使用したプログラミングで、注意点等をリストアップしてみました。
※ Open()/Close() 処理は適宜終了しているものと仮定します。

配列サイズ

   LightDB ldb = new LightDB<int,double>(2,2); 
としたとき
   // 通常
   ldb.AddRecord(new int []{1,2},new double [] {1.0,2.0});       // キー {1,2}、データ {1.0,2.0} が追加される。
   // 配列が短い場合
   ldb.AddRecord(new int []{3},new double [] {3.0});             // キー {3,0}、データ {3.0,0.0} が追加される。
   // 配列が長い場合
   ldb.AddRecord(new int []{4,5,6},new double [] {4.0,5.0,6.0}); // キー {4,5}、データ {4.0,5.0} が追加される。
上記例はエラーにならず、コメントで記述されている結果になります。

インデクサ ( […] 形式でデータフィールドの配列要素にアクセスする方法、「…」はキー。)

   LightDB ldb = new LightDB<int,int>(1,3); 
   ldb.AddRecord(new int []{1},new int [] {1,2,3});  // キー {1}、データ {1,2,3} が追加される。
としたとき
   i = ldb[1];         // i==1   FieldIndex のデフォルト値は 0
   ldb.FieldIndex = 1; // データフィールド(配列)を Data[] とすると ldb[i] は i 番目のレコードの
                       // 1 番目の配列要素(Data[ldb.FieldIndex] )にアクセスすることになります。
   i = ldb[1];         // i==2
   ldb[1] = -2;        // データは {1,-2,3} となる。
   ldb[2] = 2;         // キー値 2 は存在しないのでレコード追加。 キー {2}、データ {0,2,0} が追加される。
   i = ldb[3];         // i==0、キー値 3 は存在しないので i には 0 がセットされる(データが文字列の場合 null が返ります)
上記例はエラーにならず、コメントで記述されている結果になります。

文字列

文字列の場合、コンストラクターの引数は文字列長で配列要素数ではありません。配列要素数は 1 固定となります。
   LightDB ldb = new LightDB<string,string>(2,2); 
としたとき
   // 通常
   ldb.AddRecord(new string []{"AB"},new string [] {"あい"});     // キー "AB"、データ "あい" が追加される。
   // 文字が短い場合
   ldb.AddRecord(new string []{"A"},new string [] {"あ"});        // キー "A\0"、データ "あ\0" が追加される(読み込み時、'\0'は削除されます)。
   // 文字が長い場合
   ldb.AddRecord(new string []{"ABC"},new string [] {"あいう");   // キー "AB"、データ "あい" が追加される。
   // 2以上の配列要素を指定した場合
   ldb.AddRecord(new string []{"AB","CD"},new string [] {"あい","うえ"});  // キー "AB"、データ "あい" が追加される(先頭配列要素のみ処理対象)。
上記例はエラーにならず、コメントで記述されている結果になります。

異なるタイプのデータを格納
LightDB はキーとデータで計2種類のデータしか格納できません。異なるタイプのデータを格納するにはバイト列や文字列に変換する方法があります。

            long n = 1000000;
            LightDB<long, string> ldb = new LightDB<long, string>(1, 40);
            ldb.OpenRenew("Test.ldb");
            DateTime dt = DateTime.Now;
            for (long i = 1; i <= n; ++i)
            {
                ldb[i] = DateTime.Now.ToString() +"," + (double)i;
            }
            T.P("書込み時間 = " + (DateTime.Now - dt));
            ldb.Close();
            ldb.Open("Test.ldb");
            dt = DateTime.Now;
            DateTime tm;
            for (long i = 1; i <= n; ++i)
            {
                string[] sts = ldb[i].Split(','); 
                Assert(DateTime.TryParse(sts[0],out tm));
                Assert(i==(long)double.Parse(sts[1]));
            }
            T.P("読込み時間 = " + (DateTime.Now - dt));
            ldb.Close();
結果は以下のようになります。
書込み時間 = 00:00:06.3648112
読込み時間 = 00:00:02.5272045
百万件でも、全データ読み込みが3秒弱で終了します。

昇順、降順アクセス

  // データを昇順にアクセスする場合
    ldb.GetMinimumRecord(key, data);
    do
    {
        .... 処理 .....
    } while (ldb.GetNextRecord(key, data) == 0);

  // データを降順にアクセスする場合
    ldb.GetMaximumRecord(key, data);
    do
    {
        .... 処理 .....
    } while (ldb.GetPrevRecord(key, data) == 0);

レコードポインター

LightDB は内部で最後に処理されたレコードの位置情報(レコードポインター)を管理しています。 追加(AddRecord())、変更(ChangeRecord())等の特定のレコードを処理した場合は、後に RereadRecord() メソッドで 処理対象のレコードに(再び)アクセスすることができます。また、前記「昇順、降順アクセス」例の GetNextRecord() は レコードポインターを1個進め(次にキー値の大きいレコードに移動し)てから、 GetPrevRecord() はレコードポインターを1個戻 し(次にキー値の小さいレコードに移動し)てから、その位置でのキーとデータを読み込みます。
※レコードの削除処理(DeleteRecord())後のレコードポインターは不定になります。

  // あるキー([v0,v1,...])より大きいレコードの処理
    key[0] = v0;
    key[1] = v1;
    ...........
    ldb.ReadRecord(key, data);
    do
    {
        .... 処理 .....
    } while (ldb.GetNextRecord(key, data) == 0);

  // あるキー([v0,v1,...])より小さいレコードの処理
    key[0] = v0;
    key[1] = v1;
    ...........
    ldb.ReadRecord(key, data);
    do
    {
        .... 処理 .....
    } while (ldb.GetPrevRecord(key, data) == 0);
※ReadRecord() や ChangeRecord() が失敗した場合(検索できなかった場合)、レコードポインターは 指定されたキーにいちばん近いレコード位置にセットされています。従って、GetNextRecord() や GetPrevRecord() で(検索できなかったキーの) 次や前のレコードを得ることは可能です。

LightDB ファイルの構造

以下、 LightDB ファイルの構造です。LightDB ファイルは B-Tree 構造の複雑なバイナリー形式です。 データ書き込み時の電源断といった障害でファイル内容に不整合が生じた場合、修復するのは殆ど不可能ですので、 何らかの形で全体を再構築できる手段(バックアップ等)を用意してください。
※ オープンした LightDB ファイルの整合性の検証(修復機能はありません)には Verify() メソッドを用いることができます。

ページ

LightDB ファイルは固定長の「ページ」単位で構成されます(ディスクアクセスはページ単位で実行されます)。 つまり、LightDB ファイルのサイズは「ページサイズ」の倍数になります。

LightDB ファイル: mp ... 最大ページ数
ページ 0ページ 1…………ページ mp-1
ページサイズは LightDB が決定します。
ページ 0 はヘッダーページ
ページ 1 ~ ページ mp-1 はデータページ
0 ~ mp-1 はページ番号。

ヘッダページ(ページ 0)

「ページ 0」は各種管理情報と(LightDBの関知しない)ユーザ固有情報領域に分かれます。

ヘッダページ
LightDB 管理情報領域ユーザ固有情報領域
ユーザ固有情報領域のサイズは UserAreaSize プロパティで得られます。ページ 0 の最後の UserAreaSize バイトがユーザ固有情報領域になります。 また、ユーザ固有情報領域への情報設定(読み込み/書込み)は UserArea プロパティで実行します。

LightDB 管理情報領域

管理情報サイズと説明
"LightDB\0"8バイト、ASCII文字列
バージョン番号8バイト
全ページ数(mp)8バイト
先頭削除ページ番号8バイト、削除ページの先頭 8 バイトは次の削除ページへのリンク(0は最終ページ)
1ページのバイトサイズ8バイト
予約領域8バイト
ルートページ番号8バイト、B-Tree構造のルートノード、検索などの開始点
キーのデータタイプ4バイト、MOL_TYPE_LIGHT_DBの値
キーのバイト長(ks)4バイト、キー部分の全バイトサイズ
データのデータタイプ4バイト、MOL_TYPE_LIGHT_DBの値
データのバイト長(ds)4バイト、データ部分の全バイトサイズ
1ページに格納できる最大アイテム数(mi)4バイト、アイテムについては後述
予約領域サイズは不定、以後ユーザ固有情報領域前までは予約
※1ページ内の最大アイテム数(mi)は LightDB が決定します。
※管理情報領域の内容は「static void PrintInfo(string stDbPath)」でコンソールに出力できます(stDbPath は LightDB ファイルのファイルパス)。

データページ

管理情報サイズと説明
自ページ番号8バイト、当該ページのページ番号(削除ページの場合、次の削除ページへのリンク)
親ページ番号8バイト、親の存在しないルートページの場合 0
左ページ番号8バイト、自ページ内の最少キーより小さいキー(レコード)から成るページの番号。 0 は左ページが存在しないという意味。
有効アイテム数(ni)8バイト
アイテム(1)アイテムサイズ( => レコードサイズ+8バイト)
アイテム(2)アイテムサイズ
…………………………
アイテム(mi)アイテムサイズ
※ mi/2 ≦ ni ≦ mi の条件が成立しています(ルートページを除く)。
※ 1 ~ ni までのアイテムはキーの大小関係に従って昇順にソートされています。
※ ni+1 ~ mi までのアイテムは未使用。内容は不定です。

アイテムとレコード

レコード(先頭にキーフィールド、次にデータフィールドを繋げたもの)
キーフィールド(ks バイト)データフィールド(ds バイト)
アイテム(レコードの先頭に「次のページ番号(右子ページ) 」8 バイトを付加したもの)
右子ページ番号(8 バイト)レコード( ks+ds バイト)

「右子ページ番号」を p 、p を含むアイテムの番号を i とすると
アイテム i のキー値 < 右子ページ p の全キー値 < アイテム i+1 のキー値 の関係が成立しています。
※ 右子ページ番号がゼロの場合、「右子ページ」は存在しない、つまり当該ページは「葉ページ」になります。