QDBMバージョン1仕様書

Copyright (C) 2000-2003 Mikio Hirabayashi
Last Update: Thu, 20 Feb 2003 21:45:58 +0900

目次

  1. 概要
  2. 機能詳細
  3. インストール
  4. Depot: 基本API
  5. Depot用コマンド
  6. Curia: 拡張API
  7. Curia用コマンド
  8. Relic: 互換API
  9. Relic用コマンド
  10. ファイルフォーマット
  11. バグ
  12. FAQ
  13. ライセンス

概要

QDBMはデータベースを扱うルーチン群のライブラリである。データベースといっても単純なものであり、キーと値のペアからなるレコードを格納したデータファイルである。キーと値は任意の長さを持つ一連のバイトであり、文字列でもバイナリでも扱うことができる。テーブルやデータ型の概念はない。キーはデータベース内で一意であり、キーが重複する複数のレコードを格納することはできない。

このデータベースに対しては、キーと値を指定してレコードを格納したり、キーを指定して対応するレコードを削除したり、キーを指定して対応するレコードを検索することができる。また、データベースに格納してある全てのキーを順不同に1つずつ取り出すこともできる。このような操作は、UNIX標準で定義されているDBMライブラリおよびその互換であるNDBMやGDBMに類するものである。QDBMはDBMのより良い代替として利用することができる。


機能詳細

QDBMはGDBMを参考に次の3点を目標として開発された。処理がより高速であること、データベースファイルがより小さいこと、APIがより単純であること。QDBMの特徴は、まさにそれらを達成していることである。また、DBMが抱える3つの制限事項を回避している。すなわち、プロセス内で複数のデータベースを扱うことができ、キーと値のサイズに制限がなく、データベースファイルがスパースでない。

QDBMはレコードの探索にハッシュアルゴリズムを用いる。バケット配列に十分な要素数があれば、レコードの探索にかかる時間計算量は O(1) である。すなわち、レコードの探索に必要な時間はデータベースの規模に関わらず一定である。追加や削除に関しても同様である。ハッシュ値の衝突はセパレートチェーン法で管理する。チェーンのデータ構造は二分探索木である。バケット配列の要素数が著しく少ない場合でも、探索等の時間計算量は O(log n) に抑えられる。

QDBMはバケット配列を全てRAM上に保持することによって、処理の高速化を図る。バケット配列がRAM上にあれば、ほぼ1パスのファイル操作でレコードに該当するファイル上の領域を参照することができる。ファイルに記録されたバケット配列はreadコールでRAM上に読み込むのではなく、mmapコールでRAMに直接マッピングされる。したがって、データベースに接続する際の準備時間が極めて短く、また、複数のプロセスでメモリマップを共有することができる。

バケット配列の要素数が格納するレコード数の半分ほどであれば、データの性質によって多少前後するが、ハッシュ値の衝突率は56.7%ほどである(等倍だと36.8%、2倍だと21.3%、4倍だと11.5%、8倍だと6.0%ほど)。その場合、平均2パス以下のファイル操作でレコードを探索することができる。これを性能指標とするならば、例えば100万個のレコードを格納するためには50万要素のバケット配列が求められる。バケット配列の各要素は4バイトである。すなわち、2MバイトのRAMが利用できれば100万レコードのデータベースが構築できる。

QDBMにはデータベースに接続する方法として、リーダとライタの2種類がある。リーダは読み込み専用であり、ライタは読み書き両用である。データベースにはファイルロックによってプロセス間での排他制御が行われる。ライタが接続している間は、他のプロセスはリーダとしてもライタとしても接続できない。リーダが接続している間は、他のプロセスのリーダは接続できるが、ライタは接続できない。この機構によって、マルチタスク環境での同時接続の整合性が保証される。ただし、マルチスレッドには対応しない。

DBMにはレコードの追加操作に関して挿入モードと置換モードがある。前者では、キーが既存のレコードと重複する際に既存の値を残す。後者では、キーが既存のレコードと重複した際に新しい値に置き換える。QDBMはその2つに加えて連結モードがある。既存の値の末尾に指定された値を連結して格納する操作である。レコードの値を配列として扱う場合、要素を追加するには連結モードが役に立つ。また、DBMではレコードの値を取り出す際にはその全ての領域を処理対象にするしか方法がないが、QDBMでは値の領域の一部のみを選択して取り出すことができる。レコードの値を配列として扱う場合にはこの機能も役に立つ。

データベースにアラインメントを設定すると、各レコードは適当なパディングを空けてファイル中に配置される。既存のキーと重複するレコードに既存の値のサイズよりも大きいサイズの値を書き込もうとした際に、追加分のサイズがパディングに収まれば、そのレコードの領域を別の位置に再確保する必要がない。レコードの位置を移す処理はサイズに応じた計算量がかかるが、パディングをレコードの大きさに応じたサイズでとることによって、レコードの規模に対する更新処理の性能が一定に保たれる。

QDBMのAPIには基本APIと拡張APIと互換APIの3種類がある。基本APIではファイルを単位としてデータベースを扱う。拡張APIではディレクトリを単位としてデータベースを扱う。データベースの全てのデータをひとつのファイルに格納しようとすると、ファイルサイズがファイルシステムの制限を越える場合がある。拡張APIではそれに対処すべく、ディレクトリの中の複数のファイルに分割してデータを格納する。基本APIと拡張APIは互いに酷似した書式を備えるので、一方のAPIを使って書かれたアプリケーションを他方のAPIを用いて書き直すことはたやすい。互換APIはNDBMのアプリケーションをQDBMに移植する際に便利である。さらに、QDBMは3種類のAPIに対応したコマンドラインインタフェースを備える。それらはユニットテストやデバッグなどで活躍する。それらを駆使すると、データベースアプリケーションのプロトタイプをシェル等のスクリプト言語で作ることも容易である。


インストール

QDBMをインストールするには gcc と make が必要である。

QDBMの配布用アーカイブファイルを展開したら、生成されたディレクトリに入ってインストール作業を行う。

ビルド環境を設定する。

./configure

プログラムをビルドする。

make

プログラムの自己診断テストを行う。

make check

プログラムをインストールする。作業は root ユーザで行う。

make install

