【実務・中級編】DAOのRecordsetを「配列」に一括変換して処理する:ループ処理の速度を10倍にするテクニック – Access VBA解析バイブル

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

Recordsetのループを捨て、メモリを支配せよ:GetRowsによる爆速データ処理アーキテクチャ

「コードは動いている。しかし、データ件数が増えると目に見えて遅くなる」

もし君が、`Do Until rs.EOF` から始まる `MoveNext` のループを、当たり前のように業務ツールに組み込んでいるとしたら、君の設計は「動くコード」であっても「プロの設計」ではない。

Access VBAにおけるパフォーマンスのボトルネックの正体は、多くの場合、アルゴリズムの複雑さではなく、「VBAエンジン」と「データベースエンジン(ACE/Jet)」の間の過剰な通信(コンテキストスイッチ)にある。

本記事では、Recordsetのカーソル移動によるオーバーヘッドを完全に排除し、処理速度を劇的に向上させる極限のテクニック、`GetRows` メソッドによる「配列一括変換」の設計思想を伝授する。

1. なぜ `.MoveNext` は遅いのか:アーキテクチャの視点

まず、君が書いているそのループが、なぜ「遅い」のかをロジカルに理解してほしい。

DAOやADOのRecordsetをループで回す際、VBAは一歩進むごとに以下のプロセスを繰り返している。

1. VBAエンジンが「次のレコードへ」と命令を出す。
2. データベースエンジンが、物理的なデータファイル(.accdb等)から該当するレコードを特定する。
3. データベースエンジンが、そのレコードのデータをメモリ上に展開する。
4. データベースエンジンからVBAエンジンへ、データが渡される。

1万件のデータがあれば、この「境界を越える通信」が1万回発生する。これがカーソル移動のオーバーヘッドだ。たとえ1件あたりの通信がマイクロ秒単位であっても、累積すれば致命的な遅延となる。

プロのアーキテクトは、この通信回数を「1回」に抑え込む。それが `GetRows` メソッドの役割だ。

2. GetRowsによる「一括転送」のメカニズム

`GetRows` メソッドは、Recordsetが保持している全レコード(または指定した件数)を、一撃でメモリ上の2次元配列へと放り込む。

これにより、データベースエンジンとの通信は「1回」で終了する。残りの処理は、VBAが最も得意とする「メモリ上の配列操作」に完全に委ねられる。この「通信の最小化」こそが、速度を10倍以上に跳ね上げる鍵だ。

【警告】GetRowsの「罠」を回避せよ

`GetRows` は強力だが、初心者が必ず陥る罠が2つある。

1. インデックスの順序が逆:
通常の感覚では `Array(行, 列)` だと思うだろう。しかし、`GetRows` が返す配列は `Array(列, 行)` である。ここを間違えると、デバッグに膨大な時間を溶かすことになる。
2. メモリ・フットプリント:
数百万件のデータを一度に `GetRows` すれば、メモリ不足(Out of Memory)でアプリケーションは即座にクラッシュする。データ量に応じた「バッチ処理」の検討が必要だ。

3. 実戦投入:堅牢な高速データ取得モジュール

以下に、実務のプロダクションコードとしてそのまま使える、堅牢な実装例を示す。
単に `GetRows` を使うだけでなく、エラーハンドリングと、扱いやすい「行・列」形式への変換を考慮した設計にしている。

Option Explicit

