エンジン・オーバーヘッドを殺せ:`GetRows` によるメモリ内データ処理の極致
もし君が、数万件規模のデータを扱う業務アプリケーションにおいて、いまだに `Do Until rs.EOF` と `rs.MoveNext` を繰り返すループ処理を書いているとしたら、その設計には致命的な欠陥があると言わざるを得ない。
それは単なる「書き方の問題」ではない。「VBAエンジンとデータエンジン(ACE/Jet)間のコンテキストスイッチ」という、目に見えないコストを膨大な回数積み重ねているという、アーキテクチャ上の敗北だ。
本稿では、シニアエンジニアが到達すべき、DAO Recordsetからメモリ空間への一括転送術——`GetRows` メソッドを用いた極限の高速化手法について解説する。
1. なぜ「Recordsetループ」は遅いのか
多くの開発者が、Recordsetの各レコードを一件ずつ走査する手法を「自然な書き方」だと誤解している。しかし、その背後で何が起きているかを理解せよ。
`rs.Fields(“ColumnName”).Value` を呼び出すたびに、VBAは以下のプロセスを繰り返している。
1. 境界の越境(Boundary Crossing): VBAランタイムから、ACE/Jetエンジンという別プロセス(またはDLL)への呼び出しが発生する。
2. オブジェクトのインスタンス化: 各フィールドへのアクセスにおいて、内部的なプロパティ取得のオーバーヘッドが発生する。
3. ディスクI/Oとバッファリング: エンジン側でのカーソル移動に伴う、メモリ・ディスク間の同期。
10万件のレコードがあれば、この「越境」が少なくとも10万回、フィールド数分だけ繰り返される。これが、処理速度が指数関数的に低下する真の理由だ。我々が戦うべき相手は、アルゴリズムの複雑性ではなく、この「エンジン間の通信回数」なのだ。
2. `GetRows` による一括転送のパラダイムシフト
この問題を解決する唯一の解が、`GetRows` メソッドである。
`GetRows` は、Recordsetが保持しているカーソルの位置から、指定した行数分(あるいは全件)を、一撃で2次元配列へメモリ展開する。
これにより、エンジンとの通信は「1回(または数回)」に集約される。データはVBAの管理下にあるメモリ空間(Variant配列)へと完全に移送され、その後の処理は純粋なメモリ操作へと昇華される。CPUはエンジンの応答を待つ必要がなくなり、キャッシュの恩恵を最大限に受けることができる。
重要な注意点:配列の次元構造
ここで、多くの者が陥る罠がある。`GetRows` が生成する2次元配列は、一般的な「行・列」の順序ではない。
`Array(列インデックス, 行インデックス)` という構造になる。
この「転置された構造」を理解していない者は、配列操作の段階で必ずバグを出す。この特性を逆手に取り、列単位の処理(ベクトル演算に近い処理)を行うのがプロの作法だ。
3. 実装コード:極限のデータ転送ルーチン
以下に、実戦でそのまま使用できる、堅牢かつ高速な実装例を示す。単なる転送だけでなく、オブジェクトのライフサイクル管理とエラーハンドリングを徹底している。
Option Explicit
''' <summary>
''' DAO Recordsetの内容を高速に2次元配列へ格納する。
''' </summary>
''' <param name="rs">対象のDAO.Recordsetオブジェクト</param>
''' <returns>2次元配列 (Variant型: [Column, Row])。レコードがない場合はEmptyを返す。</returns>
Public Function DumpRecordsetToVariantArray(ByRef rs As DAO.Recordset) As Variant
On Error GoTo ErrorHandler
' 1. レコードが存在するかチェック
If rs.EOF Then
DumpRecordsetToVariantArray = Empty
Exit Function
End If
' 2. GetRowsによる一括メモリ転送
' 引数を省略することで、全レコードを一度に配列へ展開する。
' ※数百万件規模の極端に巨大なデータの場合、メモリ不足(Out of Memory)を避けるため
' Chunk単位(例: 50,000件ずつ)で取得する設計に変更すべきである。
DumpRecordsetToVariantArray = rs.GetRows()
Exit Function
ErrorHandler:
' エラー発生時は呼び出し元に責任を転嫁せず、ログを記録するか適切に処理せよ
Debug.Print "Critical Error in DumpRecordsetToVariantArray: " & Err.Description
DumpRecordsetToVariantArray = Empty
End Function
''' <summary>
''' 高速化されたデータ処理のデモンストレーション
''' </summary>
Public Sub ExecuteHighSpeedProcess()
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim dataArray As Variant
Dim rowCount As Long
Dim colCount As Long
Dim i As Long, j As Long
' 計測開始
Dim startTime As Double: startTime = Timer
Set db = CurrentDb
' テスト用の巨大なクエリを想定
Set rs = db.OpenRecordset("SELECT Field1, Field2, Field3 FROM LargeTable", dbOpenSnapshot)
' --- ここが核心:Recordsetをメモリへ隔離する ---
dataArray = DumpRecordsetToVariantArray(rs)
' Recordsetはこれ以上不要なので、即座に解放する。
' これにより、ファイルロックやメモリ占有を最小限に抑える。
rs.Close
Set rs = Nothing
' ----------------------------------------------
If IsEmpty(dataArray) Then
MsgBox "データが空です。"
GoTo Cleanup
End If
' GetRowsの配列構造は [列, 行] であることに注意
' UBound(dataArray, 1) は 列数 (Columns)
' UBound(dataArray, 2) は 行数 (Rows)
colCount = UBound(dataArray, 1)
rowCount = UBound(dataArray, 2)
' メモリ上での演算(エンジンへのアクセスは一切発生しない)
' ここでの処理は、通常のループに比べ数百倍から千倍速い
For i = 0 To rowCount
' 列インデックスを指定して行データを参照
' 例: Field1 (Index 0) の値を加工
' dataArray(0, i) = dataArray(0, i) * 1.1
Next i
Debug.Print "処理完了: " & rowCount & " 件"
Debug.Print "経過時間: " & Format(Timer - startTime, "0.000") & " 秒"
Cleanup:
Set rs = Nothing
Set db = Nothing
End Sub
4. アーキテクチャの深淵:メモリ管理と限界点
伝説的なエンジニアとして、私はあえて警告しておく。`GetRows` は万能薬ではない。
メモリの断片化とLOB型
`GetRows` で全件取得する際、対象のテーブルに `Memo` 型(現在の `LongText`)や `Binary` 型(`OLE Object`)などの巨大なオブジェクト(LOB: Large Object)が含まれている場合、一瞬にしてVBAのメモリ空間を食いつぶし、`Out of Memory` エラーを誘発する。
大規模システムを設計する場合、以下の戦略を採るべきだ。
1. プロジェクション(列の限定): `SELECT *` は悪である。必要な列のみを明示的に指定し、メモリ消費を最小化せよ。
2. チャンキング(分割取得): `rs.GetRows(10000)` のように、一定の行数ごとに配列へ吸い上げ、処理を回す「ストリーム処理」的なアプローチを検討せよ。
オブジェクトのライフサイクル
コード内で示した通り、`GetRows` 完了直後に `rs.Close` および `Set rs = Nothing` を行うのは、単なる作法ではない。これは、エンジンのリソースを即座に解放し、メモリの断片化を防ぐための「外科的な処置」である。特に、長期間稼働し続けるサーバーサイド的なVBAアプリケーションにおいては、この厳格さがシステムの生存率を左右する。
結論
`Do Until rs.EOF` は、プロトタイプを作るための書き方だ。
`GetRows` を使いこなし、データエンジンからロジックを切り離すこと。それこそが、低レイヤーの挙動を理解し、真にスケーラブルなシステムを構築できるシニアエンジニアの証である。
メモリを支配する者が、パフォーマンスを支配するのだ。
