津島博士のパフォーマンス講座 indexページ▶▶

津島博士のパフォーマンス講座 
第8回 断片化について

■津島博士による解説が動画でも! セミナー動画公開中です。
津島博士のパフォーマンス講座「パフォーマンス問題はなぜ起きるのか」  【WMV】 【MP4】 【PDF
皆さんこんにちは、暑くなってきましたが体調はいかがでしょうか。私は多少疲れ気味ですが頑張っております。今回は、今まで少し説明した断片化についてもう少し詳しく聞きたいというご要望がありましたので、断片化についてまとめて説明しようと思います。参考にして下さい。

■1.断片化は、なぜ起こるのか
まずは、断片化がなぜ起きるのかを説明します。断片化するものにはいろいろとあります。第7回に説明したメモリにも発生します。その中で代表的なのが表領域のエクステントと索引の断片化だと思います。

ある領域に対して、サイズが異なる領域を割り当てと削除を繰り返し行うと発生してしまいます。これは、第7回で説明したようにサイズが異なると再利用するときに未使用な領域が出来てしまい、利用効率が低下してしまうからです。だとしたら、同じサイズで割り当てを行えば良いと思うかもしれませんが、そう簡単ではないのです(表領域のエクステントの断片化以外は、これとは異なります)。それでは、Oracle Databaseの領域で発生する表領域のエクステント、セグメントのブロック、行断片、索引の順に説明していきます。

(1)表領域のエクステント
表領域を割り当てる単位はエクステントです。このエクステントはオブジェクト(テーブルなど)毎に指定できるので、オブジェクトの大きさなどによって異なるサイズになる可能性があります。表領域に一つのオブジェクトしか割り当てないのであれば発生しません。大きいサイズのテーブルであれば可能かもしれませんが、小さいテーブルではそうはいかないですね。よって、断片化が発生しないようなルール(例えば、表領域のデフォルト・エクステントを指定するなど)を作成して運用するのが一般的でしたが、これを行うのは簡単ではありません。これをもっと簡単にするためにローカル管理表領域ができたのです。

ローカル管理表領域を使用するとエクステントの管理がビットマップになり、以下のように固定サイズになります。これで断片化の発生を防止します。大きなサイズのテーブルはUNIFORM、あまり大きくないサイズのテーブルはAUTOALLOCATEを使用すると良いと思います。
  • UNIFORM(指定サイズがエクステント・サイズになります。デフォルトは1Mバイトです)
  • AUTOALLOCATE(管理サイズが64Kバイトになりエクステント・サイズはオブジェクト・サイズによって変わります)
エクステント・サイズについて
表領域のエクステント・サイズ(ローカル管理表領域上での管理サイズ)とオブジェクト(テーブルなど)のエクステント・サイズ(オブジェクトの拡張サイズ:INITIAL,NEXT)について誤解がないように簡単に説明します。

オブジェクトのエクステント・サイズを指定しない場合、表領域のエクステント・サイズで領域が割り当てられます(基本はこれで問題ありません)。

オブジェクトのエクステント・サイズを指定した場合、指定されたサイズの領域を割り当てます(指定されたサイズは表領域のエクステント・サイズに丸められます)。このとき、エクステント・サイズより大きなサイズを指定すると複数のエクステントを割り当てますが、連続しないエクステントでかまいません(例えば、UNIFORMサイズ1Mバイトの表領域に初期エクステント5Mバイトのテーブルを作成すると1Mバイトのエクステントを5つ作成しますが、この5つのエクステントは連続していなくてかまいません)。AUTOALLOCATEの場合は、オブジェクトのサイズでエクステント・サイズが決定されます(小さいと64Kバイト、大きいと1Mバイトや8Mバイトなどのようになります)が、これは連続された領域である必要があります(例えば、セグメント・サイズが10Mバイトだとするとエクステント・サイズは1Mバイトになりますので、64Kバイトが16ユニット連続した空き領域を必要とします)。そのため、AUTOALLOCATEに大きいサイズのテーブルと小さいサイズのテーブルを格納する場合はエクステント・サイズが異なってしまいますので、断片化が発生し易いので注意して下さい。
(2)ブロック
ブロックの断片化は、自動セグメント領域管理(ASSM)を使用している時に発生します。ASSMはビットマップでブロック内使用率(Unformatted,0-25%,25-50%,50-75%,75-100%,Full)を管理して、INSERTでブロック競合を発生しないようにランダムでブロックにアクセスします(実際には、プロセスIDを使用したハッシュ・アルゴリズムにより、INSERTの開始位置を分散させます。そのため、フリーリストがなくても競合しないようになっています)ので、使用していないブロックや使用率の低いブロックが発生してHWMが引き上がりやすくなります。これは、同時多重INSERTの数が予想できないので、先頭位置から順番にブロックを使用させることはできないからです。フリーリストのように同時多重を想定して作成しなくて良い訳ですから管理性は向上しますので、これは仕方ないところだと思います。

