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

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

ループ処理の呪縛を解け:`GetRows`によるDAO Recordsetの高速メモリ操作術

業務システムを構築していると、必ずと言っていいほど「データ量が増えると極端に動作が重くなる」という壁にぶち当たる。

多くの開発者は、その原因を「SQLのインデックス不足」や「ネットワークの遅延」に求めようとする。もちろんそれらも要因にはなり得るが、真にプロフェッショナルが疑うべきは、VBAのコードそのものが持つ「通信回数の多さ(Chatty Interface)」だ。

もし君が、`Do Until rs.EOF` を使い、ループの中で `rs.Fields(“FieldName”).Value` を呼び出しているとしたら、君のコードは「データベースエンジンとVBAエンジンの間で、一歩進むたびに何度も往復通信を行っている」状態にある。これが、数万件のデータを処理する際に、致命的なボトルネックとなる正体だ。

本稿では、DAOのRecordsetからデータを一括でメモリ上に展開する `GetRows` メソッドを使い、ループ処理を劇的に高速化する、極限の設計手法を伝授する。

1. なぜ「標準的なループ」は遅いのか:アーキテクチャの視点

まず、なぜ君の書いたループが遅いのか、そのメカニズムを理解せよ。

ボトルネックの正体:コンテキストスイッチ

DAOのRecordsetを1レコードずつ回す処理は、VBAの実行エンジンから、データベースエンジン(ACE/Jet)への「問い合わせ」を繰り返す行為だ。

1. `MoveNext` を呼ぶたびに、ポインタを次に進める命令がエンジンに飛ぶ。
2. `Fields(“Name”).Value` を呼ぶたびに、フィールド名からインデックスを特定し、値をメモリへコピーする命令が飛ぶ。

1万件のレコードに対し、3つのフィールドを取得する場合、最低でも計4万回(MoveNext + Fields×3)のコンテキストスイッチが発生する。この「小さな通信の積み重ね」が、CPUの命令パイプラインを寸断し、I/O待ちを引き起こすのだ。

解決策:バルク・フェッチ(一括取得)

これに対する最適解は、「一度の通信で、必要なデータをすべてVBA側のメモリ領域(配列)に叩き込む」ことだ。これこそが `GetRows` メソッドの役割である。

2. `GetRows` メソッドの衝撃と「罠」

`GetRows` は、Recordsetの内容を一度に2次元配列へ格納するメソッドだ。これにより、ループ処理の対象を「データベース上のポインタ移動」から「メモリ上の配列走査」へと切り替えることができる。

しかし、ここで初心者が必ず陥る致命的な罠がある。

罠:配列の次元構造

通常、我々がイメージする2次元配列は `Array(行, 列)` だ。しかし、`GetRows` が生成する配列は `Array(列, 行)` である。

  • `myArray(0, 5)` は、「0番目のフィールド(列)の、6行目のデータ」を指す。

この仕様を知らずに実装すると、行と列が逆転し、データの整合性が崩れる。この「設計上の違和感」を理解し、コードレベルで制御するのがアーキテクトの仕事だ。

3. プロダクション・レディな実装例

以下に、実務でそのまま使える、堅牢かつ高速な実装パターンを示す。単に `GetRows` を使うだけでなく、エラーハンドリングとオブジェクトのライフサイクル管理を徹底している点に注目してほしい。

Option Explicit

