データベース・ガイダンス・内部スキーマ
初めて担当するデータベースの授業で、少し不安要素もあるけど、 まず最初にシラバス配布とデータベースシステムの重要性を解説する。 以下の話は、DBエンジンを使う利点の説明でもあり、 一方でDBの内部スキーマでどういう技術が必要となるかの説明でもある。 説明の中では、データベースに要求されるACID特性も交えている。
データベースエンジンを使わないなら
データベースの重要性を理解してもらうために、データベースエンジンを使わない場合は、 どういう方法を使っていたのかを解説。使用教科書には、データベースを使わない場合の直接ファイルを触る話が実例を交えて書いてないので、その辺をちょっぴり具体的に書いてみる。
データをまるごと読み込み
4年の情報構造論であれば、目的のデータを探す処理といっても、 ポインタや処理速度のオーダなどの理解を 深めるためにも、全データを最初に目的のデータ構造で一括読み込みをして、 リスト構造なり2分木なりハッシュという演習をしていた。 しかし、こういう方法は
- データ件数が数万件を越えてきたら、主記憶がデータで埋め尽くされ、メモリが不足すれば仮想記憶が使われ性能低下といった問題や、
- readonly でなければ、 途中でシステムダウンでデータ消失といった不安がつきまとう(耐久性:D)
といった問題がつきまとう。
シーケンシャルファイル
大量のデータを主記憶にすべて読み出しておくことは、 主記憶の浪費や、永続性記憶資源でないという点で、 問題がある。 であれば、必要に応じて読み出せばいいけど、 C言語初心者の fscanf() しか理解していなければ、 先頭から1件1件必要に応じて順次ファイル(シーケンシャルファイル)として 読み出すことになる。ただし、O(N)の時間がかかり、それこそン万件であれば、非現実的。
struct Data { char name[ 10 ] ; int age ; } ; struct Data data ; while( ... ) { if ( fscanf( fp , "%s %d" , data.name , &data.age ) == 2 ) { // 読み込んだデータの処理 } }
ランダムアクセスファイル
先頭からアクセスを繰り返す時間が非現実的なのであれば、 配列のように、N番目のデータを一定時間で読み出せればいい。 こういう場合、データが固定長であれば、データサイズ×データ件数が、 そのまま並んでいるファイルをつくって、N件目を読み出したければ、 ファイル先頭から、fseek()などで先頭から、データサイズ×N[byte]目から、 データサイズだけ読み込めばいい。
// N番目アクセスを強調するために逆順アクセス FILE * fp ; fp = fopen( "filename.dat" , "rb" ) ; // バイナリモードで開くこと : for( int n = SIZE - 1 ; n >= 0 ; n-- ) { struct Data data ; if ( fseek( fp , sizeof( Data ) * n , SEEK_SET ) == 0 && fread( &data , sizeof( Data ) , 1 , fp ) == 1 ) { printf( "%s %d\n" , data.name , data.age ) ; } }
fseek( fp , offset , SEEK_SET ) は、ファイル先頭から offset[byte] に、 読み書きの位置を移動させる関数。
fread( ポインタ , size , n , fp ) は、ポインタの先に、size[byte]のデータをn件分読み込む関数。 同様に書き込み版は、fwrite( … ) がある。
しかしながら、この方法は1件あたりのデータ長が一定だからできる方法。 また、途中でデータに追加や削除が発生したら、そのデータ領域を後ろに 移動させたり、前に詰めるといった処理をすることは、データ件数が巨大であれば、 非現実的。一般的には、追加ならファイル末尾に追加するなり、削除であれば 空き領域を詰めることはせずに、消えた無効データという目印だけつけておき、 利用者の少ない時間帯に、無効データの目印の場所を切り詰める処理を実行 することになるであろう。(トランザクション処理の一例) それに、N番目アクセスに fseek() + fread/fwrite のプログラムを書くのは、面倒だし、 複数の人が読み書きすると、一貫性:C , 隔離性:I のことを考え排他制御が必要。 また、書き込み処理の最中に、アプリケーションやOSなどが落ちた場合、 どこまでデータが書き込まれているのかという問題は、原子性:A,一貫性:C の 点でDBを使わない時には、極めて面倒な話となる。
ファイルシステムをデータベースの様に扱う
特定のキーで、データベースを検索する必要があって、削除データの切り詰めや、 可変長データのデータの取り扱いが必要であれば、ファイルシステムを データベースのように扱うプログラムを書く場合も多い。 例えば、名前から個人情報を探すプログラムであれば、名前自身をファイル名に して1件1ファイルで保存すれば、データ削除はファイル消去=unlink() になるし、 1件1ファイルであれば、データの追加で後続データを 後ろに移動させるといった手間もない。 可変長のデータであっても問題ない。
char fname[ 1024 ] ; char* filename( char* s ) { sprintf( fname , "DataDirectory/%s.txt" , s ) ; return fname ; } void data_write( char* name , int age ) { FILE* fp ; if ( (fp = fopen( filename( name ) , "wt" )) != NULL ) { fprintf( fp , "%s\n%d\n" , name , age ) ; fclose( fp ) ; } } int data_read( char* name ) { FILE* fp ; char n[ 100 ] ; int age = -1 ; if ( (fp = fopen( filename( name ) , "rt" )) != NULL ) { && fscanf( fp , "%s%d" , n , &age ) == 2 ) return age ; } else { return -1 ; } }
こういった方法は、アプリケーションプログラムが無くても、OSの命令で 簡単にデータを作ったり消したりができるため、データの取り扱いが楽なのが特徴。 しかしながら、この方法には、以下のような問題がある。
- nameに"../../../"といった文字が含まれていれば、目的ディレクトリ以外を触れるかも…
- また、一般的なシステムでは、1つのディレクトリ内に保存できるファイル数には、上限があり、1万件以上は書き込めないかもしれない。
- それにディレクトリ内のファイル情報は、配列的な単純テーブルだったり、単純線形リストだったりするため、ファイル数が巨大になれば、データ検索速度はO(N)で遅くなる。最近は、こういう問題に対応するために、ディレクトリ内のファイルをBTreeなどで保存するファイルシステムも開発されてきている。
RDBでない、古くから使われているDB
データベースで古くから使われているデータベースの1例は、 "Berkeley DB"が有名。 SQLなどのデータ操作言語を使うことなく、関数呼び出しで追加検索などを行う。 トランザクションや排他制御などが実装されているので、単純なデータベースであれば、 以前は、Berkeley DB を使うことが多かった。