一連の作業が終ると、ヘッダファイル depot.h と curia.h と relic.h が /usr/local/include に、ライブラリ libqdbm.a と libqdbm.so.0.0 が /usr/local/lib に、コマンド dpmgr と dptest と dptsv と crmgr と crtest と rlmgr と rltest が /usr/local/bin にインストールされる。なお、Windows(Cygwin)では、生成されるライブラリとそのインストール先が一般的なUNIX環境とは異なる。libqdbm.a と libqdbm.dll.a が /usr/local/lib に、qdbm.dll が $SYSTEMROOT/system32 にインストールされる。

QDBMをアンインストールするには、./configure をした後の状態で以下のコマンドを実行する。作業は root ユーザで行う。

make uninstall

QDBMの古いバージョンがインストールされている場合、それをアンインストールしてからインストール作業を行うべきである。


Depot: 基本API

DepotはQDBMの基本APIである。Depotを使うためには、depot.h と stdlib.h をインクルードすべきである。通常、ソースファイルの冒頭付近で以下の記述を行う。

#include <depot.h>
#include <stdlib.h>

Depotでデータベースを扱う際には、DEPOT型へのポインタをハンドルとして用いる。これは、stdio.h の各種ルーチンがファイル入出力にFILE型へのポインタを用いているのに似ている。ハンドルは、dpopen で開き、dpclose で閉じる。ハンドルのメンバを直接参照することは推奨されない。データベースに致命的なエラーが起きた場合は、以後そのハンドルに対する dpclose を除く全ての操作は何もせずにエラーを返す。

外部変数 dpdbgfd にはデバッグ情報を出力するファイルディスクリプタを指定する。初期値は -1 であり、値が負数ならば出力を行わない。

extern int dpdbgfd;

外部変数 dpecode には直前のエラーコードが記録される。初期値は DP_ENOERR である。DPECODE型の詳細については depot.h を参照されたい。

extern DPECODE dpecode;

エラーコードに対応するメッセージ文字列を得るには、関数 dperrmsg を用いる。

const char *dperrmsg(DPECODE ecode);
ecode はエラーコードを指定する。戻り値はエラーメッセージの文字列である。戻り値の領域は書き込み禁止領域である。

データベースのハンドルを作成するには、関数 dpopen を用いる。バケット配列の要素数はデータベースを作成する時に決められ、最適化以外の手段で変更することはできない。バケット配列の要素数は、格納するレコード数の半分から4倍程度にするのがよい。ライタ(読み書き両用モード)でデータベースファイルを開く際にはそのファイルに対して排他ロックがかけられ、リーダ(読み込み専用モード)で開く際には共有ロックがかけられる。その際には該当のロックがかけられるまでブロックする。

DEPOT *dpopen(const char *name, DPOMODE omode, int bnum);
name はデータベースファイルの名前を指定する。omode は接続モードを指定し、DP_OREADER ならリーダ、DP_OWRITER ならライタとなる。omode が DP_OWRITER の場合、DP_OCREAT または DP_OTRUNC とのビット論理和にすることができる。DP_OCREAT はファイルが無い場合に新規作成することを指示し、DP_OTRUNC はファイルが存在しても作り直すことを指示する。bnum はバケット配列の要素数の目安を指定するが、0 以下ならデフォルト値が使われる。戻り値は正常ならデータベースハンドルであり、エラーなら NULL である。

一旦接続したデータベースハンドルは、処理が完了したら関数 dpclose に渡してデータベースとの接続を閉じる必要がある。データベースの更新内容は、接続を閉じた時点で初めてファイルと同期される。ライタでデータベースを開いた場合、適切に閉じないとデータベースが破壊される。

int dpclose(DEPOT *depot);
depot はデータベースハンドルを指定する。戻り値は正常なら真であり、エラーなら偽である。閉じたハンドルの領域は解放されるので、以後は利用できなくなる。

レコードを追加するには、関数 dpput を用いる。

int dpput(DEPOT *depot, const char *kbuf, int ksiz, const char *vbuf, int vsiz, DPDMODE dmode);
depot はライタで接続したデータベースハンドルを指定する。kbuf はキーのデータ領域のポインタを指定する。ksiz はキーのデータ領域のサイズをバイト数で指定するか、負数なら kbuf の文字列長となる。vbuf は値のデータ領域のポインタを指定する。vsiz は値のデータ領域のサイズをバイト数で指定するか、-1 なら vbuf の文字列長となる。dmode は DP_DOVER か DP_DKEEP か DP_DCAT で、キーが既存レコードと重複した際の制御を指定する。DP_DOVER は既存のレコードの値を上書きし、DP_DKEEP は既存のレコードを残してエラーを返し、DP_DCAT は指定された値を既存の値の末尾に加える。戻り値は正常なら真であり、エラーなら偽である。

レコードを削除するには、関数 dpout を用いる。

int dpout(DEPOT *depot, const char *kbuf, int ksiz);
depot はライタで接続したデータベースハンドルを指定する。kbuf はキーのデータ領域のポインタを指定する。ksiz はキーのデータ領域のサイズをバイト数で指定するか、負数なら kbuf の文字列長となる。戻り値は正常なら真であり、エラーなら偽である。該当のレコードがない場合も偽を返す。

レコードを検索するには、関数 dpget を用いる。この関数の戻り値は malloc で確保された領域であり、不要になったら free で解放するべきである。

char *dpget(DEPOT *depot, const char *kbuf, int ksiz, int start, int max, int *sp);
depot はデータベースハンドルを指定する。kbuf はキーのデータ領域のポインタを指定する。ksiz はキーのデータ領域のサイズをバイト数で指定するか、負数なら kbuf の文字列長となる。start は値の領域から抽出する最初のバイトのオフセットを指定する。max は値の領域から抽出するサイズをバイト数で指定するか、負数なら無制限となる。sp が NULL でなければ、その参照先に抽出した領域のバイト数を格納する。戻り値は正常なら値を格納したアロケートした領域へのポインタ、エラーなら NULL である。該当のレコードがない場合も NULL を返す。取り出そうとした値のサイズが start より小さかった場合には該当とみなさない。戻り値の領域は、実際には1バイト多く確保して終端文字が置かれるので、文字列として利用できる。

レコードの値のサイズを取得するには、関数 dpvsiz を用いる。レコードの有無も調べられる。dpget と違って実データを読み込まないので効率がよい。

