【実務・中級編】ADOでSQL Serverのテーブル値を一括取得:配列への格納による高速なデータ処理 – Access VBA解析バイブル

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

ループを捨て、メモリを支配せよ:ADO `GetRows` による超高速データ転送の極意

業務アプリケーションの設計において、多くの開発者が陥る「致命的な落とし穴」がある。それは、「Recordsetを一行ずつループして、変数に詰め替える」という、一見正しく、しかし極めて非効率な実装だ。

数千件程度のデータなら、その差は微々たるものかもしれない。しかし、SQL Serverをバックエンドに据え、数万、数十万件のデータを扱うフェーズに入ったとき、その設計はアプリケーションの応答性を破壊する「時限爆弾」へと変貌する。

今日は、真のプロフェッショナルとして、Recordsetという「対話型インターフェース」を捨て、`GetRows`メソッドによって「一括メモリ転送」を実装するための、極限の知見を伝授しよう。

1. なぜあなたのコードは遅いのか:アーキテクチャの視点

まず、DAOとADOの決定的な違いを理解せよ。

  • DAO (Data Access Objects): Accessのエンジン(Jet/ACE)を直接操作することに特化している。ローカルなファイル操作には強いが、ネットワーク越しのリモートDB操作には向かない。
  • ADO (ActiveX Data Objects): 分散環境、つまりSQL Serverなどの外部データベースとの通信を前提とした設計だ。

ADOを用いてSQL Serverからデータを取得する際、`Do Until rs.EOF` のようなループを書くということは、「アプリケーションとデータベース・ドライバーの間で、データの往復(コンテキストスイッチ)を、レコードの数だけ繰り返す」ということを意味する。

1行ごとに「次のデータはあるか?」「値は何か?」という問い合わせがネットワーク層を駆け巡る。この通信オーバーヘッドこそが、パフォーマンスを殺している真犯人だ。

2. `GetRows` メソッド:一括転送による「通信の最小化」

プロフェッショナルが選ぶ道は、`GetRows` メソッドだ。
このメソッドは、Recordsetの内容を、一撃で2次元配列(Variant型)へとメモリ展開する。

ループによる「逐次取得」が $O(n)$ 回の通信コストを要するのに対し、`GetRows` は理論上 $O(1)$ の通信で済む。データがメモリ上にドサッと降ってくる感覚だ。この「通信回数の劇的な削減」こそが、爆速の鍵となる。

【警告】配列の「次元」の罠

`GetRows` を使う際、初心者が必ずと言っていいほど陥る罠がある。それは、返される配列の構造だ。
通常のプログラミング感覚では `Array(行, 列)` を期待するだろうが、`GetRows` が返すのは `Array(列, 行)` だ。

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

この「転置された構造」を理解せずに実装を進めると、論理バグの温床となる。設計段階でこの仕様を脳に刻み込んでおけ。

3. プロダクション・クオリティのコード実装

以下に、実務でそのまま使える、堅牢で保守性の高い実装例を示す。
単なるコピペで終わらせるな。エラーハンドリング、オブジェクトの解放、そして「なぜこう書くのか」という設計思想を読み解け。

Option Explicit

' ==============================================================================
' Module: modDataService
' Description: SQL Serverからの高速データ取得モジュール
' ==============================================================================

''' <summary>
''' SQL Serverから指定したクエリの結果を2次元配列として一括取得する。
''' </summary>
''' <param name="connectionString">接続文字列</param>
''' <param name="sql">実行するSQL文</param>
''' <returns>2次元配列 (Variant)。データがない場合はEmptyを返す。</returns>
Public Function FetchDataToVariantArray(ByVal connectionString As String, _
                                       ByVal sql As String) As Variant
    
    Dim conn As ADODB.Connection
    Dim rs As ADODB.Recordset
    Dim dataArray As Variant
    
    ' エラーハンドリングの徹底:プロフェッショナルの作法
    On Error GoTo ErrorHandler

    ' 1. オブジェクトの生成
    Set conn = New ADODB.Connection
    Set rs = New ADODB.Recordset

    ' 2. 接続の確立
    ' CursorLocationをadUseClientに設定することが重要。
    ' これにより、クライアント側のメモリ上でレコードセットを制御可能にする。
    conn.CursorLocation = adUseClient
    conn.Open connectionString

    ' 3. レコードセットのオープン
    ' LockTypeはadLockReadOnly、CursorTypeはadOpenStaticで十分。
    ' 取得のみであれば、リソース消費を最小限に抑える設定を選択せよ。
    rs.Open sql, conn, adOpenStatic, adLockReadOnly

    ' 4. データが存在するか確認
    If Not rs.EOF Then
        ' 【核心】GetRowsによる一括転送
        ' Recordsetをループせず、一気にメモリ上の配列へ流し込む。
        dataArray = rs.GetRows()
    Else
        dataArray = Empty
    End If

    ' 成功時は取得した配列を返す
    FetchDataToVariantArray = dataArray