(3)行断片
UPDATE文によって行サイズが長くなると行断片(行移行/行連鎖)が発生する可能性があります。以下の図のようにUPDATE文を行うことで行サイズが長くなってしまいブロックに収まらなくなると、別のブロックにデータを格納します。このとき行全体がブロックに収まる場合はすべてのデータを移行します(行移行)。収まらない場合は複数のブロックにまたがって格納します(行連鎖)。どちらもブロックをまたがっていますので行連鎖という場合もあります。これは、UPDATE文前に行が格納されていたブロックに行ヘッダは残されるからです。行ヘッダが残るのは、ROWIDが変わると索引が利用できなくなるからです。行が1行に収まらないため複数ブロックにアクセスするのは仕方ありませんが、空き領域に入りきらないために行移行されて、1行アクセスするために2ブロックをアクセスするのは効率よくありません。このために頻繁にUPDATEされるテーブルはPCTFREEパラメータで空き領域の割合を多めに作成するようにします。
img_tsushima_110722_01.gif
(4)索引
第6回で索引の断片化について簡単に説明しましたが、もう少し詳しく、なぜ断片化が発生するかについて説明します。
まず、索引の断片化の前にBツリーの構造について説明します。

最初に、キーの挿入について説明します。挿入時に対象ブロック(同一階層レベル上ではソートされて格納されているため、キー値によって追加するブロックが決まります)に空きが無いとブロックを分割しますが、この分割は追加するキー値によって次の二つの動作を行います。
  • 追加しようとするキー値が最大の場合は、新しいキーだけ新規ブロックに格納します。
  • 追加するキー値が最大でない場合は、ブロック内のキーと追加しようとするキーの中で中央の値のキーで二つのブロックに分割します。
 追加しようとした値が常に大きい場合は、ブロック内使用率を100%に維持できますが、最大でない場合には50%の使用率になることを意味します(以下の図を参照)。索引作成時にソートを行うのは、常に追加する値が大きくなるようにして、使用率を向上させるためです。使用率を100%で作成すると、索引の更新時に常に分割が発生してしまいます。そのため、更新が多い索引では、テーブルと同じように作成時にPCTFREEパラメータでブロック内の空き領域の割合を指定できるようになっているのです。
img_tsushima_110722_02.gif
 また、キーの削除を行っていくと、ブロック内の使用率が低下していきます(更新も削除と追加を行いますので同じです)。このようにリーフ・ブロックが増えてしまうことを索引の断片化と言います。これを回避するためにBツリー構造では、使用率が50%を下回ると隣接するブロックとマージして50%を維持するようになっています。

このマージ処理は、Bツリーのバランスと性能を維持する上で重要です。しかし、問題になるのがマージ処理(マージするかの判断も含め)に要するコストが高いことです。また、そのマージ後のブロックに幾つかの追加操作が行われただけで、分割しなければならないことです。そのため、OracleのBツリー索引は自動マージ処理を行わないようにしています。Oracle8iからは自動的には行いませんが、以下のSQL文で索引の結合ができるようになっています。ただし、同じブランチ内のリーフ・ブロックのみが結合の対象になります(50%を下回るリーフ・ブロックが隣接していても、上のブランチ・ブロックが異なれば結合しません)ので、すべてを結合してくれる訳ではありません。また、リーフ・ブロック数は減りますが、ツリーの階層は変化しません(索引範囲スキャンやキャッシュヒット率は向上します)。そこまで変更したいのであれば第6回で説明したように再構築が必要になります。
SQL> ALTER INDEX <索引名> COALESCE;
■2. 遅くなるのはなぜか
それでは、なぜ断片化するとパフォーマンスが遅くなるか説明します。

断片化で遅くなるのは、基本はI/O効率が良くないからです。I/O効率といってもいろいろとありますが、I/O回数が増えてしまう、1回のサービスタイム(処理時間)が悪くなってしまうなどがあります。どのように効率が悪くなってしまうのかを、表領域、ブロック、行断片、索引の順に説明します。

(1)表領域のエクステント
表領域のエクステントの断片化は、利用効率が悪くなって無駄な領域が増えてしまい、領域不足になってしまう場合やI/Oのサービスタイムが長くなってしまう場合です。アクセス範囲が広くなるので、ハードディスクにアクセスする時間が何倍も遅くなってしまいます。例えば、テーブルのフルスキャンをすると、連続領域でないとシーケンシャル・アクセスの性能が低下してしまいます
ハードディスクについて
ここでハードディスクのアクセスを簡単に説明します。
ハードディスクへのアクセス時間は、
  • ディスクシーク時間:磁気ヘッドを移動するのに要する時間。ハードディスクの中で一番遅い処理
  • 回転待ち時間:磁気ヘッドが目的の位置に来るのに要する時間。回転スピードが高速であれは短くなります
  • データ転送時間