int dpvsiz(DEPOT *depot, const char *kbuf, int ksiz);
depot はデータベースハンドルを指定する。kbuf はキーのデータ領域のポインタを指定する。ksiz はキーのデータ領域のサイズをバイト数で指定するか、負数なら kbuf の文字列長となる。戻り値は該当レコードの値のバイト数であり、該当がなかったり、エラーの場合は -1 である。

データベースに格納された全てのレコードを参照するためのイテレータを初期化するには、関数 dpiterinit を用いる。

int dpiterinit(DEPOT *depot);
depot はデータベースハンドルを指定する。戻り値は正常なら真であり、エラーなら偽である。

イテレータを初期化してから関数 dpiternext を呼び出すことで、順序は制御できないが、全てのレコードを1回ずつ参照することができる。この関数の戻り値は malloc で確保された領域であり、不要になったら free で解放するべきである。

char *dpiternext(DEPOT *depot, int *sp);
depot はデータベースハンドルを指定する。sp が NULL でなければ、その参照先に抽出した領域のバイト数を格納する。戻り値は正常ならキーを格納したアロケートした領域へのポインタ、エラーなら NULL である。イテレータが最後まできて該当のレコードがない場合も NULL を返す。戻り値の領域は、実際には1バイト多く確保して終端文字が置かれるので、文字列として利用できる。

データベースのアラインメントを設定には、関数 dpsetalign を用いる。アラインメントを設定しておくと、レコードの上書きを頻繁にする場合の処理効率が良くなる。アラインメントの基本サイズは、一連の更新操作をした後の状態での標準的な値のサイズを指定するのがよい。アラインメントのユニットサイズは、基本サイズの4倍から16倍程度にするのがよい。

int dpsetalign(DEPOT *depot, int asiz, int aunit);
depot はライタで接続したデータベースハンドルを指定する。asiz はアラインメントの基本サイズを指定する。aunit はアラインメントのユニットサイズを指定する。戻り値は正常なら真であり、エラーなら偽である。レコードの値を格納する領域のサイズは、アラインメントの倍数で確保される。アラインメントは値のサイズがユニットサイズに達するごとに基本サイズを倍加して算出される。アラインメントの設定はデータベースに保存されないので、データベースを開く度に指定すること。

データベースを更新した内容をファイルとデバイスに同期させるには、関数 dpsync を用いる。データベースを閉じないうちに別プロセスにデータベースファイルを利用させる場合に役立つ。

int dpsync(DEPOT *depot);
depot はライタで接続したデータベースハンドルを指定する。戻り値は正常なら真であり、エラーなら偽である。

レコードを削除したり、置換モードや連結モードで書き込みを繰り返す場合は、データベース内に不要な領域が蓄積する。不要領域を削除してデータベースを最適化するには、関数 dpoptimize を用いる。

int dpoptimize(DEPOT *depot, int bnum);
depot はライタで接続したデータベースハンドルを指定する。bnum はバケット配列の新しい容量を指定するが、0 以下ならレコード数に最適な値が指定される。戻り値は正常なら真であり、エラーなら偽である。

データベースの名前を得るには、関数 dpname を用いる。

char *dpname(DEPOT *depot);
depot はデータベースハンドルを指定する。戻り値は正常なら値を格納したアロケートした領域へのポインタ、エラーなら NULL である。

データベースのファイルサイズを得るには、関数 dpfsiz を用いる。

int dpfsiz(DEPOT *depot);
depot はデータベースハンドルを指定する。戻り値は正常ならデータベースファイルのサイズ、エラーなら -1 である。

データベースのバケット配列の要素数を得るには、関数 dpbnum を用いる。

int dpbnum(DEPOT *depot);
depot はデータベースハンドルを指定する。戻り値は正常ならデータベースのバケットの要素数、エラーなら -1 である。

データベースのバケット配列の利用要素数を得るには、関数 dpbusenum を用いる。

int dpbusenum(DEPOT *depot);
depot はデータベースハンドルを指定する。戻り値は正常ならバケット配列の利用要素数、エラーなら -1 である。

データベースのレコード数を得るには、関数 dprnum を用いる。

int dprnum(DEPOT *depot);
depot はデータベースハンドルを指定する。戻り値は正常ならデータベースのレコード数、エラーなら -1 である。

データベースハンドルがライタかどうかを調べるには、関数 dpwritable を用いる。

int dpwritable(DEPOT *depot);
depot はデータベースハンドルを指定する。戻り値はライタなら真、そうでなければ偽である。

データベースファイルのファイルディスクリプタを得るには、関数 dpfdesc を用いる。ただし、ファイルディスクリプタを直接操ることは推奨されない。

int dpfdesc(DEPOT *depot);
depot はデータベースハンドルを指定する。戻り値はデータベースファイルのファイルディスクリプタである。

データベースの内部で用いるハッシュ関数として、dpinnerhash がある。アプリケーションがバケット配列の状態を予測する際に役立つ。

int dpinnerhash(const char *kbuf, int ksiz);
kbuf はキーのデータ領域のポインタを指定する。ksiz はキーのデータ領域のサイズをバイト数で指定するか、負数なら kbuf の文字列長となる。戻り値はキーから31ビット長のハッシュ値を算出した値である。

データベースの内部で用いるハッシュ関数と独立したハッシュ関数として、dpouterhash がある。アプリケーションがデータベースの更に上でハッシュアルゴリズムを利用する際に役に立つ。

int dpouterhash(const char *kbuf, int ksiz);
kbuf はキーのデータ領域のポインタを指定する。ksiz はキーのデータ領域のサイズをバイト数で指定するか、負数なら kbuf の文字列長となる。戻り値はキーから31ビット長のハッシュ値を算出した値である。

ある数以上の素数を得るには、関数 dpprimenum を用いる。バケット配列のサイズを決める場合に役立つ。

int dpprimenum(int num);
num は適当な正の数を指定する。戻り値は、指定した数と同じかより大きくかつなるべく小さい素数である。

Depotを利用したプログラムをビルドするには、ライブラリ libqdbm.a または libqdbm.so をリンク対象に加える必要がある。例えば、hoge.c から hoge を作るには、以下のようにビルドを行う。

gcc -I/usr/local/include -L/usr/local/lib -o hoge hoge.c -lqdbm

名前と対応させて電話番号を格納し、それを検索するアプリケーションのサンプルコードを以下に示す。

#include <depot.h>
#include <stdlib.h>
#include <stdio.h>

#define NAME     "mikio"
#define NUMBER   "000-1234-5678"
#define DBNAME   "book"