CleanUp:
    ' 5. リソースの確実な解放(メモリリーク防止)
    ' エラーが発生しても、必ずここを通るように設計する。
    On Error Resume Next
    If Not rs Is Nothing Then
        If rs.State = adStateOpen Then rs.Close
    End If
    Set rs = Nothing
    
    If Not conn Is Nothing Then
        If conn.State = adStateOpen Then conn.Close
    End If
    Set conn = Nothing
    Exit Function

ErrorHandler:
    ' 実務ではログ出力やユーザーへの通知をここで行う
    MsgBox "データ取得中に致命的なエラーが発生しました。" & vbCrLf & _
           "Error: " & Err.Description, vbCritical, "Data Service Error"
    FetchDataToVariantArray = Empty
    Resume CleanUp
End Function

''' <summary>
''' 取得した配列をデバッグ表示する(列・行の構造確認用)
''' </summary>
Public Sub TestDataFetch()
    Dim connStr As String
    Dim sql As String
    Dim data As Variant
    Dim r As Long, c As Long

    ' 環境に合わせて書き換えよ
    connStr = "Provider=SQLOLEDB;Data Source=YOUR_SERVER;Initial Catalog=YOUR_DB;Integrated Security=SSPI;"
    sql = "SELECT TOP 100 ID, UserName, Email FROM Users ORDER BY ID"

    data = FetchDataToVariantArray(connStr, sql)

    If IsEmpty(data) Then
        Debug.Print "データが見つかりませんでした。"
        Exit Sub
    End If

    ' GetRowsの配列構造は Array(Column, Row) であることに注意!
    ' LBound/UBoundを使用して、動的なサイズに対応させる。
    Dim maxCol As Long: maxCol = UBound(data, 1)
    Dim maxRow As Long: maxRow = UBound(data, 2)

    Debug.Print "取得件数: " & (maxRow + 1) & " 件"
    Debug.Print "列数: " & (maxCol + 1) & " 列"

    ' 高速なメモリ内ループ
    For r = 0 To maxRow
        For c = 0 To maxCol
            ' 出力例: [Row 0, Col 1] = Value
            Debug.Print "Row " & r & ", Col " & c & ": " & data(c, r);
        Next c
        Debug.Print "" ' 改行
    Next r
End Sub

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

この手法をマスターした君が、次に意識すべきは「堅牢性」だ。

1. CursorLocation の選択

`conn.CursorLocation = adUseClient` を忘れてはならない。サーバーサイドカーソルを使用すると、`GetRows` によるクライアント側への一括転送が最適化されない場合がある。クライアント側でレコードセットを保持することで、`GetRows` の真価が発揮される。

2. メモリ消費量とデータサイズ

`GetRows` は「すべてのデータを一度にメモリに載せる」手法だ。もし、数千万件の巨大なテーブルに対して、WHERE句もなしに `GetRows` を実行すれば、Access(あるいは実行環境)はメモリ不足(Out of Memory)で即座にクラッシュする。
「フィルタリング(WHERE句)は、常にSQL Server側で行わせる」。これは鉄則だ。

3. トランザクションの境界

今回の例は参照(Read)だが、もし更新(Write)を伴う設計にするなら、トランザクションの範囲を最小限にせよ。`GetRows` で取得した配列をメモリ上で加工し、その結果を再び一括で更新する(バッチ更新)という設計にすれば、データベースへの負荷とロック時間を劇的に抑えることができる。

まとめ:エンジニアとしての矜持

「動けばいい」というコードを書くのは、ただの作業員だ。
「なぜ速いのか」「なぜ壊れないのか」を論理的に説明でき、アーキテクチャのレベルで最適解を導き出せる者こそが、エンジニアである。

`Recordset` のループを捨て、`GetRows` によるメモリ操作を極めること。それが、大規模な業務システムを支える、プロフェッショナルへの第一歩だ。

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