この記事で分かること
- DLookupやDCount関数がAccessの動作を重くする技術的な根本原因
- クエリ結合(JOIN)やVBA(DAO Recordset)を活用した高パフォーマンスな代替コーディング手法
- 帳票画面や大量データの一括バッチ処理を正常化させる、段階的な改修プロセス
DLookup・DCount関数がAccessの動作を遅くするメカニズム
ドメイン集計関数は、SQLやVBAコードに短い引数を書くだけで、別テーブルの任意のデータをピンポイントで参照できる非常に便利なツールです。しかし、この利便性の裏では、データベースエンジンに対して非常に高い負荷がかかっています。
1. 暗黙的な個別クエリの「超多頻度発行」
DLookupやDCountが実行されるたびに、Accessの内部では対象となるデータベーステーブルへの接続がオープンされ、専用のSELECT文が都度生成・実行された後、接続を閉じるという一連の独立した処理が行われています。これが、データアクセスを遅延させる決定的なオーバーヘッドを生む原因です。
例えば、1,000件のレコードを表示するクエリがあり、その中の1つのフィールドにDLookupを埋め込んでいるケースを考えます。このクエリを開くだけで、内部的には1,000回ものSQL発行とデータベース接続の開閉が繰り返されます。これは、Web開発の分野などでパフォーマンス上の大敵とされる「N+1問題」とまったく同じ現象であり、データの行数に比例して処理時間は幾何級数的に増加してしまいます。
2. インデックスの未設定によるフルスキャン
DCount関数を使って、ある条件に適合する件数を取得する際、その抽出条件(Criteria)に指定したフィールドにインデックスが設定されていない場合、さらに深刻な状況に陥ります。データベースエンジンは件数を確定させるために、対象テーブルのすべてのレコードを先頭から順にスキャン(全件走査)せざるを得ません。レコード件数が数万件、数十万件と膨らむにつれて、わずか1回の関数呼び出しだけでもCPUとメモリに莫大な処理負荷を強いることになります。
速度改善につながるクエリ・VBAコードの具体的な改修手法
ドメイン関数への依存から脱却し、データベースエンジンの本来の結合処理能力を活かすために、実務で導入すべき3つの主要な改修手法を解説します。
【手法1】クエリでのテーブル結合(JOIN)への置き換え
クエリの中で参照用のDLookupを使用している部分の多くは、テーブル同士のリレーショナル関係を明示した外部結合(LEFT JOIN)や内部結合(INNER JOIN)へと置き換えることが可能です。これにより、データベースエンジンは一度のアクセスで関連データをまとめて一括取得するため、余分な接続と切断が一切発生しなくなります。
| 評価軸 | ドメイン関数(改善前) | テーブル結合(改善後) | DAO Recordset(VBA) |
|---|---|---|---|
| 処理の仕組み | 1レコードごとに独立したSQLを発行する | RDBMSの機能で一括結合して取得する | データをメモリ上に一括展開して巡回する |
| 実行速度 | 非常に遅い(件数依存) | 極めて高速 | 高速(メモリ処理) |
| コード量 | 非常に少ない(一行で記述可能) | クエリデザイナーで視覚的に完結 | やや多め(VBA記述が必要) |
| 最適な用途 | レコード数が極小の画面、単一の変数取得 | 帳票画面、集計対象の基礎クエリ | 複雑な分岐を伴う一括バッチ処理 |
【手法2】VBAでのRecordset(DAO)の一括展開と巡回
VBAのループ処理(Do Until LoopやFor…Nextなど)の内部で、DLookupやDCountを何度も繰り返し呼び出すのは、パフォーマンスを最悪にする典型的なアンチパターンです。この状況は、DAO(Data Access Objects)を活用してデータをメモリ上にあらかじめ一括で読み込み、その内部を巡回・探索するコードへ改修することで劇的に改善します。
▼ 改善前(アンチパターン:毎回DBへ問い合わせを行う)
Dim i As Long
For i = 1 To 1000
' ループのたびに裏側で接続処理と検索処理が発生する
Me("TxtName" & i) = DLookup("ItemName", "tblItems", "ItemID = " & i)
Next i
▼ 改善後(DAO Recordsetを用いたメモリ上での高速アクセス)
Dim db As DAO.Database
Dim rs As DAO.Recordset
Set db = CurrentDb
' 読み取り専用のスナップショットとして一括ロード
Set rs = db.OpenRecordset("tblItems", dbOpenSnapshot)
Dim i As Long
For i = 1 To 1000
rs.FindFirst "ItemID = " & i
If Not rs.NoMatch Then
Me("TxtName" & i) = rs!ItemName
End If
Next i
rs.Close
Set rs = Nothing
Set db = Nothing
このようにあらかじめバッファをメモリ上に構築してから探索を実行することで、不要なディスクI/O(ファイルの入出力)が最小化され、体感速度を大幅に引き上げることができます。
【手法3】DLookupの代わりに「サブクエリ」を利用する
帳票や画面の元データとなるクエリで、関連マスタから特定の値だけを参照したい際、通常の結合を行うとクエリ結果そのものが「更新不可能(編集不可)」になってしまうことがあります。このようなケースでは、クエリのフィールド選択部分にSQLのスカラサブクエリ(副問合せ)を記述します。データベースエンジンによる最適化機能が適用されやすいため、ドメイン関数を使う場合よりも格段に俊敏な応答を期待できます。
実務で直面しやすいトラブル事例と段階的な解決プロセス
実務の現場で発生しがちな、具体的な遅延トラブルの例と、システムを健全化させるためのアプローチ手順をご紹介します。
事例①:帳票フォームをスクロールするとフリーズする
【状況】
継続的に使用されている売上推移画面(帳票表示形式)において、各行に顧客の「最終来店日」や「顧客分類」を表示するためのDLookup式が埋め込まれたテキストボックスが複数存在していました。ユーザーがスクロールバーを動かすたびに再描画の計算が走り、動作が非常に重くなったり、応答なし状態に陥ったりしていました。
【解決へのアプローチ】
- データ構造の可視化:現在フォームの「レコードソース」に直接登録されている、未結合の単一テーブルを確認。
- 結合クエリの新規作成:「qry_SalesSummary」を新規に立ち上げ、売上テーブルと顧客マスタテーブルを「LEFT JOIN」にて連結。必要な情報を一つのビューとして合成。
- コントロールのソース変更:フォーム側に配置された計算テキストボックスの式をすべて削除し、作成した結合クエリに存在する実際のフィールド名へと割り当てを変更。
- 結果:描画に伴う無駄なクエリ発行がゼロとなり、どれほど激しくスクロールしても瞬時にデータが表示される快適な画面に生まれ変わりました。
事例②:夜間の一括バッチ更新が、データ量増加により朝までに完了しない
【状況】
他システムから取り込んだ数万件の販売データをマスタ情報と突合し、該当マスタがない場合は追加し、ある場合は数量を合算するVBAプログラム。このループ処理の中で「DCount(“*”, “tblMaster”, “Code = ‘” & targetCode & “‘”)」を実行して重複チェックを行っていたため、データの増加とともに1トランザクションあたりの処理速度が低下し、夜間帯に処理が終わらなくなっていました。
【解決へのアプローチ】
- ロジックの見直し:VBAの「1件ずつ読み込んで確認する方式」を全面的に廃止。
- 一括処理SQLの組み立て:「不一致追加クエリ(新規追加分を抽出して一括投入するSQL)」および「一括更新クエリ(既存データを結合して合算するSQL)」の2つを定義。
- プログラムの軽量化:VBA側のループ処理を全て削除し、作成した2つのアクションクエリを「DoCmd.RunSQL」または「CurrentDb.Execute」を用いて一括実行する単純なフローへと改変。
- 結果:これまで完了までに数時間(最悪の場合は数日)を要していた一括バッチ処理が、データベースエンジンのバッチ処理機能に委ねられた結果、わずか「10秒未満」で完了するようになりました。
開発効率とパフォーマンスを両立させるシステム設計の心得
ドメイン関数は機能として簡易的であるため、絶対に利用してはならないわけではありません。重要なのは、適切な適材適所の判断ルールを開発者が持つことです。
ドメイン関数を利用して差し支えないケース
- レコード数が常に1件に制限されている「初期設定マスタ」や「メニューコントロールパネル」のような画面。
- 新規入力用フォーム(単票形式)において、特定の製品コードを入力した瞬間のイベント(AfterUpdate)で、関連情報(単価など)を単発で取得する場合。
ドメイン関数の使用を回避し、設計を見直すべきケース
- データの表示件数が任意に変動するすべての「一覧形式画面」や「サブフォーム」。
- クエリ内の「抽出条件(WHERE句)」や「選択列(SELECT句)」の中に埋め込む行為。
- VBA内の繰り返し文(ループ処理)における条件判定での呼び出し。
下支えとなる「インデックス設計」の点検
どのような高速化ロジック(JOIN結合やRecordsetの活用)を組んだとしても、テーブル間の紐付けを行うキーフィールド(顧客IDやコード番号など)にインデックスが登録されていなければ、期待通りの成果は出ません。コードを書き換えるタイミングで、関係する全テーブルのデザインを確認し、検索キーとなる項目に適切なインデックスを設定する習慣をつけましょう。これだけで、既存システムの実行速度が何十倍にも向上することが珍しくありません。
Q&A(よくある質問)
DLookupの代わりにDFirstやDLastを使えば処理は軽くなりますか?
いいえ、軽くなりません。DFirstやDLast、あるいはDSumなどもすべてドメイン集計関数であり、内部で全く同じように暗黙的な接続処理とテーブルの個別読み出しを行います。速度の向上を目指す場合は、関数の変更ではなく、クエリのテーブル結合(JOIN)への移行、またはDAO Recordsetによる一括処理へのアーキテクチャ変更が不可欠です。
テーブル結合(JOIN)に切り替えたら、フォーム上でデータが更新できなくなりました。
クエリで複数テーブルを結合すると、結合の構造(多対多の関連、または主キーの設定漏れなど)によってはクエリ自体が「更新不可(読み取り専用)」の状態になります。この場合、画面用のクエリは参照用として結合で組み立て、データ更新処理はフォームの更新前イベントなどでVBAを介して個別に行うか、コンボボックスの「値集合ソース」機能を利用して表示を工夫するといった回避アプローチを採用してください。
既存の巨大なAccessシステムがあります。どこから優先して手をつけるべきですか?
最優先すべきは「エンドユーザーが業務の中で頻繁に使用し、画面表示や読み込みに明確なストレス(待ち時間)が生じている帳票画面」です。次に「夜間処理やインポート処理など、定期的に長時間の待ち時間が発生しているVBAバッチ」を探し、その内部のDCountやDLookupを撤去していきましょう。あまり使われない画面や、1件のみを表示する単票フォーム内の処理は改修優先度を下げて問題ありません。
数千件程度のデータ量でも、クエリ結合への改修は必要ですか?
初期段階では体感速度に大きな差が出ない可能性もあります。しかし、Accessはローカルネットワークを介してファイル共有(割当ドライブなど)で使われることも多く、データ量の微増やネットワーク帯域の混雑によって、ドメイン関数の遅延が突然顕在化します。将来的なスケールや運用中の安定性を鑑みるならば、初期段階から「JOIN結合」や「DAO一括処理」を開発標準として定めておくことを強くおすすめします。