/* 関数プロトタイプ */
int main(int argc, char **argv);

/* メインルーチン */
int main(int argc, char **argv){
  DEPOT *depot;
  char *val;

  /* データベースを開く */
  if(!(depot = dpopen(DBNAME, DP_OWRITER | DP_OCREAT, -1))){
    fprintf(stderr, "%s\n", dperrmsg(dpecode));
    exit(1);
  }

  /* レコードを格納する */
  if(!dpput(depot, NAME, -1, NUMBER, -1, DP_DOVER)){
    fprintf(stderr, "%s\n", dperrmsg(dpecode));
  }

  /* レコードを検索する */
  if(!(val = dpget(depot, NAME, -1, 0, -1, NULL))){
    fprintf(stderr, "%s\n", dperrmsg(dpecode));
  } else {
    printf("Name: %s\n", NAME);
    printf("Number: %s\n", val);
    free(val);
  }

  /* データベースを閉じる */
  if(!dpclose(depot)){
    fprintf(stderr, "%s\n", dperrmsg(dpecode));
    exit(1);
  }

  return 0;
}

Depot用コマンド

Depotに対応するコマンドラインインタフェースは以下のものである。

dpmgr はDepotやそのアプリケーションのデバッグに役立つツールである。データベースを更新したり、状態を調べる機能を持つ。シェルスクリプトでデータベースアプリケーションを作るのにも利用できる。以下の書式で用いる。name はファイル名、key はレコードのキー、val はレコードの値を指定する。

データベースファイルを作成する。
dpmgr create [-v] [-bnum num] name
キーと値に対応するレコードを追加する。
dpmgr put [-v] [-kx] [-vx] [-keep] [-cat] name key val
キーに対応するレコードを削除する。
dpmgr out [-v] [-kx] name key
キーに対応するレコードの値を取得して標準出力する。
dpmgr get [-v] [-kx] [-start num] [-max num] [-ox] [-n] name key
データベース内の全てのレコードのキーを改行で区切って標準出力する。
dpmgr list [-v] [-ox] name
データベースを最適化する。
dpmgr optimize [-v] [-bnum num] name
データベースの雑多な情報を出力する。
dpmgr inform [-v] name
QDBMのバージョン情報を標準出力する。
dpmgr version
各オプションは以下の機能を持つ。
-v : デバッグ情報を出力する。
-bnum num : バケット配列の要素数を num に指定する。
-kx : 2桁単位の16進数によるバイナリ表現として key を扱う。
-vx : 2桁単位の16進数によるバイナリ表現として val を扱う。
-keep : 既存のレコードとキーが重複時に上書きせずにエラーにする。
-cat : 既存のレコードとキーが重複時に値を末尾に追加する。
-start : 値から取り出すデータの開始オフセットを指定する。
-max : 値から取り出すデータの最大の長さを指定する。
-ox : 2桁単位の16進数によるバイナリ表現として標準出力を行う。
-n : 標準出力の末尾に付加される改行文字の出力を抑制する。

dpmgr は処理が正常に終了すれば 0 を返し、エラーがあればそれ以外の値を返して終了する。

dptest はDepotの機能テストや性能テストに用いるツールである。dptest によって生成されたデータベースファイルを dpmgr によって解析したり、time コマンドによって dptest の実行時間を計るとよい。以下の書式で用いる。name はファイル名、rnum はレコード数、bnum はバケット配列の要素数を指定する。

「00000001」「00000002」のように変化する8バイトのキーと適当な8バイトの値を連続してデータベースに追加する。
dptest write [-cat num] [-asiz num] [-aunit aunit] name rnum bnum
上記で生成したデータベースの全レコードを検索する。
dptest read name
各種操作の組み合わせテストを行う。
dptest combo name
無作為な長さの無作為なデータを連続してデータベースに追加する。
dptest rand [-cat num] [-asiz num] [-aunit aunit] name rnum bnum
各オプションは以下の機能を持つ。
-cat num : num の回数だけ処理を繰り返してキーの重複を起こし、連結モードで処理する。
-asiz num : アラインメントの基本サイズを num 指定する。
-aunit num : アラインメントのユニットサイズを num に指定する。

dptest は処理が正常に終了すれば 0 を返し、エラーがあればそれ以外の値を返して終了する。

dptsv はタブ区切りでキーと値を表現した行からなるTSVファイルとDepotのデータベースを相互変換する。以下の書式で用いる。name はデータベース名を指定する。export サブコマンドではTSVのデータは標準入力から読み込む。キーが重複するレコードは後者を優先する。-bnum オプションの num はバケット配列の要素数を指定する。import サブコマンドではTSVのデータが標準出力に書き出される。

TSVファイルを読み込んでデータベースを作成する。
dptsv import [-bnum num] name
データベースの全てのレコードをTSVファイルとして出力する。
dptsv export name

dptsv は処理が正常に終了すれば 0 を返し、エラーがあればそれ以外の値を返して終了する。


Curia: 拡張API

CuriaはQDBMの拡張APIであり、複数のデータベースファイルをディレクトリで一括して扱う機能を提供する。データベースを複数のファイルに分割することで、ファイルシステムによるファイルサイズの制限を回避することができる。また、複数のデバイスにファイルを分散させることで、スケーラビリティが向上する。

Depotではファイル名を指定してデータベースを構築するが、Curiaではディレクトリ名を指定してデータベースを構築する。指定したディレクトリの下には、depot という名前のデータベースファイルが生成される。これはディレクトリの属性を保持するもので、レコードの実データは格納されない。それとは別に、データベースを分割した個数だけ、4桁の十進数値の名前を持つサブディレクトリが生成され、各々のサブディレクトリの中には depot という名前でデータベースファイルが生成される。レコードの実データはそれらに格納される。例えば、hoge という名前のデータベースを作成し、分割数を3にする場合、hoge/depot 、hoge/0001/depot 、hoge/0002/depot 、hoge/0003/depot が生成される。データベースを作成する際にすでにディレクトリが存在していてもエラーとはならない。したがって、予めサブディレクトリを生成しておいて、各々に異なるデバイスのファイルシステムをマウントしておけば、データベースファイルを複数のデバイスに分割して格納することができる。NFS等を利用すれば複数のファイルサーバにデータベースファイルを分散させることもできる。

Curiaを使うためには、depot.h と curia.h と stdlib.h をインクルードすべきである。通常、ソースファイルの冒頭付近で以下の記述を行う。