からなっています。不連続な領域をアクセスする場合は,シーク時間などのために,連続した領域をアクセスする場合より何倍も遅くなります。そのため、できるだけ連続領域にデータを格納する方が効率が良くなります。Oracle Databaseは、エクステントという連続領域単位に割り当てを行います。テーブルなどを作成するときに、できるだけ大きなサイズのエクステントにするば良いのですが、正確な領域見積もりができなかったりで難しい訳です。
(2)ブロック
ブロックの断片化により、ブロックの利用効率が悪くなってブロック数が増えてしまうことで、I/Oが増えてしまい、パフォーマンスが遅くなってしまいます。
Oracle Database 10gからは、ASSMを使用していれば、以下のSQL文でセグメント縮小を行うことで領域の断片化を解消できます。
  • COMPACT:HWMを引き下げないで領域の断片化のみを解消します
  • CASCADE:テーブルに作成されている索引も縮小処理を行います
 ただし、行移動が有効になっている必要があります(デフォルトでは有効になっていません)。ブロックをマージして使用ブロック数を削減するので行が移動するからです(索引もメンテナンスされますのでUNUSABLE状態にはなりません)。このときHWMを引き下げるとデータが入っていないエクステントは解放されて、他のセグメントで利用できるようになります。
SQL> ALTER TABLE <テーブル名> ENABLE ROW MOVEMENT;
SQL> ALTER TABLE <テーブル名> SHRINK SPACE [COMPACT] [CASCADE];
(3)行断片
Oracleはブロック単位にI/Oを行いますので、行断片が起こると1行のデータが複数ブロックに格納されてしまうため、アクセス量が増えてしまいます。空き領域に入りきらないために行移動されて、1行アクセスするのに2ブロックをアクセスするのは効率よくありませんので、定期的に以下のように確認して、あまりにも多い場合には解消するようにしましょう。
SQL> ANALYZE TABLE <テーブル名> COMPUTE STATISTICS;
SQL> SELECT num_rows, chain_cnt FROM dba_tables WHERE table_name = '<テーブル名>';
 行断片はセグメント縮小では解消されませんので、完全に解消したい場合は以下のようにMOVEなどをすることによって再構築させる必要があります。
SQL> ALTER TABLE <テーブル名> MOVE TABLESPACE <表領域名>;
(4)索引
索引の断片化が発生するのは、ブロック内の利用効率が悪くなりリーフ・ブロックが増えてしまい、アクセスブロック数が増えてしまうからです。第6回で説明したように、リーフ・ブロックが増えるとBツリーの階層が増加する可能性があります。これは、ブランチ・ブロック数をルート・ブロックで管理できなくなるとブランチ・ブロックの階層が増えるからです。以下の図のように、ブロックにキーが50個格納できるとします(これは、ブロックサイズとキーサイズによります。多く格納できると階層は少なくなります)。1ブランチ・ブロックで50個のリーフ・ブロックを管理できるので、3階層まではリーフ・ブロックが2,500(50×50)までになります。

Bツリー索引は、階層が多いとアクセス効率が悪くなります。アクセスするときルート・ブロックからリーフ・ブロックにアクセスしてからデータ・ブロックにアクセスするため、できるだけ階層の少ない索引を作った方が良い訳です。

断片化は更新を頻繁に行うと発生しますので、定期的に確認して再構築する必要があります。そのため索引の結合や再構成などのオンライン機能が提供されています。ただし、再構成は既存の索引を読み込んで新たな索引を作成しますので、領域が2倍必要になります。
1ブロックに50個のキーが格納できるとすると、リーフ・ブロックが50個までだと2階層(ルート、リーフ)、2,500(50×50)までだと3階層(ルート、ブランチ、リーフ)、これを超えると4階層(ルート、ブランチ、ブランチ、リーフ)になります。
img_tsushima_110722_04.gif
■3.おわりに
今回は断片化についていろいろと説明しました。暑くなってきましたが皆さま体調に気を付けて下さい。次回も頑張りますのでよろしくお願いします。質問をお待ちしています。

それでは、次回まで、ごきげんよう。
img_tsushima.gif ■津島博士より
長年に渡りデータベースの構築やパフォーマンスチューニングなどに従事し、最近では若手エンジニアの育成および大規模データベース案件などの支援に従事しております。今までの経験が少しでもお役に立てればと思い、この連載を始めることに致しました。できるだけ長く続けたいと思いますのでよろしくお願いいたします。

Oracle Databaseは、技術の進化により非常に扱いやすくなったと思います。私自身も昔のバージョンを使用したころに比べると非常に楽になったと感じています。いろいろと進化したとはいえパフォーマンス問題が発生しなくなった訳ではありません。今でも多くの担当者が色々と苦労していると思います。その中でスキルや機能を知らずに苦労している場合もあるように思いますので、ここで紹介していけたらと考えています。

この連載では、このようなOracle技術者(データベース技術者)の方へのアドバイスとして様々なパフォーマンス問題を題材に解説していこうと考えています。既にデータベース運用を行っている管理者、これから管理者を目指す方までを対象に、様々な疑問に対して少しでも何かの手助けになればと願っています。できるだけ読者の皆様からの疑問に答えていきたいと思っておりますので(問合せなどの具体例を使用して説明した方が分かりやすいと思いますので)、パフォーマンス問題に関する様々な質問をお願いいたします。

津島博士の記事についてのご質問はこちらまでお願いいたします。

津島博士のパフォーマンス講座 indexページ▶▶