図2●ページの内部構造。Oracleの場合,レコード・データはページの後ろから,レコード・ディレクトリはページの前から割り当てられる
図2●ページの内部構造。Oracleの場合,レコード・データはページの後ろから,レコード・ディレクトリはページの前から割り当てられる
[画像のクリックで拡大表示]
図3●レコードの内部構造。先頭にレコード・ヘッダーがあり,その後ろにフィールド長とフィールド・データの組が並ぶ
図3●レコードの内部構造。先頭にレコード・ヘッダーがあり,その後ろにフィールド長とフィールド・データの組が並ぶ
[画像のクリックで拡大表示]
表1●ANSIのデータ型に対応するOracleとSQL Serverのデータ型とサイズ
表1●ANSIのデータ型に対応するOracleとSQL Serverのデータ型とサイズ
[画像のクリックで拡大表示]
図4●SQL*Plusで数値型のフィールドを持つテーブルを作成してフィールドの内部型を調べたところ
図4●SQL*Plusで数値型のフィールドを持つテーブルを作成してフィールドの内部型を調べたところ
[画像のクリックで拡大表示]
図5●図4のテーブルにレコードを追加してから各フィールドが内部で占めるサイズを調べたところ。同じデータ型でもフィールドの値によってサイズが違うことがわかる
図5●図4のテーブルにレコードを追加してから各フィールドが内部で占めるサイズを調べたところ。同じデータ型でもフィールドの値によってサイズが違うことがわかる
[画像のクリックで拡大表示]
図6●レコード・チェーンの様子。更新後のデータは別のページに割り当てて,元のレコードの位置にはその位置へのポインタを格納する
図6●レコード・チェーンの様子。更新後のデータは別のページに割り当てて,元のレコードの位置にはその位置へのポインタを格納する
[画像のクリックで拡大表示]

テーブルとレコードの構造を詳しく見てみよう

 次に,RDBMSの基本であるテーブルがどのようにディスクに格納されているのか,その構造について見ていくことにしましょう。

 テーブルを構成する各ページの構造は,おおよそ(図2[拡大表示])のようになっています*11。図の左上にある「ページ・ヘッダー」は,ページ・アドレスやページ・タイプ(例えば,格納しているのがテーブルであるかインデックスであるか)など,ページについての一般的な情報を格納しています。その右の「テーブル・ディレクトリ」は,ページが格納するテーブル名など,そのテーブルに関する情報を保持します。

 その次の「レコード・ディレクトリ」は,いわばページ内に存在するレコードの一覧表です。各レコードのROWID(行識別子)*12と,ページ内での物理的な位置(オフセット)のペアを,レコードの数だけ保持しています。一番下の「レコード・データ」の部分が,実際のレコードの内容を格納する場所です。

 ページ・ヘッダーとテーブル・ディレクトリのサイズは固定ですが,レコード・ディレクトリとレコード・データはレコードの数や各レコードのサイズによって変化します。図2の場合,レコード・データは基本的にページの後ろから割り当てていき,レコード・ディレクトリはページの前から領域を割り当てていきます。したがって,ページの中央部分が空き領域になります。

 レコードの内部構造はRDBMSによって異なります。Oracleの場合には大体(図3[拡大表示])のようになっています*13。先頭にレコード・ヘッダーがあり,そのレコードのフィールド数やレコード・チェーン*14している場合のポインタなどを格納しています。その後ろには,レコードを構成するフィールドのフィールド長とフィールド・データのペアが順に並びます*15。レコードにフィールドが格納される順序は,すべてのレコードで同じです。

 レコードからフィールド・データを取り出すには,まずレコード・ディレクトリを参照して,ページ内部でのレコードの位置を取得します。後は,そのレコードの中でフィールドの長さを順番に調べていけば該当フィールドに到達します。これからわかるように,Oracleの場合,レコードの最後のフィールドを取り出すのは最初のフィールドを取り出すより時間がかかります*16

 Oracleではフィールドの値がNULL*17の場合にはフィールド長(0)だけが記録されます。また,NULLの値を持つフィールドがレコードの最後に位置する場合は,フィールド長も省略されます。そのため,NULLになることが多いフィールドはなるべくレコードの最後に保持するほうが領域を節約できます。