#include <depot.h>
#include <curia.h>
#include <stdlib.h>

Curiaでデータベースを扱う際には、CURIA型へのポインタをハンドルとして用いる。ハンドルは、cropen で開き、crclose で閉じる。ハンドルのメンバを直接参照することは推奨されない。

CuriaでもDepotと同じく外部変数 dpecode に直前のエラーコードが記録される。エラーコードに対応するメッセージ文字列を得るには、関数 dperrmsg を用いる。

データベースのハンドルを作成するには、関数 cropen を用いる。バケット配列の要素数はデータベースを作成する時に決められ、最適化以外の手段で変更することはできない。データベースファイルの分割数はデータベースを作成する時に指定したものから変更することはできない。バケット配列の要素数は、格納するレコード数の半分から4倍程度にするのがよい。データベースファイルの分割数の最大値は 256 個である。ライタ(読み書き両用モード)でデータベースファイルを開く際にはそのファイルに対して排他ロックがかけられ、リーダ(読み込み専用モード)で開く際には共有ロックがかけられる。その際には該当のロックがかけられるまでブロックする。

CURIA *cropen(const char *name, CROMODE omode, int bnum, int dnum);
name はデータベースディレクトリの名前を指定する。omode は接続モードを指定し、CR_OREADER ならリーダ、CR_OWRITER ならライタとなる。omode が CR_OWRITER の場合、CR_OCREAT または CR_OTRUNC とのビット論理和にすることができる。CR_OCREAT はファイルが無い場合に新規作成することを指示し、CR_OTRUNC はファイルが存在しても作り直すことを指示する。bnum はバケット配列の要素数の目安を指定するが、0 以下ならデフォルト値が使われる。dnum は要素データベースの数を指定するが、0 以下ならデフォルト値が使われる。戻り値は正常ならデータベースハンドルであり、エラーなら NULL である。

一旦接続したデータベースハンドルは、処理が完了したら関数 crclose に渡してデータベースとの接続を閉じる必要がある。データベースの更新内容は、接続を閉じた時点で初めてファイルと同期される。ライタでデータベースを開いた場合、適切に閉じないとデータベースが破壊される。

int crclose(CURIA *curia);
curia はデータベースハンドルを指定する。戻り値は正常なら真であり、エラーなら偽である。閉じたハンドルの領域は解放されるので、以後は利用することができなくなる。

Curiaにはその他に以下の関数がある。これらの振舞いはDepotの対応する関数と同様なので、そちらを参照されたい。Depotの各関数の名前の接頭辞 dp を cr に読み変えれば対応がとれる。ただし、dperrmsg と dpinnerhash と dpouterhash と dpprimenum に対応する関数はCuriaにはない。

int crput(CURIA *curia, const char *kbuf, int ksiz, const char *vbuf, int vsiz, CRDMODE dmode);
int crout(CURIA *curia, const char *kbuf, int ksiz);
char *crget(CURIA *curia, const char *kbuf, int ksiz, int start, int max, int *sp);
int crvsiz(CURIA *curia, const char *kbuf, int ksiz);
int criterinit(CURIA *curia);
char *criternext(CURIA *curia, int *sp);
int crsetalign(CURIA *curia, int asiz, int aunit);
int crsync(CURIA *curia);
int croptimize(CURIA *curia, int bnum);
char *crname(CURIA *curia);
int crfsiz(CURIA *curia);
int crbnum(CURIA *curia);
int crbusenum(CURIA *curia);
int crrnum(CURIA *curia);

Curiaにはラージオブジェクトを扱う機能がある。通常のレコードのデータはデータベースファイルに格納されるが、ラージオブジェクトのレコードのデータは個別のファイルに格納される。ラージオブジェクトのファイルはハッシュ値を元にディレクトリに分けて格納されるので、通常のレコードには劣るが、それなりの速度で参照できる。サイズが大きく参照頻度が低いデータは、ラージオブジェクトとしてデータベースファイルから分離すべきである。そうすれば、通常のレコードに対する処理速度が向上する。ラージオブジェクトのディレクトリ階層はデータベースファイルが格納されるサブディレクトリの中の lob という名前のディレクトリの中に作られる。通常のデータベースとラージオブジェクトのデータベースはキー空間が異なり、互いに干渉することはない。関数 crfsiz 、crbnum 、crbusenum 、crrnum の値にはラージオブジェクトのレコードは反映されない。

ラージオブジェクト用データベースにレコードを追加するには、関数 crputlob を用いる。

int crputlob(CURIA *curia, const char *kbuf, int ksiz, const char *vbuf, int vsiz, CRDMODE dmode);
curia はライタで接続したデータベースハンドルを指定する。kbuf はキーのデータ領域のポインタを指定する。ksiz はキーのデータ領域のサイズをバイト数で指定するか、負数なら kbuf の文字列長となる。vbuf は値のデータ領域のポインタを指定する。vsiz は値のデータ領域のサイズをバイト数で指定するか、-1 なら vbuf の文字列長となる。dmode は CR_DOVER か CR_DKEEP か CR_DCAT で、キーが既存レコードと重複した際の制御を指定する。CR_DOVER は既存のレコードの値を上書きし、CR_DKEEP は既存のレコードを残してエラーを返し、CR_DCAT は指定された値を既存の値の末尾に加える。戻り値は正常なら真であり、エラーなら偽である。通常のデータベースとラージオブジェクト用のデータベースはキー空間が異なり、ラージオブジェクトの追加操作によって通常のレコードに上書きや追加が起こることはない。

ラージオブジェクト用データベースからレコードを削除するには、関数 croutlob を用いる。

int croutlob(CURIA *curia, const char *kbuf, int ksiz);
curia はライタで接続したデータベースハンドルを指定する。kbuf はキーのデータ領域のポインタを指定する。ksiz はキーのデータ領域のサイズをバイト数で指定するか、負数なら kbuf の文字列長となる。戻り値は正常なら真であり、エラーなら偽である。該当のレコードがない場合も偽を返す。通常のデータベースとラージオブジェクト用のデータベースはキー空間が異なり、ラージオブジェクトの削除操作によって通常のレコードの削除が起こることはない。

ラージオブジェクト用データベースからレコードの値を抽出するには、関数 crgetlob を用いる。

