パフォーマンスの最適化を図る .NET アプリケーションの設計
パフォーマンス重視の .NET アプリケーションを開発することは容易ではありません。ADO.NET データ プロバイダーは、コードの実行速度が非常に遅くても、それを伝える例外をスローしません。
データの取得
データを効率よく取得するには、必要なデータのみを返す最も効率のよい方法を選択します。ここでは、.NET アプリケーションでデータを取得するときに、システムのパフォーマンスを最適化する方法を説明します。
アーキテクチャについての理解
ADO.NET は ADO.NET データ プロバイダーを使用してデータにアクセスします。.NET の実行時のデータ アクセスは、すべて ADO.NET データ プロバイダーを介して行われます。
ADO.NET の開発ウィザードでは、OLE DB を使用してウィザードのグリッドを埋めていきます。設計時にこれらのウィザードがアクセスするのは OLE DB ですが、生成されたランタイム コンポーネントがデータのアクセスに使用するのは ADO.NET データ プロバイダーです。.NET アプリケーションで OLEDB を介したデータ アクセスを最適化するのは実用的ではありません。代わりに ADO.NET データ プロバイダーを、実行時のすべての作業を行うコンポーネントとして最適化します。
長いデータの取得
ネットワーク経由で長いデータを取得するのには時間がかかり、リソースも消費するので、特に必要な場合以外は、アプリケーションから長いデータを要求しないようにします。
ユーザーが長いデータを必要とすることはほとんどありません。ユーザーがこのような結果項目の確認を要求する場合は、選択リストに長いデータの列だけを指定して、アプリケーションからもう一度データベースを照会します。このようにすると、平均的なユーザーは、ネットワーク トラフィックのパフォーマンスにさほど影響を与えずに結果セットを取得することができます。
アプリケーションによっては、ADO.NET データ プロバイダーにクエリを送信する前に選択リストを編成しないものもあります(一部のアプリケーションでは、select * from <テーブル名> ... などの構文を使用します)。長いデータが選択リストに入っていると、アプリケーションが長いデータを結果セットにバインドしていなくても、フェッチ時に長いデータを取得する必要があるデータ プロバイダーもあります。できれば、テーブルの一部の列のみを取得する方法を試してください。
場合によっては長いデータを取得しなければならないことがあります。このような場合でも、一般に 100 KB を超えるような大量のテキストを画面に表示させるのは望ましくありません。
取得するデータのサイズの縮小
ネットワーク トラフィックを減らしてパフォーマンスを向上させるために、最大行数や最大フィールド サイズの設定を呼び出したり、行サイズやフィールド サイズを制限するほかのデータベース固有のコマンドを呼び出したりして、取得するデータのサイズを扱いやすいサイズまで下げることができます。取得するデータのサイズを下げるもう 1 つの方法は、列のサイズを小さくすることです。データ プロバイダーでパケット サイズを定義できる場合は、必要な最小パケット サイズを指定します。
また、必要な行のみが返されるようにしてください。たとえば、2 列しか必要でないのに 5 列を返すようにしていた場合、不要な列に長いデータが入っていると、パフォーマンスが低下します。
CommandBuilder オブジェクトの使用
CommandBuilder オブジェクトは SQL ステートメントを生成するには便利だと思われがちです。しかし、これを使用するとパフォーマンスに悪影響を及ぼす恐れがあります。同時処理の制限が原因で、CommandBuilder オブジェクトは効率のよい SQL ステートメントを生成することができません。たとえば、次の SQL ステートメントは Command Builder で作成されたものです。
CommandText: UPDATE TEST01.EMP SET EMPNO = ?, ENAME = ?, JOB = ?, MGR = ?, HIREDATE = ?, SAL = ?, COMM = ?, DEPT = ?
WHERE
( (EMPNO = ?) AND ((ENAME IS NULL AND ? IS NULL)
OR (ENAME = ?)) AND ((JOB IS NULL AND ? IS NULL)
OR (JOB = ?)) AND ((MGR IS NULL AND ? IS NULL)
OR (MGR = ?)) AND ((HIREDATE IS NULL AND ? IS NULL)
OR (HIREDATE = ?)) AND ((SAL IS NULL AND ? IS NULL)
OR (SAL = ?)) AND ((COMM IS NULL AND ? IS NULL)
OR (COMM = ?)) AND ((DEPT IS NULL AND ? IS NULL)
OR (DEPT = ?)) )
ほとんどの場合、Command Builder で生成される Update ステートメントや Delete ステートメントよりも、エンド ユーザーの方が効率のよいステートメントを記述できます。
もう 1 つの問題は、CommandBuilder オブジェクトの設計にあります。CommandBuilder オブジェクトは常に DataAdapter オブジェクトと関連付けられ、DataAdapter オブジェクトが生成する RowUpdating イベントと RowUpdated イベントのリスナーとして CommandBuilder オブジェクト自身を登録します。したがって、行を更新するたびに、この 2 つのイベントが処理されなければなりません。
正しいデータ型の選択
データ型によっては、取得や送信に時間がかかるものがあります。スキーマを設計するときには、最も効率よく処理できるデータ型を選択してください。たとえば、整数データは浮動小数点データより速く処理できます。浮動小数点データは、内部データベース固有の形式に基づいて定義され、通常、圧縮形式になっています。このようなデータは、ワイヤ プロトコルで処理できるように、解凍して別の形式に変換する必要があります。
処理時間が最も短いデータ型は文字列で、その次が整数です。整数の場合は、通常、何らかの変換またはバイトの並べ替えが必要です。浮動小数点データとタイムスタンプの処理には、整数の少なくとも 2 倍の時間が必要です。
.NET オブジェクトとメソッドの選択
ここでは、.NET オブジェクトとメソッドを選択して使用するときに、システムのパフォーマンスを最適化する方法を説明します。
ストアド プロシージャの引数としてのパラメーター マーカーの使用
ストアド プロシージャを呼び出す場合は、リテラル引数ではなく、常に、引数マーカーのパラメーター マーカーを使用します。
Command オブジェクトの CommandText プロパティにストアド プロシージャ名を設定する場合、そのリテラル引数を CommandText へ物理的にコーディングしないでください。たとえば、次のようなリテラル引数は使いません。
{call expense (3567, 'John', 987.32)}
ADO.NET データ プロバイダーでは、データベース サーバーのストアド プロシージャを呼び出すことができますが、その際プロシージャをその他の SQL クエリとして実行します。ストアド プロシージャを SQL クエリとして実行すると、データベース サーバーがステートメントを解析し、引数の型を検証し、引数を正しいデータ型に変換します。
次の例で、アプリケーション プログラマは getCustName の引数を整数 12345 であると見なすでしょう。
{call getCustName (12345)}
しかし、SQL は常に文字列としてデータベース サーバーに送信されます。データベース サーバーが SQL クエリを解析し、引数値を分離しても結果はまだ文字列です。データベース サーバーで文字列 "12345" を整数値 12345 に変換する必要があります。パラメーター マーカーを使用することで文字列変換の必要がなくなるので、データベース サーバーでの処理量を減らすことができます。
{call getCustName (?)}
.NET アプリケーションの設計
ここでは、.NET アプリケーションを設計するときに、システム パフォーマンスを最適化する方法を説明します。
接続の管理
アプリケーションのパフォーマンスを向上させるには、接続を適切に管理することが重要です。接続を複数回行う代わりに、1 回の接続で複数のステートメント オブジェクトを使用することで、アプリケーションのパフォーマンスを最適化します。初期接続の確立後は、データソースへの接続は行わないようにします。
接続プールを使用すると、特に、ネットワークまたは World Wide Web を介して接続するアプリケーションのパフォーマンスを大幅に向上させることができます。また、接続プールによって接続の再利用が可能になります。接続を閉じても、データベースとの物理的接続を閉じるわけではありません。アプリケーションが接続を要求すると、アクティブな接続が再利用されるので、新しい接続の作成に必要なネットワークへの I/O は発生しません。
接続は、あらかじめ割り当てておきます。まず、どの接続文字列が必要かを確認します。1 つの固有な接続文字列で、新しい接続プールが 1 つ作成されます。
一度作成された接続プールは、アクティブなプロセスが終了するか、Connection Lifetime に指定された時間が過ぎるまで破棄されません。アクティブでないプールや空のプールを維持するのに必要なシステム オーバーヘッドは、ごくわずかです。
接続とステートメントの処理は、実装前に決めておく必要があります。接続方法を慎重に管理することで、アプリケーションのパフォーマンスが向上し、メンテナンスも簡単になります。
接続の開閉
接続は、それが必要になる直前に開いてください。必要になるより早く接続を開くと、ほかのユーザーが使用できる接続の数が減り、リソースの需要が増える可能性があります。
使用可能なリソースを保持するには、接続が必要でなくなったらすぐに接続を明示的に閉じます。有効範囲外になった接続がガベージ コレクターによって暗黙的にクリーンアップされるのを待つ場合は、接続は直ちに接続プールに戻されず、実際には使用されていないリソースに関連付けられます。
finally ブロックの内側で接続を閉じます。finally ブロック内のコードは、例外が発生した場合でも必ず実行されます。これにより、接続が明示的に閉じられることが保証されます。たとえば、次のようにします。
try
{
DBConn.Open();
… // 必要な作業を行います
}
catch (Exception ex)
{
// 例外を処理します
}
finally
{
// 接続を閉じます
if (DBConn != null)
DBConn.Close();
}
接続プールを使用している場合には、接続の開閉は不経済な操作ではありません。データ プロバイダーの Connection オブジェクトの Close() メソッドを使用すると、接続は接続プールに追加されるか戻されます。ただし、自動的に接続を閉じると、その接続に関連付けられているすべての DataReader オブジェクトが閉じられることを忘れないでください。
ステートメント キャッシングの使用
ステートメント キャッシュは、プリペアド ステートメントのグループまたは Command オブジェクトのインスタンスで、アプリケーションによって再使用が可能です。ステートメント キャッシュを使用するとアプリケーションのパフォーマンスを向上させることができます。これは、プリペアド ステートメントの動作が、そのステートメントがアプリケーションの存続期間中に何度再使用されたとしても、1 度だけ実行されるためです。
ステートメント キャッシュは物理接続に属します。実行された後、プリペアド ステートメントはステートメント キャッシュに置かれ、接続が閉じられるまで保持されます。
アプリケーションが使用する全プリペアド ステートメントをキャッシュすれば、パフォーマンスが向上するように思われます。しかし、この手法では、接続プールを使ってステートメント キャッシングを実装した場合、データベースのメモリに負担をかける結果になります。この場合、プールされた各接続がステートメント キャッシュを持ち、アプリケーションで使用される全プリペアド ステートメントを各自のキャッシュに含むことになります。これらのプールされたプリペアド ステートメントは、すべてデータベースのメモリにも保持されます。
コマンドの複数回使用
Command.Prepare メソッドを使用するかどうかによって、クエリ実行のパフォーマンスは良くも悪くも大きな影響を受けます。Command.Prepare メソッドは、基となるデータ プロバイダーに対し、パラメーター マーカーを使用するステートメントの複数回実行を最適化するように指示します。実行メソッド(ExecuteReader、ExecuteNonQuery、または ExecuteScalar)が使用されているかどうかにかかわらず、あらゆるコマンドの準備が可能であることに留意してください。
ADO.NET データ プロバイダーが、プリペアド ステートメントを含んでいるストアド プロシージャをサーバー上で作成することにより、Command.Prepare を実装する場合を考えてみましょう。ストアド プロシージャの作成には多くのオーバーヘッドを要しますが、ステートメントは複数回実行することができます。ストアド プロシージャの作成はパフォーマンスに悪影響を与えますが、プロシージャの作成時にクエリが解析され、最適化パスが保管されるため、作成したステートメントの実行は最小化されます。同じステートメントを複数回実行するアプリケーションは、Command.Prepare を呼び出してからそのコマンドを複数回実行することで、大きなメリットを得ることができます。
しかし、1 回しか実行しないステートメントに対して Command.Prepare を使用すると、不要なオーバーヘッドが生じる結果になります。さらに、大きな単一の実行クエリ バッチに対して Command.Prepare を使用するアプリケーションではパフォーマンスが低下します。同様に、常に Command.Prepare を使用するか、まったく Command.Prepare を使用しないアプリケーションは、プリペアド ステートメントとアンプリペアド ステートメントを論理的に組み合わせて使用するアプリケーションとは同じように機能しません。
ネイティブの管理プロバイダーの使用
アンマネージ コード、つまり .NET 環境外のコードへのブリッジは、パフォーマンスを低下させます。マネージ コードからアンマネージ コードを呼び出すと、データ プロバイダーの実行速度はマネージ コードのみのデータ プロバイダーよりも著しく遅くなります。このようなパフォーマンスが大きく落ちる方法は、極力避けます。
ブリッジを使用する場合は、そのブリッジのためのコードを書くことになります。後でデータベース固有の ADO.NET データ プロバイダーが使用可能になったとき、このコードを書き直す必要があります。つまり、オブジェクト名、スキーマ情報、エラー処理、およびパラメーターを書き直さなければなりません。ブリッジ用ではなく管理データ プロバイダー用にコード化することによって、貴重な時間とリソースを節約できます。
データの更新
ここでは、データベースのデータを更新するときのシステム パフォーマンスを最適化する方法を説明します。
切断された DataSet の使用
結果セットのサイズが、なるべく小さくなるようにします。サーバーから結果セットをすべて取得してから、DataSet を埋める必要があります。結果セット全体をクライアントのメモリに保存します。
データソースへの変更の同期
データ ソースへの変更の同期を取るには、次の例で示すように主キーを使用して、PsqlDataAdapter にロジックを作成する必要があります。
string updateSQL As String = "UPDATE emp SET sal = ?, job = ?" +
" = WHERE empno = ?";