4 Oracle Real-World Performanceのアプリケーションの設計

Real-World Performanceのアプリケーションを設計する場合、バインド変数、インスツルメンテーションおよびセット・ベース処理をどのようにコーディングするか考慮する必要があります。

トピック:

4.1 バインド変数の使用

SQL文またはPL/SQLブロックにおけるバインド変数プレースホルダは、実行時にデータを提供する場所を示します。

たとえば、次の文で作成された表にアプリケーションからデータを挿入する場合を考えます。

CREATE TABLE test (x VARCHAR2(30), y VARCHAR2(30));

データは実行時まで未知であるため、動的なSQLを使用する必要があります。

次の文は1つの行をtest表内に挿入し、x列とy列の文字列リテラルを結合します。

INSERT INTO test (x,y) VALUES ( ''' || REPLACE (x, '''', '''''') || '''),
                                ''' || REPLACE (y, '''', '''''') || ''');

次の文は、x列およびy列のバインド変数:xおよび:yを使用して、1つの行をtest表内に挿入します。

INSERT INTO test (x,y) VALUES (:x, :y);

コーディングがより容易なのは、バインド変数プレースホルダを使用する文です。

次に、前述したそれぞれの方法を使用して表testに1,000行を挿入する、動的な一括ロード操作を想定します。

文字列リテラルを結合する方法は1,000のINSERT文を使用し、それぞれはハード解析、修飾、セキュリティ・チェック、最適化およびコンパイルが行われる必要があります。それぞれの文はハード解析されるため、ラッチの数も大幅に増えます。ラッチは相互排除ロック・メカニズム、つまりシリアライズ・デバイスで、同時実行が禁止されています。

バインド変数プレースホルダを使用するメソッドは、1つのINSERT文のみを使用します。文はソフト解析、修飾、セキュリティ・チェック、最適化およびコンパイルが行われてから共有プールにキャッシュされます。コンパイルされた文は共有プールから、1,000個の挿入のそれぞれに使用されます。この文キャッシュはバインド変数を使用する大きな利点です。

バインド変数プレースホルダを使用するアプリケーションは、文字列結合を使用するアプリケーションよりもスケーラブルで、サポートするユーザー数は増加し、必要なリソースは減少し、実行速度は向上します。そして、SQLインジェクション攻撃を受ける可能性も減少します。SQL文で文字列結合が使用されていると、エンド・ユーザーがその文を改変し、なんらかの不正行為にアプリケーションを利用することが可能になります。

入力変数用のバインド変数プレースホルダは、DELETE文、INSERT文、SELECT文およびUPDATE文と、式またはリテラルが使用可能なPL/SQLブロック内の任意の位置で使用できます。PL/SQLでは、出力変数にもバインド変数プレースホルダを使用できます。バインドは、問合せ以外の操作では入力変数と出力変数の両方に使用されます。

関連項目:

  • SQLインジェクションからアプリケーションを保護するためのバインド変数の使用方法の詳細は、『Oracle Database PL/SQL言語リファレンス』を参照してください

  • OCIでバインド変数プレースホルダを使用する方法の詳細は、『Oracle Call Interfaceプログラマーズ・ガイド』を参照してください

ビデオ:

Oracle Real-World Performanceの技術を理解してバインド変数を使用するには、次の動画を参照してください。

4.2 インスツルメンテーションの使用

インスツルメンテーションの使用は、アプリケーションを通じてデバッグ・コードを追加することを意味します。有効な場合、このコードは、問題の特定に役立つ情報が含まれるトレース・ファイルを生成します。トレース・ファイルは、問題のある層の特定に役立つため、多層アプリケーションをデバッグする際に特に有効です。

関連項目:

詳細は、「SQLトレース機能(SQL_TRACE)」を参照してください

4.3 セット・ベース処理の使用

データ・ウェアハウス環境のデータベース・アプリケーションの一般的なタスクは巨大なデータ・セットを問い合せているか変更しています。

たとえば、あるアプリケーションでは何千万もの行でデータ・セット・ナンバリングを結合し、一連の条件でフィルタリングし、集計してからユーザーに結果を表示する場合があります。一方、あるアプリケーションでは、指定した条件に基づいて行を十億行の表からフィルタリングして除外してから別の表に一致する行を挿入する場合もあります。