char *crgetlob(CURIA *curia, const char *kbuf, int ksiz, int start, int max, int *sp);
curia はデータベースハンドルを指定する。kbuf はキーのデータ領域のポインタを指定する。ksiz はキーのデータ領域のサイズをバイト数で指定するか、負数なら kbuf の文字列長となる。start は値の領域から抽出する最初のバイトのオフセットを指定する。max は値の領域から抽出するサイズをバイト数で指定するか、負数なら無制限となる。sp が NULL でなければ、その参照先に抽出した領域のバイト数を格納する。戻り値は正常なら値を格納したアロケートした領域へのポインタ、エラーなら NULL である。該当のレコードがない場合も NULL を返す。取り出そうとした値のサイズが start より小さかった場合には該当とみなさない。戻り値の領域は、実際には1バイト多く確保して終端文字が置かれるので、文字列として利用できる。通常のデータベースとラージオブジェクト用のデータベースはキー空間が異なり、ラージオブジェクトの検索操作によって通常のレコードが該当することはない。

ラージオブジェクト用データベースにあるレコードの値のサイズを取得するには、関数 crvsizlob を用いる。

int crvsizlob(CURIA *curia, const char *kbuf, int ksiz);
curia はデータベースハンドルを指定する。kbuf はキーのデータ領域のポインタを指定する。ksiz はキーのデータ領域のサイズをバイト数で指定するか、負数なら kbuf の文字列長となる。戻り値は該当レコードの値のバイト数であり、該当がなかったり、エラーの場合は -1 である。通常のデータベースとラージオブジェクト用のデータベースはキー空間が異なり、ラージオブジェクトの検索操作によって通常のレコードが該当することはない。

ラージオブジェクト用データベースのレコード数を得るには、関数 crrnumlob を用いる。

int crrnumlob(CURIA *curia);
curia はデータベースハンドルを指定する。戻り値は正常ならレコード数、エラーなら -1 である。

Curiaを利用したプログラムをビルドする方法は、Depotの場合と全く同じである。


Curia用コマンド

Curiaに対応するコマンドラインインタフェースは以下のものである。

crmgr はCuriaやそのアプリケーションのデバッグに役立つツールである。データベースを更新したり、状態を調べる機能を持つ。シェルスクリプトでデータベースアプリケーションを作るのにも利用できる。以下の書式で用いる。name はディレクトリ名、key はレコードのキー、val はレコードの値を指定する。

データベースディレクトリを作成する。
crmgr create [-v] [-bnum num] [-dnum num] name
キーと値に対応するレコードを追加する。
crmgr put [-v] [-kx] [-vx] [-keep] [-cat] [-lob] name key val
キーに対応するレコードを削除する。
crmgr out [-v] [-kx] [-lob] name key
キーに対応するレコードの値を取得して標準出力する。
crmgr get [-v] [-kx] [-start num] [-max num] [-ox] [-lob] [-n] name key
データベース内の全てのレコードのキーを改行で区切って標準出力する。
crmgr list [-v] [-ox] name
データベースを最適化する。
crmgr optimize [-v] [-bnum num] name
データベースの雑多な情報を出力する。
crmgr inform [-v] name
QDBMのバージョン情報を標準出力する。
crmgr version
各オプションは以下の機能を持つ。
-v : デバッグ情報を出力する。
-bnum num : バケット配列の要素数を num に指定する。
-dnum num : データベースファイルの分割数を num に指定する。
-kx : 2桁単位の16進数によるバイナリ表現として key を扱う。
-vx : 2桁単位の16進数によるバイナリ表現として val を扱う。
-keep : 既存のレコードとキーが重複時に上書きせずにエラーにする。
-cat : 既存のレコードとキーが重複時に値を末尾に追加する。
-start : 値から取り出すデータの開始オフセットを指定する。
-max : 値から取り出すデータの最大の長さを指定する。
-ox : 2桁単位の16進数によるバイナリ表現として標準出力を行う。
-lob : ラージオブジェクトを扱う。
-n : 標準出力の末尾に付加される改行文字の出力を抑制する。

crmgr は処理が正常に終了すれば 0 を返し、エラーがあればそれ以外の値を返して終了する。

crtest はCuriaの機能テストや性能テストに用いるツールである。crtest によって生成されたデータベースディレクトリを crmgr によって解析したり、time コマンドによって crtest の実行時間を計るとよい。以下の書式で用いる。name はディレクトリ名、rnum はレコード数、bnum はバケット配列の要素数を指定する。

「00000001」「00000002」のように変化する8バイトのキーと適当な8バイトの値を連続してデータベースに追加する。
crtest write [-cat num] [-asiz num] [-aunit aunit] [-lob] name rnum bnum
上記で生成したデータベースの全レコードを検索する。
crtest read [-lob] name
各種操作の組み合わせテストを行う。
crtest combo name
各オプションは以下の機能を持つ。
-cat num : num の回数だけ処理を繰り返してキーの重複を起こし、連結モードで処理する。
-asiz num : アラインメントの基本サイズを num 指定する。
-aunit num : アラインメントのユニットサイズを num に指定する。
-lob : ラージオブジェクトを扱う。

crtest は処理が正常に終了すれば 0 を返し、エラーがあればそれ以外の値を返して終了する。


Relic: 互換API

Relicは、NDBMと互換するAPIである。すなわち、Depotの関数群をNDBMのAPIで包んだものである。NDBMのアプリケーションをRelicを使ってQDBMに移植するのはたやすい。ほとんどの場合、インクルードするヘッダファイルを ndbm.h から relic.h に代えるだけでよい(ndbm.h を relic.h へのリンクにしてもよい)。

オリジナルのNDBMでは、データベースは2つのファイルの対からなる。ひとつは接尾辞に .dir がつく名前で、キーのビットマップを格納する。もうひとつは接尾辞に .pag がつく名前で、データの実体を格納する。Relicでは .dir のファイルは単なるダミーとして作成し、.pag のファイルをデータベースとする。RelicではオリジナルのNDBMと違い、格納するデータのサイズに制限はない。オリジナルのNDBMで生成したデータベースファイルをRelicで扱うことはできない。

Relicを使うためには、relic.h と sys/types.h と sys/stat.h と fcntl.h をインクルードすべきである。通常、ソースファイルの冒頭付近で以下の記述を行う。

#include <relic.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

Relicでデータベースを扱う際には、DBM型へのポインタをハンドルとして用いる。ハンドルは、dbm_open で開き、dbm_close で閉じる。ハンドルのメンバを直接参照することは推奨されない。