''' <summary>
''' DAO.Recordsetのデータを、扱いやすい「行・列」形式の2次元配列に一括変換する。
''' </summary>
''' <param name="rs">ソースとなるDAO.Recordset</param>
''' <returns>Variant型の2次元配列 [行, 列]。データがない場合はEmptyを返す。</returns>
Public Function GetRecordsetAsArray(ByRef rs As DAO.Recordset) As Variant
    On Error GoTo ErrorHandler

    ' 1. Recordsetの状態チェック
    If rs Is Nothing Then
        GetRecordsetAsArray = Empty
        Exit Function
    End If

    If rs.EOF And rs.BOF Then
        GetRecordsetAsArray = Empty
        Exit Function
    End If

    ' 2. GetRowsによる一括取得
    ' 注意: GetRowsは [列, 行] の形式で配列を返す
    Dim rawData As Variant
    rawData = rs.GetRows()

    ' 3. 配列の次元取得
    Dim colCount As Long: colCount = UBound(rawData, 1)
    Dim rowCount As Long: rowCount = UBound(rawData, 2)

    ' 4. [列, 行] から [行, 列] へ組み替える(プログラマの使いやすさ優先)
    ' これにより、通常のループ処理 own[i, j] が可能になる
    Dim processedArray As Variant
    ReDim processedArray(0 To rowCount, 0 To colCount)

    Dim r As Long, c As Long
    For r = 0 To rowCount
        For c = 0 To colCount
            processedArray(r, c) = rawData(c, r)
        Next c
    Next r

    GetRecordsetAsArray = processedArray
    Exit Function

ErrorHandler:
    ' ログ出力などのエラーハンドリングをここに実装
    Debug.Print "Error in GetRecordsetAsArray: " & Err.Description
    GetRecordsetAsArray = Empty
End Function

''' <summary>
''' 実装テスト用メインルーチン
''' </summary>
Public Sub TestHighSpeedProcess()
    Dim db As DAO.Database
    Dim rs As DAO.Recordset
    Dim dataArray As Variant
    Dim i As Long, j As Long
    
    Set db = CurrentDb
    ' テスト用のSQL(実際の実務に合わせて変更)
    Set rs = db.OpenRecordset("SELECT * FROM T_LargeData", dbOpenSnapshot)

    ' --- 高速化の核心 ---
    ' Recordsetをループせず、一括で配列へ
    dataArray = GetRecordsetAsArray(rs)
    
    ' Recordsetは即座に閉じる(メモリ解放とロック回避)
    rs.Close
    Set rs = Nothing

    ' 配列が取得できたか確認
    If IsEmpty(dataArray) Then
        MsgBox "データが存在しません。", vbInformation
        Exit Sub
    End If

    ' --- メモリ上での高速計算・加工処理 ---
    ' ここでのループは、データベースへの通信が発生しないため極めて高速
    For i = LBound(dataArray, 1) To UBound(dataArray, 1)
        ' 例: 1列目の値に基づいたロジック
        ' If dataArray(i, 0) > 100 Then ...
    Next i

    MsgBox "処理完了。取得件数: " & (UBound(dataArray, 1) + 1) & " 件", vbInformation

    Set db = Nothing
End Sub

4. アーキテクトからの設計指針

このテクニックを導入する際、君が守るべき「設計の鉄則」を伝える。

① 「読み取り」と「書き込み」を分離せよ

`GetRows` は読み取り専用の高速化手法だ。配列に取り込んだ後は、データベースとの接続を一旦断ち切る(`rs.Close`)のが理想的な設計である。
「配列でデータを加工」→「加工が終わった後に、新しいRecordsetを開いて一括で書き込む」という、Read-Process-Writeの分離を徹底せよ。これにより、トランザクションの範囲を最小限に抑え、データベースのロック競合を防ぐことができる。

② メモリ使用量の予測

`GetRows` は全データをメモリに載せる。
例えば、1行が1KBのデータが10万行ある場合、それだけで約100MBのメモリを消費する。Access(32bit版)のメモリ空間には限界があることを忘れるな。データ件数が極端に多い(例:数百万件)場合は、`GetRows` の引数に件数を指定し、例えば「1万件ずつ取得して処理する」というチャンク(Chunk)処理を設計に組み込むべきだ。

③ 型の安全性

`GetRows` で取得した配列には、データベースの `Null` が含まれる可能性がある。VBAの配列内で `Null` を扱うと、型不一致のエラーを招きやすい。
配列から値を取り出す際は、`Nz()` 関数のようなガードを常に意識するか、あるいは型変換を厳格に行う設計にせよ。

結論

`MoveNext` によるループは、小規模なツールなら許容される。しかし、エンタープライズレベルの業務システムを構築するのであれば、それは「怠慢」に等しい。

「通信を最小化し、メモリを支配する」

この思想をコードに落とし込めるかどうかが、単なるプログラマーと、信頼されるアーキテクトを分かつ境界線である。次回の開発からは、迷わず `GetRows` を選択せよ。

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