アプリケーション開発者の課題は、これらの大きなデータ・セットを処理する際に、どうすれば高いパフォーマンスを実現できるか、という点です。処理方法は2つのカテゴリに分類されます: 繰返しおよびセット・ベースです。何年にも渡るテストによって、Oracle Real-World Performanceグループはセット・ベース処理の方法の方が、大きなデータ・セットを処理するデータベース・アプリケーションにおいては桁違いに優れていることがわかりました。

トピック:

4.3.1 繰返しデータ処理

繰返しデータ処理は、行ごと、配列を使用して、または手動の並列度を使用してデータを処理します。

トピック:

4.3.1.1 繰返しデータ処理について

このタイプの処理では、アプリケーションは条件ロジックを使用して行セット全体を繰り返します。

繰返しのアプリケーションは、PL/SQL、Java,、その他あらゆる手続き型言語またはオブジェクト指向言語で記述できます。この方法が「繰返し」と言われるのは、行ソースを1つ以上の行を含むサブグループに分割してから、各サブグループを処理するからです。単一プロセスがすべてのサブグループを通して繰り返すことも、複数プロセスが並列にサブグループを通して繰り返すこともできます。

必須ではありませんが、通常、繰返し処理では次のようにクライアント/サーバー・モデルを使用します。

  1. データベース・サーバーからクライアント・アプリケーションに行のグループを転送します。

  2. クライアント・アプリケーション内でグループを処理します。

  3. 処理されたグループをデータベース・サーバーに転送して戻します。

行ごとの処理、配列処理および手動の並列度の3つの主要な方法を使用して繰返しアルゴリズムを実装できます。どの方法も結果は同じですが、パフォーマンスの観点からは、それぞれに利点と欠点があります。

4.3.1.2 繰返しデータ処理: 行ごと

繰返しの方法では、行ごとの処理が最も一般的です。

単一処理はデータ・セット内をループし、一度に1つの行を処理します。典型的な実装では、アプリケーションはデータベースからそれぞれの行を取得し、中間層で処理してから行をデータベースに送って戻し、データベースでDMLが実行されコミットされます。

機能要件が、ext_scan_eventsという外部表を問い合せて、その行をstage1_scan_eventsという名前のヒープ構成表に挿入することだとします。次に示すPL/SQLブロックでは、行ごとの方法を使用して、この要件を満たしています。

declare
  cursor c is select s.* from ext_scan_events s;
  r c%rowtype;
begin
  open c;
  loop
    fetch c into r;
    exit when c%notfound;
    insert into stage1_scan_events d values r;
    commit;
  end loop;
  close c;
end;

行ごとのコードは次の処理を実行するためカーソル・ループを使用します。

  1. ext_scan_eventsから1行をクライアント・ホストで実行しているアプリケーションにフェッチするか、これ以上行が存在しない場合にはプログラムを終了します。

  2. stage1_scan_eventsに行を挿入します。

  3. 前述の挿入をコミットします。

  4. ステップ1に戻ります。

行ごとの方法には次のメリットがあります。

  • 小さなデータ・セットでは優れたパフォーマンスを示します。ext_scan_eventsに10,000レコードが含まれているとします。アプリケーションが1行を1ミリ秒で処理すると、合計処理時間は10秒になります。

  • ループ・アルゴリズムは、プロの開発者は皆慣れているため、すぐに記述しやすく、理解しやすいです。

行ごとの方法には次のデメリットがあります。

  • 大きなデータ・セットの処理時間が許容できないほど長くなる場合があります。ext_scan_eventsに10億行が含まれていて、アプリケーションが各行の処理に平均1ミリ秒かかると、合計処理時間は12日間になります。1兆行の処理には32年かかります。

  • アプリケーションはシリアルに実行されるため、最新のハードウェアで稼働しているOracle Databaseのネイティブの並列処理機能を活用できません。たとえば、行ごとの方法では、マルチ・コア・コンピュータ、Oracle RACまたはOracle Exadata Machineの恩恵を受けられません。たとえば、データベース・ホストに16 CPUと32コアが含まれている場合、単独のデータベース・サーバー・プロセスが各行を読取りまたは書込みしている間、31コアがアイドル状態になります。複数インスタンスがOracle RACデプロイメントに存在する場合、1つのインスタンスのみがデータを処理できます。