データの格納、削除、検索に用いる関数とのデータの授受には、キーと値を表現するのにdatum型の構造体を用いる。

typedef struct { void *dptr; size_t dsize; } datum;
dptr はデータ領域へのポインタである。dsize はデータ領域の長さである。

データベースのハンドルを作成するには、関数 dbm_open を用いる。

DBM *dbm_open(char *name, int flags, int mode);
name はデータベースの名前を指定するが、ファイル名はそれに接尾辞をつけたものになる。flags は open コールに渡すものと同じだが、O_WRONLY は O_RDWR と同じになり、追加フラグでは O_CREAT と O_TRUNC のみが有効である。mode は open コールに渡すものと同じでファイルのモードを指定する。戻り値は正常ならデータベースハンドルであり、エラーなら NULL である。

一旦接続したデータベースハンドルは、処理が完了したら関数 dbm_close に渡してデータベースとの接続を閉じる必要がある。

void dbm_close(DBM *db);
db はデータベースハンドルを指定する。閉じたハンドルの領域は解放されるので、以後は利用することができなくなる。

レコードを追加するには、関数 dbm_store を用いる。

int dbm_store(DBM *db, datum key, datum content, int flags);
db はデータベースハンドルを指定する。key はキーの構造体を指定する。content は値の構造体を指定する。frags が DBM_INSERT ならキーの重複時に書き込みを断念し、DBM_REPLACE なら上書きを行う。戻り値は正常なら 0 であり、重複での断念なら 1 であり、その他のエラーなら -1 である。

レコードを削除するには、関数 dbm_delete を用いる。

int dbm_delete(DBM *db, datum key);
db はデータベースハンドルを指定する。key はキーの構造体を指定する。戻り値は正常なら 0 であり、エラーなら -1 である。

レコードを検索するには、関数 dbm_fetch を用いる。

datum dbm_fetch(DBM *db, datum key);
db はデータベースハンドルを指定する。key はキーの構造体を指定する。戻り値は値の構造体である。該当があればメンバ dptr がその領域を指し、dsize がサイズを示す。該当がなければ dptr が NULL となる。dptr の指す領域はハンドルに関連づけられて確保され、同じハンドルに対して次にこの関数を呼び出すか、ハンドルを閉じるまで、有効なデータを保持する。戻り値の領域の1バイト後には終端文字が置かれるので、文字列として利用できる。

データベースに格納された最初のキーを得るには、関数 dbm_firstkey を用いる。レコードの順番は不定である。

datum dbm_firstkey(DBM *db);
db はデータベースハンドルを指定する。戻り値は値の構造体である。該当があればメンバ dptr がその領域を指し、dsize がサイズを示す。該当がなければ dptr が NULL となる。dptr の指す領域はハンドルに関連づけられて確保され、同じハンドルに対して次にこの関数を呼び出すか、ハンドルを閉じるまで、有効なデータを保持する。戻り値の領域の1バイト後には終端文字が置かれるので、文字列として利用できる。

データベースに格納された次のキーを得るには、関数 dbm_nextkey を用いる。レコードの順番は不定である。

datum dbm_nextkey(DBM *db);
db はデータベースハンドルを指定する。戻り値は値の構造体である。該当があればメンバ dptr がその領域を指し、dsize がサイズを示す。該当がなければ dptr が NULL となる。dptr の指す領域はハンドルに関連づけられて確保され、同じハンドルに対して次にこの関数を呼び出すか、ハンドルを閉じるまで、有効なデータを保持する。戻り値の領域の1バイト後には終端文字が置かれるので、文字列として利用できる。

関数 dbm_error は何もしない。

int dbm_error(DBM *db);
db はデータベースハンドルを指定する。戻り値は 0 である。この関数は互換性のためにのみ存在する。

関数 dbm_clearerr は何もしない。

int dbm_clearerr(DBM *db);
db はデータベースハンドルを指定する。戻り値は 0 である。この関数は互換性のためにのみ存在する。

データベースが読み込み専用かどうかを調べるには、関数 dbm_rdonly を用いる。ただし、ファイルディスクリプタを直接操ることは推奨されない。

int dbm_rdonly(DBM *db);
db はデータベースハンドルを指定する。戻り値は読み込み専用なら真、そうでなければ偽である。

ディレクトリファイルのファイルディスクリプタを得るには、関数 dbm_dirfno を用いる。ただし、データベースファイルをを直接操ることは推奨されない。

int dbm_dirfno(DBM *db);
db はデータベースハンドルを指定する。戻り値はディレクトリファイルのファイルディスクリプタである。

データファイルのファイルディスクリプタを得るには、関数 dbm_pagfno を用いる。ただし、データベースファイルをを直接操ることは推奨されない。

int dbm_pagfno(DBM *db);
db はデータベースハンドルを指定する。戻り値はデータファイルのファイルディスクリプタである。

Relicを利用したプログラムをビルドする方法は、Depotの場合と全く同じである。リンカに渡すオプションは -lndbm ではなく -lqdbm である。


Relic用コマンド

Relicに対応するコマンドラインインタフェースは以下のものである。

rlmgr はRelicやそのアプリケーションのデバッグに役立つツールである。データベースを更新したり、状態を調べる機能を持つ。シェルスクリプトでデータベースアプリケーションを作るのにも利用できる。以下の書式で用いる。name はデータベース名、key はレコードのキー、val はレコードの値を指定する。

データベースファイルを作成する。
rlmgr create name
キーと値に対応するレコードを追加する。
rlmgr store [-kx] [-vx] [-insert] name key val
キーに対応するレコードを削除する。
rlmgr delete [-kx] name key
キーに対応するレコードの値を取得して標準出力する。
rlmgr fetch [-kx] [-ox] [-n] name key
データベース内の全てのレコードのキーを改行で区切って標準出力する。
rlmgr list [-ox] name
各オプションは以下の機能を持つ。
-kx : 2桁単位の16進数によるバイナリ表現として key を扱う。
-vx : 2桁単位の16進数によるバイナリ表現として val を扱う。
-insert : 既存のレコードとキーが重複時に上書きせずにエラーにする。
-ox : 2桁単位の16進数によるバイナリ表現として標準出力を行う。
-n : 標準出力の末尾に付加される改行文字の出力を抑制する。

