【テクニカル・上級編】DAOのRecordsetを配列に一括格納:ループ処理を10倍速くするメモリ操作術 – Access VBA解析バイブル

スポンサーリンク
スポンサーリンク

エンジン・オーバーヘッドを殺せ:`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` を使いこなし、データエンジンからロジックを切り離すこと。それこそが、低レイヤーの挙動を理解し、真にスケーラブルなシステムを構築できるシニアエンジニアの証である。

メモリを支配する者が、パフォーマンスを支配するのだ。

タイトルとURLをコピーしました