4.3.1.3 繰返しデータ処理: 配列

配列処理は、各繰返しに単一の行ではなく行のグループを処理すること以外は行ごとの処理と同じです。

行ごとの方法と同様に、配列処理はシリアルで、一度に1つのデータベース・サーバー・プロセスのみが行のグループに対して操作することを意味します。典型的な配列実装では、アプリケーションはデータベースから各行のグループを取得し、中間層で処理してからグループをデータベースに送って戻し、行のグループに対してDMLが実行され、コミットされます。

機能要件は 「繰返しデータ処理: 行ごと」の例と同じで、ext_scan_eventsという名前の外部表を問い合せて、その行をstage1_scan_eventsという名前のヒープ構成表に挿入することだとします。データベース・サーバーとは別のホストのSQL*Plusで実行する次のPL/SQLブロックは、配列の方法を利用してこの要件を満たします。

declare
  cursor c is select s.* from ext_scan_events s;
  type t is table of c%rowtype index by binary_integer;
  a t;
  rows binary_integer := 0;
begin
  open c;
  loop
    fetch c bulk collect into a limit array_size;
    exit when a.count = 0;
    forall i in 1..a.count
      insert into stage1_scan_events d values a(i);
    commit;
  end loop;
  close c;
end;

前述のコードは、FETCH文でのBULK COLLECTオペレータの使用の点で、同等の行ごとのコードとは異なります。これは、PLS_INTEGER型のarray_size値によって制限されています。たとえば、array_sizeが100に設定されている場合、アプリケーションは100行ごとのグループで行をフェッチします。

カーソル・ループは、次の一連の処理を実行します。

  1. ext_scan_eventsから行の配列をクライアント・ホストで実行しているアプリケーションにフェッチするか、ループ・カウンタが0になれば終了します。

  2. 行の配列全体をループし、各行をstage1_scan_events表に挿入します。

  3. 前述の挿入をコミットします。

  4. ステップ1に戻ります。

PL/SQLでは、配列コードは終了条件をテストするためにカーソル属性c%notfoundではなくカウンタを使用する点において、行ごとのコードとは異なります。その理由は、フェッチが表内で行の最後のグループを収集する場合、c%notfoundがループを強制的に終了してしまい、これは望ましくない動作だからです。カウンタを使用すると、各フェッチは指定した数の行を収集し、収集が空になるとプログラムが終了します。

配列の方法は行ごとの方法に対して次のメリットがあります。

  • 配列では、アプリケーションが一度に行をグループで処理でき、これはクライアントとサーバー間でネットワーク・ラウンドトリップ、COMMIT時間およびコード・パスを削減することを意味します。これらの要素を組み合せると、桁違いに合計処理時間を短縮する可能性があります

  • サーバー・プロセスは挿入をバッチ処理し、挿入ごとではなく挿入のグループごとにコミットするため、データベースはさらに効率的です。コミットの数を減らすと、入出力負荷を削減し、長い同期待ちイベントが発生する可能性が減少します。

この方法のデメリットは行ごとの処理と同じです。大きなデータ・セットの処理時間が、許容できないほどになる場合があります。1兆行の表の場合、処理時間を32年から3.2年に削減しても、まだ許容範囲にはなりません。また、アプリケーションは単一CPUコアでシリアルに実行される必要があり、そのためOracle Databaseのネイティブの並列度は活用できません。

4.3.1.4 繰返しデータ処理: 手動の並列度

手動の並列度は行ごとおよび配列処理と同じ繰返しアルゴリズムを使用しますが、複数のサーバー・プロセスが並列にジョブを操作できるようにします。

典型的な実装では、アプリケーションは複数回ソース・データをスキャンしてから、ORA_HASHファンクションを使用してデータを並列の挿入処理間で分けます。