rlmgr は処理が正常に終了すれば 0 を返し、エラーがあればそれ以外の値を返して終了する。

rltest はRelicの機能テストや性能テストに用いるツールである。rltest によって生成されたデータベースファイルを rlmgr によって解析したり、time コマンドによって rltest の実行時間を計るとよい。以下の書式で用いる。name はファイル名、rnum はレコード数を指定する。

「00000001」「00000002」のように変化する8バイトのキーと適当な8バイトの値を連続してデータベースに追加する。
rltest write name rnum
上記で生成したデータベースを検索する。
rltest read name rnum

rltest は処理が正常に終了すれば 0 を返し、エラーがあればそれ以外の値を返して終了する。


ファイルフォーマット

Depotが管理するデータベースファイルの内容は、ヘッダ部、バケット部、レコード部の3つに大別される。

ヘッダ部はファイルの先頭から 48 バイトの固定長でとられ、以下の情報が記録される。

  1. 識別のためのマジックナンバー : オフセット 0 から始まる。文字列 "[depot]\n\f" を内容とする。
  2. 検証のためのファイルサイズ : オフセット 16 から始まる。int型の整数である。
  3. バケット配列の要素数 : オフセット 24 から始まる。int型の整数である。
  4. 格納しているレコードの数 : オフセット 32 から始まる。int型の整数である。

バケット部はヘッダ部の直後にバケット配列の要素数に応じた大きさでとられ、チェーンの先頭要素のオフセットが各要素に記録される。

レコード部はバケット部の直後からファイルの末尾までを占め、各レコードの以下の情報を持つ要素が記録される。

  1. フラグ(削除用) : int型の整数である。
  2. キーの第2ハッシュ値 : int型の整数である。
  3. キーのサイズ : int型の整数である。
  4. 値のサイズ : int型の整数である。
  5. パディングのサイズ : int型の整数である。
  6. 左の子の位置 : int型の整数である。
  7. 右の子の位置 : int型の整数である。
  8. キーの実データ : キーのサイズで定義される長さを持つ一連のバイトである。
  9. 値の実データ : 値のサイズで定義される長さを持つ一連のバイトである。
  10. パディング : 値のサイズとアラインメントにより算出される長さを持つ一連のバイトである。

データベースファイルはスパースではないので、通常のファイルと同様に複製等の操作を行うことができる。Depotはバイトオーダの調整をしないでファイルの読み書きを行っているので、バイトオーダの異なる環境にデータベースファイルをコピーしてもそのままでは利用できない。

データベースファイルのマジックナンバを file コマンドに識別させたい場合は、magic ファイルに以下の行を追記するとよい。

0       string          [depot]\n\f     database file of QDBM
>16     long            x               \b, filesize:%d
>24     long            x               \b, buckets:%d
>32     long            x               \b, records:%d
0       string          [depot]\0\v     dummy file of QDBM

バグ

Segmentation Fault等によるクラッシュ、予期せぬデータの消失等の不整合、メモリリーク、その他諸々のバグに関して、既知のもので未修正のものはない。

QDBMのデータベースは書き込みエラーに対して脆弱である。ロールバックやバックアップの機能はない。外的エラー要因への対処はアプリケーションに任される。例えば、ディスクが一杯になったり、シグナル等を受け取って処理を中断せざるを得ない時の対処がそれにあたる。

バグを発見したら、是非とも作者にフィードバックしてほしい。その際、QDBMのバージョンと、利用環境のOSとコンパイラのバージョンも教えてほしい。


FAQ

Q.: どのプラットホームで利用できるか。
A.: POSIX互換の環境であれば利用できるはずである。少なくとも、Linux 2.4とSunOS 5.8では利用できる。
Q.: Windows上で利用できるか。
A.: Cygwin環境でなら利用できる。MinGWには対応していない。
Q.: アプリケーションの良いサンプルコードはあるか。
A.: dptsv.c 、dptest.c 、dpmgr.c の順に見てほしい。それらはQDBMの配布用アーカイブに含まれる。
Q. 性能の実測値はどのようなものか。
A. コマンド dptest によって重複しないレコードの追加と検索にかかる時間を計った。バケット配列の要素数はレコード数の2倍とした。2.53GHzのPentium 4プロセッサを1個、333MHzのRAMを1GB搭載したLinux 2.4のマシンでは、100万レコードの追加に5.0秒、その全ての検索に4.5秒、1000万レコードの追加に50.9秒、その全ての検索に44.9秒かかった。500MHzPentium 3プロセッサを1個、133MHzのRAMを192MB搭載したLinux 2.4のマシンでは、100万レコードの追加に12.0秒、その全ての検索に11.1秒、1000万レコードの追加に495.0秒、その全ての検索に414.8秒かかった。
Q.: なぜB木を使わないのか。
A.: QDBMは範囲検索や順序に基づく参照を目的とするデータベースではない。B木はそれらが可能な点でハッシュより優れているが、単純な一致検索の性能はハッシュの方が優れている。ハッシュチェーンに二分探索木を用いるのは、B木よりも各ノードに必要な領域のサイズが小さいため、その分バケット配列の要素数を増やせるからである。
Q.: 「QDBM」とはどういう意味なのか。
A.: 「QDBM」は「Quick DataBase Manager」の略である。高速に動作するという意味と、アプリケーションの開発が迅速にできるという意味が込められている。
Q.: 「Depot」「Curia」「Relic」はどういう意味なのか。どう発音するのか。
A.: 「depot」は、空港、倉庫、補給所など、物質が集まる場所を意味する英単語である。発音を片仮名で表現するなら「ディーポゥ」となる。「curia」は、宮廷、法廷など、権威が集まる場所を意味する英単語である。発音を片仮名で表現するなら「キュリア」となる。「relic」は、遺物、遺跡など、過去の残骸を意味する英単語である。発音を片仮名で表現するなら「レリック」となる。

ライセンス

QDBMは平林幹雄が作成し、著作権を保持するものである。QDBMはGNU GENERAL PUBLIC LICENSE Version 2に基づくフリーソフトウェアとして無償で配布される。QDBMは誰でも自由に使用し、改編し、頒布することができる。一方で、QDBMは完全に無保証であり、その使用および不使用に伴ういかなる損害に対しても作者は責任を負わない。

作者と連絡をとるには mikio@24h.co.jp 宛に電子メールを送ればよい。感想や改善案やバグレポートなどを寄せると作者は喜ぶ。