Table of Contents
FireDACのパフォーマンスチューニング
本ブログでは、Delphi / C++Builderをある程度使用したことのある方をを対象として、FireDACの基本的な利用方法を解説していきます。
第6回のテーマ
- 単なる焼き直しのアプリでもパフォーマンスに問題なし?
- FireDACのデータフェッチの内部動作
- パフォーマンス改善のためのFireDACオプションの変更
第5回までで、実際にFireDACを利用したアプリケーション作成の演習を通してFireDACの基礎的な内容を学んできました。今回は、アプリケーション開発に不可欠な「パフォーマンスチューニング」をテーマにFireDACのパフォーマンスを向上させるためのポイントについて解説していきます。これは、既存のアプリケーションをFireDACへ移行する上でのヒントにもなります。
単なる焼き直しのアプリでもパフォーマンスに問題なし?
FireDACは、「ハイパフォーマンスデータアクセス」という大きな特徴を持っており、BDEと同等以上のパフォーマンスが発揮することもできます。
しかしながら、実際にはアプリケーションの要件、システムリソース(CPU、ディスク、メモリ、ネットワーク)やDBリソースなど、システムを構成する実行環境は一様ではないため、全ての局面において常にハイパフォーマンスを発揮できるわけではありません。
例えば、過去のDelphi / C++Builderバージョンで作成したプロジェクトをWindows 10へ移行した場合、それに合わせてアプリケーションが取り巻く環境も変わるため、コンポーネントの設定やアプリケーションコードを変えずにそのまま移行したような、いわゆる焼き直しアプリでは、ターゲットのデータベースに対しするパフォーマンスの維持が難しいかもしれません。
BDEとFireDACのデータセットは、それぞれプロパティ名やメソッド名などに互換性はあり、その外部インターフェイスは共通に見えます。しかし、その内部的な動作は全く異なる部分も多く、新しい実行環境に合わせて動作テストを行っていく中で、適切なプロパティに変更したり、さらに踏み込んでアプリケーションコードの変更を検討する必要も生じます。
こうしたポイントを理解するために、次節では、BDEとFireDACのデータフェッチの違い、そしてFireDACの内部動作について簡単に触れたいと思います。
FireDACのデータフェッチの内部動作
エンバカデロのサポートセンターでは、開発者から過去のDelphi / C++Builderバージョンで作成したプロジェクトを移行する際、BDEからFireDACへ置き換えると、データフェッチの速度が遅くなったという報告をよく受けます。これにはBDEとFireDACの内部的な動作の違いが大きく影響しています。その要因の一つはカーソルの動作の違いによるものです。
「カーソル」とは、検索セットからデータを移動する仕組みで、データの現在位置を示し、その位置から1行ずつデータを取り出して、更新や削除といった処理が行えます。カーソルは、PCのメモリやディスクなどのリソースとして保持し、カーソルの種類によって、どの場所にリソースとして保持されるかが決まります。
カーソルの種類を簡単にまとめると、以下の通りです。
(1) サーバーカーソル
RDBMS上にカーソルが作成され、サーバー上のリソースを使用してカーソルを制御します。結果セットもサーバーで管理されます。
メリット | デメリット |
---|---|
サーバー上にカーソルを持っているので、データセットの行移動も高速 | 貴重なサーバーリソースを消費する |
(2) クライアントカーソル
RDBMS上ではなく、クライアントPC側にカーソルを作成し、リソースはクライアントPCに保持されます。
使用するカーソルの違いは、例えばデータセットのレコードを1行移動させるNextメソッドの動作にも影響します。
サーバーカーソルでは、Nextメソッドに対しサーバーで開いているカーソルを1行移動するだけですが、 クライアントカーソルの場合、クライアントPCにデータをダウンロードしておかなければなりません。これは、データ件数が多い場合はもちろんですが、カラム数が多い場合にも速度に大きな違いが生じます。
BDEの動作は、DBエンジン(ドライバ)への依存性が高く、ほとんどの場合「サーバーカーソル」で動作しているため、データフェッチは高速です。これだけ見れば、BDEの方式が優れているように思えますが、デメリットも存在します。
BDEの方式では、結果セットとカーソル位置の管理は、常にサーバー上のリソースを消費しているため、クライアントのアクティブ数が多い場合、サーバーのリソースを圧迫する可能性があります。これは最近主流であるクラウド上に配置されているデータベース環境においては、不特定多数のクライアントからのアクセスが想定されるため、現在の稼働環境を前提とすれば、適した選択ではありません。
それに対してFireDACでは、「サーバーカーソル」と「クライアントカーソル」を使い分けてクライアント側のデータセットで移動したカーソル位置に応じて、適切なタイミング(オンデマンド)でデータの取得を行い、クライアントカーソルのオーバーヘッドをできるだけ軽減する、サーバーカーソルの動作をエミュレートするような仕組みを持っています。
このFireDACの内部的な仕組みは、データフェッチの速度を犠牲にする処理になりますが、取得済みのデータセットをクライアントPCのメモリ上に保持しているため、サーバーのリソースの使用を極力抑える仕様になっており、データベースに対する負荷は少なく、余計なネットワークのトラフィックも発生しないというメリットがあります。
なお、FireDACではデータフェッチに関する様々なオプションが用意されており、デフォルトのオプション設定を変更することよって、パフォーマンスの向上が見込めます。そのため、オプションの設定次第では、BDEと同等以上のパフォーマンスを発揮することも期待できます。
次の節では、FireDACで用意されているデータフェッチに関するオプションについて見ていきましょう。
パフォーマンス改善のためのFireDACオプションの変更
FireDACでは、 5 つのグループに分類されている約60個の豊富なオプションが用意されており、FireDACの各オプションは、接続単位(TFDConnection)、データセット単位(TFDQuery、TFDTableなど)でそれぞれ個別に変更することができます。
オブションの種類 | 説明 |
---|---|
FetchOptions | データの取得(フェッチ)方法の制御 |
FormatOptions | データ型のマッピング方法の制御 |
ResourceOptions | リソースの使用方法 (非同期実行など)の制御 |
UpdateOptions | FireDACのデータ更新方法の制御 |
TxOptions | トランザクションの実行方法の制御 |
このうち、本節では、データフェッチに関連するオプション(FetchOptions)を中心に説明します。FetchOptionsには、様々な設定オプションが用意されていますが、ここではプロパティを変更することでパフォーマンスの向上が見込める6つの項目をピックアップします。
(1) データのフェッチ方法を変更する
FetchOptions.Modeプロパティを使用すると、データをフェッチする方法を制御することができます。(Modeプロパティのデフォルトは、fmOnDemand)
fmAllは、データセットを開いた時に全件データを取得します。そのため、大量のデータを表示する場合には、最初のレコードの表示に時間がかかることがあります。但し、特定の環境ではこのモードに設定することでパフォーマンス向上の効果があります。
fmOnDemandは、データセットを開いた時に全件データを取得しません。データアクセスコンポーネント(クライアント側)のカーソルが移動すると、下記で紹介するFetchOptions.RowSetSizeに設定されているサイズ分のレコードを、適宜データベースへ問い合わせて、取得する仕組みを取っています。これは大量のデータを取得する場合、パフォーマンスを極力落とさずにデータを表示するメリットを持っています。
例えば、 Select文を実行し、結果セットが100万件あったとしても、必要なデータセットをオンデマンドで取得しているため、TDBGridなどのデータコントロールへの表示する場合、fmAllと比べて最初のレコードを表示する速度は格段に違います。
原則、データのフェッチはFireDACが自動的に行いますが、開発者自身が手動でフェッチすることもできます。例えば、独自にバッググラウンドでデータセットを取得する処理をマルチスレッドで実装する場合は、ModeプロパティをfmManualに変更してください。
状況に応じて選択するモードは異なりますが、一般的なパフォーマンス最適化を試みる場合は、デフォルト(fmOnDemand)のまま、以下で説明するRowSetSizeの値を適宜変更して動作を試してみることをお勧めします。
(2) 取得するレコード数の変更し、ラウンドトリップの回数を抑える
FetchOptions.RowSetSizeプロパティは、1 回の取得で取り出すレコード数を制御します。
FireDACでは、FetchOptions.Mode=fmOnDemandの場合、カーソルが移動するとオンデマンドでクライアントPCからネットワークを介してDBサーバーへアクセスして、データをダウンロードし、クライアントPCのメモリ内に保持するサイクルを繰り返します(これをラウンドトリップと呼びます)。
RowSetSizeの値を変更することによって、ラウンドトリップの回数を抑えることができるため、フェッチ時のパフォーマンス向上が見込めます。
例えば、100,000行の結果セットのフェッチを行なった場合
- RowSetSize = 1 => 7.5 秒
- RowSetSize = 100 => 0.65 秒
のように、実行する環境でパフォーマンスが向上する事例がありました。
ただし、RowSetSizeは、この値を大きくすれば、それに比例して実行速度が向上するわけではありません。RowSetSizeを大きい値(例えば2000〜3000など)に設定すると、次の行セットを取得するまでに遅延も大きくなるため、結果的にパフォーマンスが低下することもあります。またFireDACのdocwikiでは、RowsetSize <= 100 と RowsetSize <= 500 を比較しても、そのパフォーマンスが大きく変わらないという記述もあります。
これらの情報を総合的に判断すると、どの環境にも合致する最適値は存在しないという結論になります。ですから、ご自分の環境にどの値が適しているかを確認し、最適値を見つけるという作業が必要になります。あくまで目安となりますが、RowsetSize の値は、100以下で調整してみてください。
(3) カーソルの種類を変更する
FetchOptions.CursorKindプロパティは、FireDACコマンドまたはデータセットで使用されるカーソルの種類を指定します。
このプロパティのデフォルト値はckAutomaticで、このモードでは、他のFetchオプションや結果セットの構造、そして接続しているデータベースの種類に応じて、最速のカーソルの種類を自動的に選択します。
なお、一部のデータベースの種類やドライバ接続では、ckAutomaticではなく、明示的に別のカーソルに変更したほうがパフォーマンスが向上することがありますが、デフォルトのカーソル(ckAutomatic)で動作を試してください。
(4) メタデータの自動取得を抑止する
FireDACでは、主キーなどのテーブルの構造を把握するために、メタデータを自動取得するアナライズ機能を持っています。デフォルトでは、このオプション(FetchOptions.Items.fiMeta)はtrueです。
この機能が働くことで、事前にDBサーバーへメタデータを取得するコマンドを投げるため、dbExpressなどの他のDBコンポーネントと比べると、スループットの実行速度が遅くなることがあります。その場合は、このオプションを無効(FetchOptions.fiMeta=false)にすることで、メタデータを投げなくなり、パフォーマンスの向上が見込める場合があります。
ただし、このオプションを無効にする場合は、開発者が明示的に主キーフィールドを指定しなければならないため、次のいずれかを行う必要があります。以下は、docwikiからの抜粋です。
- pfInKey を主キー フィールドの ProviderFlags に含める。
- KeyFields を主キー フィールド名のコロン区切りリストに設定する。
- 主キー列の TFDDatSColumn.Options に coInKey を含める。
(5) 行セットのページング
FetchOptions.RecsSkipとRecsMax の設定によって、結果セットのページングが可能になります。これらのオプションを指定すると、データセットを開いたのち、最初の RecsSkip 個のレコードをスキップし、それ以降のレコードのうち RecxMax 個までを取得します。
これは、指定した範囲のデータのみを取得することができるため、結果セットの総レコード数が多い(例えば、10万件など)場合、ページングによってパフォーマンスの向上が期待できます。
結果セットのうち、先頭のレコードから1000件を取得するコード例は、以下の通りです(取得するレコードは、1行目 〜 1000行目)。
Delphi:
[crayon-676c6cfd028f4175678664/]C++Builder:
[crayon-676c6cfd0290a073567469/]また別のパターンで、結果セットのうち、先頭の100レコード目から100件を取得するコード例は、以下の通りです(取得するレコードは、100行目 〜 200行目)。
Delphi:
[crayon-676c6cfd0290c158245052/]C++Builder:
[crayon-676c6cfd0290e280115380/]なお、データセットを開いた後で、RecsSkip および RecsMax プロパティ値を変更しても効果はありません。これらのオプションは、データセットを開く前に指定しておく必要があります。
一度開いたデータセットに対してページングを行うには、接続を解除し、RecsSkip および RecsMax値を設定した後で、再度データセットを開かなければなりません。
RecsSkip プロパティや RecsMax プロパティが指定されている場合、FireDAC では、可能であれば、元の SELECT コマンドを変更して TOP/ROWS やそれと似た句を適用します。
(6) 単方向データセットに設定
FireDACのデフォルト設定(FetchOptions.UniDicectional=False)では、双方向データセットをサポートしており、結果セットをフェッチして、クライアントPC内にダウンロードしたデータは、メモリあるいはデータストレージ内に保持されます。
もし結果セットの総レコード数が非常に大きい場合、取得したデータサイズによってクライアントPCのメモリが圧迫する可能性があります。
UniDicectional=Trueに設定することで、単方向データセットに変更でき、取得したデータはメモリ内に保持されず、破棄されるようになります。その結果、実行パフォーマンスの向上と消費されるメモリを劇的に削減することができます。この事例はこちらのブログ記事でも言及されています。
ただし、データをTDBGridなどのグリッド コントロールに表示する際には、単方向データセット(UniDicectional=True)では表示することができません。双方向にスクロールするようなデータの表示が必要な場合は、双方向データセット(UniDicectional=False)を設定してください。
FireDACのデータフェッチに関するパフォーマンス改善のヒントはdocwikiに詳しくは記載されています。こちらも併せて参照してください。
アプリケーションコードを見直す必要も
FireDACのオプション設定を変更してもパフォーマンスが改善しない場合、既存のアプリケーションのコードを見直す必要もあります。
例えば、アプリケーションコードで実行しているSQL文は、本当に適切でしょうか。
- select * で全部のカラムを取得していませんか?
- select 文でwhere句を指定せず、全件取得していませんか?
- 取得するレコード数は、そのアプリケーションにとって適切ですか? (例えば、100万件のデータ取得は本当に必要ですか? もっと少なくできないでしょうか?)
- DBGridなどのデータコントロールで表示する場合、画面に見えていないデータまで取得する必要はありますか?
もし過去のDelphi / C++Builderバージョンで作成したプロジェクトを移行している場合、既存のコードをそのまま焼き直していると、新しい環境では非効率な処理や、あまり適切ではないコードが多く含まれていることもあります。もし何か該当する点があれば、既存コードのリファククタリングを行うことによって、パフォーマンスの向上が期待できます。
今回紹介した内容以外にもFireDACのパフォーマンスチューニングを行う方法はありますが、実行する環境や接続するデータベースの種類、そしてアプリケーションコードなどによって、チューニングの選択肢は様々です。アプリケーション開発においてパフォーマンスチューニングは避けて通ることはできませんが、このブログ記事の情報が参考となれば幸いです。
ぜひ、ご自身のアプリケーションの実行環境で試してみてください。
基礎から学べるFireDACデータアクセス再入門
データベースアクセスは、Delphi / C++Builderが得意とする分野です。最新のDelphi / C++Builderでは、FireDACと呼ばれる共通データベースアクセスコンポーネントを使って、多様なデータソースへのアクセスをサポートしています。
そこで本連載では、FireDACの基本的な概念の理解から使い方、さらには応用方法までを学習できる記事を提供。Delphi / C++Builderでの開発経験はあるけれど、FireDACについては詳しくない。あるいは、今回データベースアプリケーションを構築するのに、どのような技術があるのかを知りたい。こんな悩みを抱えている方に最適なシリーズです。