ORA_HASHファンクションは、特定の式のハッシュ値を計算します。ファンクションは3つの引数をとります。

  • expr。これは通常、列名です

  • max_bucket。これはハッシュ・バケットの数を指定します

  • seed_value。同じデータから複数の結果を有効にします(デフォルトは0です)

たとえば、次の文は販売表を0から9にナンバリングした10個の行のバケットに分割し、バケット1から行を戻します。

SELECT * FROM sales WHERE ORA_HASH(cust_id, 9) = 1;

アプリケーションがこの方法でORA_HASHを使用し、n個のハッシュ・バケットが存在すると、各サーバー・プロセスはデータの1/nを操作します。

機能要件が行ごとおよび配列の例と同じであるとします。ソース表からスキャン・イベントを読み取り、それをstage1_scan_events表に挿入します。主な違いを次に示します。

  • スキャン・イベントは大量のフラット・ファイルに格納されます。ext_scan_events_dets表はこれらのフラット・ファイルを説明します。ext_scan_events_dets.file_seq_nbr列は数値のプライマリ・キーを格納し、ext_file_name列はファイル名を格納します。

  • 32サーバー・プロセスは並行して実行する必要があり、それぞれのサーバー・プロセスは別々の外部表を問い合せます。32個の外部表はext_scan_events_31内でext_scan_events_0という名前が付けられます。ただし、各サーバー・プロセスは同じstage1_scan_events表に挿入します。

  • 同じPL/SQLプログラムの32個のスレッドを実行することで、PL/SQLを使用して並列度を実現します。それぞれのスレッドはOracle Schedulerによって管理される別々のジョブとして並行して実行されます。ジョブはスケジュールおよびプログラムの組合せです。

データベース・サーバーとは別のホストのSQL*Plusで実行する次のPL/SQLコードは手動の並列度を使用します。

declare
  sqlstmt varchar2(1024) := q'[
-- BEGIN embedded anonymous block
  cursor c is select s.* from ext_scan_events_${thr} s;
  type t is table of c%rowtype index by binary_integer;
  a t;
  rows binary_integer := 0;
begin
  for r in (select ext_file_name from ext_scan_events_dets where ora_hash(file_seq_nbr,${thrs}) = ${thr})
  loop
    execute immediate
      'alter table ext_scan_events_${thr} location' || '(' || r.ext_file_name || ')';
    open c;
    loop
      fetch c bulk collect into a limit ${array_size};
      exit when a.count = 0;
      forall i in 1..a.count
        insert into stage1_scan_events d values a(i);
      commit;
--  demo instrumentation
      rows := rows + a.count; if rows > 1e3 then exit when not sd_control.p_progress('loading','userdefined',rows); rows := 0; end if;
    end loop;
    close c;
  end loop;
end;
-- END   embedded anonymous block
]';

begin
  sqlstmt := replace(sqlstmt, '${array_size}', to_char(array_size));
  sqlstmt := replace(sqlstmt, '${thr}', thr);
  sqlstmt := replace(sqlstmt, '${thrs}', thrs);
  execute immediate sqlstmt;
end;

このプログラムには、外側から内側へ3つの繰返しの構成があります。

  1. フラット・ファイルの名前を取得し、外部表の場所としてフラット・ファイル名を指定するのにDDLを使用する外側FOR LOOP

  2. 外部表の問合せから行のグループをフェッチする中間のLOOP文。

  3. 各グループを通じて繰り返し、行を挿入する最も内側のFORALL

このサンプル・プログラムでは、すべてのジョブで$thrsを31に設定し、$thrをすべてのジョブで0から31の間の別の値に設定します。たとえば、ジョブ1では、$thrが0に設定され、ジョブ2では$thrが1に設定され(続く)、となる場合があります。

$thrが0に設定されている最初のジョブによって実行されたプログラムでは、外部FOR LOOPは次の問合せの結果を通じて繰り返されます。

select ext_file_name 
from   ext_scan_events_dets 
where  ora_hash(file_seq_nbr,31) = 0

ORA_HASHファンクションはext_scan_events_dets表を32個の均等に分散されたバケットに分割し、SELECT文はバケット0のファイル名を取得します。たとえば、問合せ結果セットには次のファイル名が含まれている場合があります。