フィールドのサイズは格納するデータによって変わる

 データがフィールドに格納されるときの内部表現についても見ておきましょう。これも,RDBMSによって違いがあります。ANSI*18データ型に対応するOracleとSQL Serverのデータ型および内部表現のサイズを(表1[拡大表示])に示しておきます。

 数値データを数値型として扱う場合は,必要な精度(全体の桁数)と位取り(小数点未満の桁数)に応じて適切なデータ型を選択しなければなりません。例えば格納する値が−(2の31乗)~(2の31乗)−1の範囲で収まる場合はINTEGER型を利用できます。もっと大きな値を扱う場合は,BIGINTやDECIMALを選べばいいでしょう。

 Oracleの場合,数値はすべて内部的にNUMBER型というデータ型で扱います。ですからINTEGER,FLOAT,DECIMALなどのデータ型は,内部ではいずれもNUMBER型になります。加えて,データが占有する領域のサイズも,精度や位取りだけで決まるわけではなく,格納する値によって変わってきます。

 例えば,Oracleの付属ツールSQL*Plusで(図4[拡大表示])の(1)のようなSQL文を指定してテーブルを作成したとしましょう。このテーブルの各フィールドのデータ型をdescribeステートメントで表示してみると,図4の(2)のようになります。テーブルを作成時にDECIMALやINTEGERと指定しても,内部的にはNUMBER型が使用されているのがわかります。

 さらに,このテーブルにデータを格納してからOracleのvsize関数で各フィールドのサイズを調べてみたのが(図5[拡大表示])です。図4で見たようにdecimal_cはDECIMAL(10)と定義しており,内部的にはNUMBER(10)として扱われます。“VSIZE(DECIMAL_C)”の項目を見ると,1234をインサートした場合はサイズが3バイトですが(図5の(a)),12345をインサートした場合は4バイトになっていることがわかります(同(b))。データ型としてはNUMBER(10)ですが,フィールドが実際に占有するサイズは格納する値によって変わるわけです。

 もう一つ見てみましょう。inte_cはINTEGERで定義しており,内部的にはNUMBER(38)として定義されています。データ定義から言えば,inte_cはdecimal_cよりサイズが大きいデータ型です。しかしdecimal_cに12345,inte_cに1234をそれぞれ格納した場合の実際の格納領域は,decimal_cは4バイト,inte_cは3バイトになっています(図5(b))。

 文字列型についてはどうでしょうか。文字列データを扱う場合は,CHARまたはVARCHARを使用します。CHARは固定長で,文字列の長さによらず,指定した長さの領域を占有します。例えばCHAR(50)と定義した場合,格納する文字列の長さが50バイトより小さければ50バイトになるように空白が埋め込まれます。“BLACK”を格納すると,BLACKの後に45バイトの空白が埋め込まれるわけです。

 一方,VARCHARは可変長であり,実際に格納する文字列の長さのぶんだけディスクを占有します。そのため,“BLACK”をVARCHAR(50)に格納した場合は5バイトだけで済みます。半面,レコードを更新する際に文字列の長さが変わるとページの空き領域の検索などの作業が発生するため,固定長を使う場合よりパフォーマンスが低下します。CHARとVARCHARのどちらで文字列データを定義するかは,文字列の最大長や更新頻度などを考えて決定する必要があります。

更新処理ではレコード・チェーンに要注意

 テーブルの物理的な構造についてわかったところで,レコードの追加,削除,更新の際にRDBMSが内部でどのように動作しているのかを見ていきましょう。

 レコードを追加する場合にはまず,レコードを格納できるだけの空き領域を持つページを見つけなくてはなりません。RDBMSはそのために,レコードを追加できるページの番号を格納したリスト(フリー・リスト)を保持しています。このリストを順にあたっていけば,(もし存在するなら)必要な大きさの空き領域を見つけることができます*19

 十分なサイズの空き領域が見つかったら,RDBMSはその位置にレコードを書き込みます。その際,現在のレコード・ディレクトリの後ろに,そのレコードのROWIDとページ内での物理的な位置から成る新しいエントリを追加する作業も併せて行います。ただし,以前にレコードを削除したときのエントリの領域が残っている場合は,それを再利用することもあります。

 レコードを削除する場合は,該当するレコードをレコード・ディレクトリから検索し,レコード・ディレクトリのエントリと,レコード・データをページから削除します。その際,空き領域の断片化を防ぐために,レコード・データ全体を後ろ方向に詰めることもあります。ただ,削除するたびに詰めるのでは効率が悪いため,現在のOracleやSQL Server 2000では新しいレコードを追加するために連続した空き領域が必要になった時点で詰めるようになっています。この場合,レコード・データを指すページ内オフセットの値は変更する必要がありますが,ROWID自体は変わらないため,インデックスなどを書き換える必要はありません。

 レコードを更新する場合は,現在格納されている位置のままで内容だけを書き換えるのが基本です。したがって,更新用に新しいページを探す必要はありません。ただし,更新によって可変長フィールドのサイズが変わる場合は,レコード全体の長さが変化するため,元の位置に収まりきらないこともあります。この場合,そのページの空き領域が十分あれば,そこに更新後のレコードの内容を書き込みます。空き領域が足りない場合は,フリー・リストから十分な空き領域を持つ別のページを探し出し,そこに更新後のレコードを書き込みます。このとき,元のレコードの位置には更新後のレコードを指すポインタを格納しておき,ROWIDが変わらないようにします。

 このように,一つのレコードが複数のページにまたがって格納される状況をレコード・チェーン(行連鎖)と呼びます((図6[拡大表示]))。レコード・チェーンが発生すると一つのレコードを読むために複数回のディスク入出力が発生するため,パフォーマンスが低下してしまいます。

 レコード・チェーンの発生を防ぐためには,ページの空き領域が一定以下になったらそのページに新規のレコードを追加しないようにするのが効果的です。つまり,ページの空き領域の一部を,更新用に予約しておくわけです。多くのRDBMSは,そのための設定パラメータを用意しています。

 例えばOracleでは,テーブルを作成する際にPCTFREE,PCTUSEDという二つのパラメータを指定できます*20。ページの空き領域がPCTFREEで指定した割合以下になると,以後そのページへの新規レコードの格納が禁止されます。その後,レコードが削除されるなどして使用領域の割合がPCTUSED以下になると,再び新規レコードの追加が可能になります。

 これらのパラメータはテーブルごとに設定できるので,扱うデータのサイズやアクセスの性質に合わせて適切に設定してください。例えば,検索中心のデータベースなら,各ページにできるだけ多くのレコードを格納したほうがディスクの使用量やパフォーマンスの点で有利なため,PCTFREEを小さめに設定します。対して更新処理が多いシステムであれば,レコード・チェーンが発生しないようにPCTFREEを大きめに設定する必要があります。


加藤 比呂武(かとうひろむ)