Recordsetの「ループ」を捨てよ:GetRowsによるメモリ局所性の極致と、DAOアーキテクチャの真実
多くの開発者が、Access VBAにおけるデータ処理を「Recordsetを1行ずつ `MoveNext` で回すこと」だと誤解している。
もし君が、数万件のレコードを扱うシステムにおいて、`Do Until rs.EOF` という構文を漫然と使い続けているのなら、君はエンジニアとして致命的な損失を招いている。それは単なる「書き方の問題」ではない。データエンジンとアプリケーション間の通信オーバーヘッド、およびメモリ局所性(Memory Locality)を完全に無視した、設計上の敗北である。
本稿では、DAOのRecordsetを「配列」へと一括変換し、処理速度を桁違いに向上させる `GetRows` メソッドの真髄を解説する。これは、レガシーな環境を現代的なパフォーマンス水準へと引き上げるための、極めて実戦的なアーキテクチャ戦略である。
1. なぜ `MoveNext` は遅いのか:カーソル移動の物理的コスト
なぜ `Do Until rs.EOF` は遅いのか。その理由は、Recordsetが「データそのもの」ではなく、データベースエンジン(ACE/Jet)への「窓口(インターフェース)」に過ぎないからだ。
`MoveNext` を実行するたびに、VBAの実行エンジンは以下のプロセスを踏む:
1. DAOオブジェクトを介して、データベースエンジンへ次行のポインタ移動を要求する。
2. エンジン側でカーソル位置を更新し、要求されたフィールドのメタデータをメモリ上にロードする。
3. VBAのVariant型へとデータをブリッジ(型変換)する。
この「オブジェクトを介した往復」が、ループの回数分だけ繰り返される。10万回のループがあれば、10万回のコンテキストスイッチに近いオーバーヘッドが発生するのだ。我々が目指すべきは、この「窓口への問い合わせ」を最小化し、データを一気にVBAのメモリ空間(ヒープ領域)へ引き込むことである。
2. `GetRows` によるバルク・フェッチ戦略
ここで登場するのが `GetRows` メソッドである。このメソッドは、Recordsetが保持する全レコード(または指定した件数)を、一度の呼び出しで Variant型の2次元配列 に一括転送する。
これにより、データベースエンジンとの通信は「1回」で完結する。データはVBAのメモリ上に展開され、以降の処理は低レイヤーのメモリ操作に近い速度で行われる。これが、ループ処理を10倍、あるいはそれ以上に高速化させる理論的根拠である。
3. 実装例:速度差を可視化する
以下のコードは、標準的な `MoveNext` による処理と、`GetRows` を用いた配列処理のパフォーマンスを比較するためのベンチマークである。
Option Explicit
' ======================================================================================
' 性能検証用モジュール
' 目的: Recordsetの逐次走査とGetRowsによる一括取得の速度差を測定する
' ======================================================================================
Public Sub Benchmark_RecordsetProcessing()
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim i As Long
Dim startTime As Double
Dim totalRows As Long
' テストデータの準備(事前に大量のレコードを持つテーブルを用意しておくこと)
Set db = CurrentDb
Set rs = db.OpenRecordset("SELECT ID, TargetValue FROM tblLargeData", dbOpenSnapshot)
If rs.EOF Then
MsgBox "データが存在しません。"
Exit Sub
End If
rs.MoveLast
totalRows = rs.RecordCount
rs.MoveFirst
Debug.Print "--- Benchmark Start: " & totalRows & " rows ---"
' --- パターン1: 従来の逐次処理 (MoveNext) ---
startTime = Timer
Dim sum1 As Double
sum1 = 0
Do Until rs.EOF
sum1 = sum1 + rs!TargetValue
rs.MoveNext
Loop
Debug.Print "Pattern 1 (MoveNext): " & Format(Timer - startTime, "0.000") & " sec (Sum: " & sum1 & ")"
' --- パターン2: GetRows による配列処理 ---
rs.MoveFirst
startTime = Timer
' Recordsetの全データを一括で2次元配列に格納
Dim dataArray As Variant
dataArray = rs.GetRows
Dim sum2 As Double
sum2 = 0
' GetRowsの特性: 配列のインデックスは [Column, Row] になることに注意
' 0番目の列(ID)を飛ばして、1番目の列(TargetValue)を走査
Dim colIdx As Long: colIdx = 1
Dim rowIdx As Long
' LBound/UBoundを使用して配列の境界を厳密に制御
For rowIdx = LBound(dataArray, 2) To UBound(dataArray, 2)
sum2 = sum2 + dataArray(colIdx, rowIdx)
Next rowIdx
Debug.Print "Pattern 2 (GetRows): " & Format(Timer - startTime, "0.000") & " sec (Sum: " & sum2 & ")"
' --- 後処理 ---
' オブジェクトのライフサイクルを明示的に管理
rs.Close
Set rs = Nothing
Set db = Nothing
Debug.Print "--- Benchmark End ---"
End Sub
4. 極限の知見:`GetRows` の「罠」とメモリ管理
このテクニックをマスターするためには、二つの重要な知見を脳に刻んでおく必要がある。
① 配列の次元構造(Transposed Matrix)
`GetRows` が返す配列は、一般的な感覚とは異なる構造をしている。
通常、我々は `Array(Row, Column)` という構造を期待するが、`GetRows` が生成するのは `Array(Column, Row)` である。
- `dataArray(0, 5)` は、「0番目のフィールドの、6行目のデータ」を指す。
この構造を理解せずに `dataArray(row, col)` とアクセスすれば、即座に `Subscript out of range` エラーでシステムは沈黙する。この「転置された構造」こそが、メモリ上の連続性を確保し、高速な列アクセスを可能にする設計思想なのだ。
② オブジェクトのライフサイクルとメモリ解放
大規模なデータを `GetRows` で取得する場合、一時的にメモリ使用量が急増する。VBAは強力なガベージコレクションを持たないため、使い終わった `Recordset` オブジェクトや、巨大な `Variant` 配列は、明示的に `Set … = Nothing` もしくは `Erase` を行い、スコープから外すことが鉄則である。
特に、ループ内で `GetRows` を繰り返し呼び出すような設計は、メモリリーク(厳密にはメモリの断片化と枯渇)を引き起こし、Windows OS全体の不安定化を招くリスクがある。
結論:アーキテクトとしての選択
「動けばいい」というレベルのプログラマは、`MoveNext` を使い続ける。
しかし、システムの堅牢性と極限のパフォーマンスを追求するアーキテクトは、データの流れ(Data Flow)を制御し、最小のコストで最大の出力を得る。
`GetRows` によるバルク・フェッチは、Access VBAというレガシーな武器を、現代のデータ処理エンジンへと昇華させるための、最も基本的かつ強力な戦術である。君のコードに、この「集合論的思考」を取り入れてほしい。