/disk1/scan_ev_101
/disk2/scan_ev_003
/disk1/scan_ev_077
...
/disk4/scan_ev_314

中間のLOOPはファイル名のリストを通して繰り返されます。たとえば、結果セットの最初のファイル名が/disk1/scan_ev_101であるとします。ジョブ1では、外部表はext_scan_events_0という名前で、そのため、LOOPの最初の繰返しではこの表の場所が次のように変更されます。

alter table ext_scan_events_0 location(/disk1/scan_ev_101);

最も内部のFORALL文では、BULK COLLECTオペレータがext_scan_events_0表から配列に行を取得し、stage1_scan_events表に行を挿入してからバルク挿入をコミットします。プログラムがFORALL文を終了すると、プログラムはループ内の次のアイテムに進み、外部表のファイルの場所を/disk2/scan_ev_003に変更してから前述の繰返しのように行を問合せ、挿入およびコミットを行います。Job 1はこの方法で、ハッシュ・バケット0に対応するフラット・ファイルに含まれているすべてのレコードがstage1_scan_events表に挿入されるまで処理を続行します。

ジョブ1が実行されている間、他の31個のOracle Schedulerジョブは並列に実行されます。たとえば、ジョブ2が$thrを1に設定すると、これはカーソルを表ext_scan_events_1の問合せとして定義し、ジョブ32まで続けると、$thrを31に設定してカーソルを表ext_scan_events_31の問合せとして定義します。この方法により、各ジョブは同時にスキャン・イベント・ファイルの別々のサブセットから読取り、サブセットから同じstage1_scan_events表にレコードを挿入します。

手動の並列度の方法は、他の繰返し方法よりも次の点においてメリットがあります。

  • サーバー・プロセスが並行して動作するため、大きなデータ・セットで優れたパフォーマンスを実現します。たとえば、32プロセスが作業を分割し、データベースに十分なCPUおよびメモリー・リソースがあって競合が発生しない場合、データベースは配列の方法が単一のジョブを実行するのにかかった時間で同時に32個の挿入ジョブを実行する可能性があります。大きなデータ・セットでのパフォーマンス向上は、シリアルな方法に対して桁違いになることが多いです。

  • アプリケーションがORA_HASHを使用してワークロードを分散すると、実行の各スレッドは同じ量のデータにアクセスできます。各スレッドが同じ量のデータを読み書きすると、並行プロセスは同じ時間で終了します。これは、アプリケーションが実行されているかぎり、データベースがハードウェアを活用していることを意味します。

手動の並列度の方法には次のデメリットがあります。

  • コードが比較的長く、複雑でわかりにくくなります。ワークロードを分散する作業はデータベースではなく、開発者の役割になるため、アルゴリズムが複雑になります。実質、アプリケーションは並行アルゴリズムで実行されるのではなく、シリアルなアルゴリズムが並行して実行されます。

  • 通常、データを分割する開始コストは一定のオーバーヘッドを要します。並列に行を処理するメインの作業をデータベースが開始する前に一定量の準備作業を実行する必要があります。この開始時の制約は、データを分割しないその他の方法にはありません。

  • データベース・オブジェクトの共通セットで複数のスレッドが同じ操作を実行すると、ロック競合およびラッチ競合が発生する可能性があります。たとえば、32個の別々のサーバー・プロセスがバッファの同じセットを更新しようとしている場合、バッファ・ビジーの待機が起こりえます。また、複数サーバー・プロセスがおおよそ同じ時間でCOMMIT文を発行している場合、ログ・ファイルの同期待ちが起こりえます。

  • 並行処理は、その他の繰返しの方法と比べて甚大なCPUリソースを消費します。データベース・ホストにスレッドを同時に処理する十分なコアがない場合、パフォーマンスが悪化します。たとえば、4つのコアのみが32個のスレッドに対して使用可能な場合、一定時間にスレッドがCPUを使用できる可能性は1/8です。

4.3.2 セット・ベース処理

セット・ベース処理は、データベースの内部でデータ・セットを処理するSQL技術です。