''' <summary>
''' DAO Recordsetのデータを高速に2次元配列として取得する
''' </summary>
''' <param name="rs">開いているDAO.Recordsetオブジェクト</param>
''' <returns>2次元配列 (Array(Column, Row))。データがない場合はEmpty</returns>
Public Function GetRecordsetAsArray(ByRef rs As DAO.Recordset) As Variant
    On Error GoTo Err_Handler

    ' 1. レコードセットが空でないか確認
    If rs.EOF And rs.BOF Then
        GetRecordsetAsArray = Empty
        Exit Function
    End If

    ' 2. GetRowsによる一括メモリ展開
    ' これにより、DBエンジンとの通信は「この1回」で済む
    GetRecordsetAsArray = rs.GetRows()

    Exit Function

Err_Handler:
    ' 呼び出し元にエラーを伝播させる
    Err.Raise Err.Number, "GetRecordsetAsArray", Err.Description
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 r As Long, c As Long
    
    ' パフォーマンス計測用
    Dim startTime As Double: startTime = Timer

    On Error GoTo Err_Fatal

    Set db = CurrentDb
    ' テスト用のクエリ(大量データを想定)
    Set rs = db.OpenRecordset("SELECT ID, UserName, Amount FROM T_SalesData", dbOpenSnapshot)

    ' --- 戦略的フェッチ ---
    dataArray = GetRecordsetAsArray(rs)

    ' Recordsetはもう不要なので即座に閉じる(メモリ解放の鉄則)
    rs.Close
    Set rs = Nothing

    ' データが存在するかチェック
    If IsEmpty(dataArray) Then
        MsgBox "データが見つかりませんでした。"
        GoTo CleanUp
    End If

    ' --- メモリ上での高速ループ処理 ---
    ' GetRowsの配列構造は Array(Column, Row) であることに注意
    ' LBound/UBoundを使用して、次元を正確に把握する
    colCount = UBound(dataArray, 1) ' 列方向の最大インデックス
    rowCount = UBound(dataArray, 2) ' 行方向の最大インデックス

    Debug.Print "--- 処理開始 ---"
    
    ' 列方向(c)を外側、行方向(r)を内側にするのがメモリ局所性の観点では望ましいが、
    ' VBAの配列アクセスにおいては、やりたいロジックに合わせる。
    ' ここでは「行」を主軸に処理を行う。
    For r = 0 To rowCount
        ' 例: 0番目の列(ID)を取得
        ' Dim currentID As Long: currentID = dataArray(0, r)
        
        ' 例: 2番目の列(Amount)に基づいて計算
        ' (実際の実装では、ここで複雑なビジネスロジックを回す)
        
        ' 画面描画(DoEvents等)をループ内で行うのは厳禁。
        ' 処理が重い場合は、一定件数ごとにのみ行うこと。
    Next r

    Debug.Print "処理完了。経過時間: " & Format(Timer - startTime, "0.000") & " 秒"

CleanUp:
    On Error Resume Next
    If Not rs Is Nothing Then rs.Close: Set rs = Nothing
    Set db = Nothing
    Exit Sub

Err_Fatal:
    MsgBox "致命的なエラーが発生しました: " & Err.Description, vbCritical
    Resume CleanUp
End Sub

4. 運用設計における「極限の注意点」

この手法を導入する際、君がリーダーとしてチームに徹底させるべき注意点が3つある。

① メモリ消費量とのトレードオフ

`GetRows` はデータをすべてRAM上に展開する。もし100万件のレコードを一度に `GetRows` しようとすれば、Accessのプロセスはメモリ不足(Out of Memory)で即死する。

  • 設計指針: 大規模データの場合は、SQLの `WHERE` 句で範囲を絞るか、あるいは `TOP` 句を用いて「チャンク(塊)」ごとに分割して取得する設計にせよ。

② トランザクションの境界線

`GetRows` で取得したデータは、もはやデータベースとの接続が切れていても(Recordsetを閉じても)操作可能だ。

  • 設計指針: 取得したメモリ上のデータに基づいて計算を行い、その結果をDBに書き戻す場合は、書き戻し処理(UPDATE/INSERT)を必ずトランザクション (`BeginTrans` / `CommitTrans`) で囲め。 メモリ上のデータは「スナップショット」に過ぎず、DB側の最新状態を反映していない可能性がある。

③ 型の安全性

`GetRows` が返すのは `Variant` 型の配列だ。Null値が含まれている場合、配列の要素は `Null` となる。

  • 設計指針: 配列から値を取り出す際は、`Nz()` 関数や、明示的な `IsNull()` チェックを忘れるな。これを怠ると、計算ロジックで「型が一致しません」というエラーが多発し、保守性を著しく低下させる。

結論

`Do Until rs.EOF` は、小規模なツールであれば許容される。しかし、プロフェッショナルが構築する「業務基盤」においては、その書き方は慢心であり、怠慢である。

`GetRows` を使いこなし、I/Oの壁を突破してメモリ上でロジックを完結させる。この「データの流れ(Data Flow)」をコントロールする感覚こそが、単なるプログラマと、アーキテクトを分かつ境界線なのだ。

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