セット・ベース・モデルでは、SQL文が結果を定義し、データベースが結果を取得するための最も効果的な方法を判断します。対照的に、繰返しのアルゴリズムは条件ロジックを使用して、データベースからクライアント・アプリケーションに各行または行の各グループをプルし、クライアントでデータを処理してから、データベースにデータを送信して戻します。セット・ベース処理では、データがデータベースから離れないため、ネットワークのランドトリップおよびデータベースAPIのオーバーヘッドがなくなります。これは、COMMITの数を減らします。

前述の例と同じ機能要件だとします。次のSQL文は、セット・ベース・アルゴリズムを使用してこの要件を満たします。

alter session enable parallel dml;
insert /*+ APPEND */ into stage1_scan_events d
  select s.* from ext_scan_events s;
commit;

INSERT文にext_scan_events表の副問合せがあるため、単一のSQL文がすべての行を読み書きします。また、アプリケーションはデータベースがすべての行を挿入した後、単一のCOMMITを実行します。対照的に、繰返しのアプリケーションでは各行または行の各グループの挿入の後にCOMMITを実行します。

セット・ベースの方法は、繰返しの方法に対して次に示す大きなメリットがあります。

  • Oracle Real-World Performanceのデモとクラスで示すように、大きなデータ・セットでのパフォーマンスは桁違いに高速です。プログラムの実行時間が数時間から数秒に縮まることは珍しいことではありません。大きなデータ・セットでのパフォーマンスの向上があまりに大きいため、繰返しの方法の正当化が非常に難しくなります。

  • 処理速度の劇的な向上の副次的な効果として、DBAが長時間実行のエラーが起きやすいバッチ・ジョブをやめることができ、ビジネス・プロセスをリアルタイムに刷新します。たとえば、6時間のバッチを毎晩実行するかわりに、日中に必要に応じて12秒のジョブを実行すればよくなります。

  • アクセス・メソッドではなく、SQLが結果を定義するため、コードの長さは著しく短く(2、3行までに)なります。これは、アプリケーションではなくデータベースが行を分割、取得および操作する最適な方法を決定することを意味します。

  • 手動の並列度と対照的に、アプリケーションではなくデータベースがプロセスを管理するため、並列DMLはパフォーマンスが最適化されます。したがって、クライアント・アプリケーションでワークロードを手動で分割する必要がなくなり、各プロセスが同じ時間に終了することが期待できます。

  • データ・セットを結合するときには、データベースは相対的に効率の悪いアプリケーション・レベルのループではなく、効率のよいハッシュ結合を自動的に使用します。

  • APPENDのヒントはダイレクト・パス・ロードを実行し、これはデータベースがREDOおよびUNDOを作成せず、そのため、I/OおよびCPUの無駄が減ることを意味します。典型的なETLワークロードでは、バッファ・キャッシュが問題の原因となります。バッファ・キャッシュ内でデータを変更し、データとそれに関連するUNDOおよびREDOを書き込んで戻すことは、多くのリソースを消費します。バッファ・キャッシュは必要な速さでブロックを管理できず、ブロックをバッファ・キャッシュに入れて再度戻す操作(通常一度に8 Kのブロック)には高いCPUコストがかかるため、バッファのボリュームについていくために、データベース・ライターおよびサーバー・プロセスの両方が懸命に取り組む必要があります。

セット・ベース処理のデメリット:

  • この方法はデータベース開発者に馴染みがなく、そのため難しいものとなります。INSERTの例は比較的簡単です。ただし、複数の外部結合を必要とするような、さらに複雑な文ではさらに難しいアルゴリズムが必要になります。外部結合をパイプライン処理し、WITH句とCASE文を使用することに慣れていない開発者は、将来的に起こりうるセット・ベース・コードを書くことも理解することも考えると気が滅入る可能性があります。

  • セット・ベース・モデルは繰返しモデルとはまったく異なるため、変更することはソース・コードを完全に書き直すことになります。対照的に、行ごとのコードを配列ベースのコードに変更することは、これに比べるとささいなことです。

セット・ベース処理のデメリットに関係なく、Oracle Real-World Performanceのグループは、大きなデータ・セットに対するずば抜けたパフォーマンスの向上が、その労力を妥当なものとすると信じています。