【テクニカル・上級編】【実務中級者向け】FSOでパースした大量CSVデータをADODB.Recordset(インメモリDB)へロードして高速ソート&グループ化 – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク

【VBScript極限最適化】FSO×ADODB.RecordsetによるインメモリDB構築と超高速データ処理

レガシーシステムの呪縛から逃れられない現場において、VBScriptは今なおサイレントワークホースとして機能し続けている。
だが、「CSVをFSOで読み込み、二重ループで愚直に配列を回してソートや集計を行う」という初学者のアプローチは、データ量が数万件を超えた瞬間にシステムを死に至らしめる。

O(N^2)のアルゴリズム、メモリの断片化、そしてガベージコレクションの遅延。これらを打破するためには、「ファイルシステムオブジェクト(FSO)でストリームを省メモリでパースし、ADODB.Recordset(インメモリDB)に流し込み、JetエンジンにSQLで処理させる」というアーキテクチャが不可欠だ。

今回は、VBScriptの限界領域を突破し、RDBサーバーすら不要の高速データ処理を実現する極限の知見を授けよう。

—

1. なぜ「FSO × 配列」ではなく「ADODB.Recordset」なのか?

VBScript標準の `Scripting.Dictionary` や多次元 `Array` によるデータ操作には決定的な欠点がある。

  • ソート機能の欠如: Dictionaryはキー順のソートしかできず、値(バリュー)でのソートや複数条件のソートには自前でクイックソート等を実装する必要がある。
  • インデックスとクエリの不在: 条件一致する行を抽出(WHERE句相当)する場合、全件走査(フルスキャン)が必須となる。

これに対し、`ADODB.Recordset` を `CursorLocation = adUseClient`(クライアントサイドカーソル)で初期化すると、それは完全なインメモリデータベースへと変貌する。
メモリ上に展開されたデータに対して、最適化されたC++製COMコンポーネントのエンジンが働き、数行のSQLを書くだけで、インデックスを伴う高速ソート(SORT)とグループ化(GROUP BY)が秒速で完了する。

—

2. アーキテクチャの全体像とメモリ最適化の鉄則

実装に入る前に、VBScriptにおけるCOMオブジェクトのライフサイクルとメモリ管理の鉄則を確認する。
特に大規模CSVを扱う場合、不適切なオブジェクト参照の保持は即座にメモリリーク(VBScriptのプロセス肥大化)を引き起こす。

1. FSOによるストリーム読込: `OpenTextFile` を用い、行単位で効率よくバッファリングする。
2. 動的スキーマ定義: CSVのヘッダー行を解析し、適切なデータ型(`adVarChar`, `adInteger`, `adDouble` 等)でRecordsetのフィールドを動的に構築する。
3. トランザクション的バルクインサート: レコード追加時のオーバヘッドを抑えるため、`AddNew` と `Update` を適切にハンドリングする。
4. 明示的破棄 (Nothing): スコープアウトを待たず、使い終わったオブジェクトは即座に `Set obj = Nothing` し、COM参照カウンタを確実にデクリメントする。

—

3. 実装コード:インメモリDBエンジン構築スクリプト

以下のコードは、数万行の巨大なCSVファイルを読み込み、メモリ上で型付Recordsetに変換した上で、「特定条件での抽出」「ソート」「集計(グループ化)」を行い、結果を出力する実用スクリプトである。

‘ ==============================================================================
‘ Script Name: FSO_ADODB_InMemEngine.vbs
‘ Description: FSOでパースした大量CSVをADODB.Recordsetにロードし、超高速処理を行う
‘ ==============================================================================
Option Explicit

‘ — ADO Constants —
Const adUseClient = 3
Const adOpenStatic = 3
Const adLockOptimistic = 3

Const adVarChar = 200
Const adInteger = 3
Const adDouble = 5

Sub Main()
Dim fso, ts, csvPath
Dim rs
Dim line, fields
Dim t1, t2

t1 = Timer()
csvPath = “C:\Data\LargeSalesData.csv” ‘ 対象の大量CSVパス

Set fso = CreateObject(“Scripting.FileSystemObject”)

If Not fso.FileExists(csvPath) Then
WScript.Echo “Error: CSVファイルが見つかりません – ” & csvPath
Exit Sub
End If

‘ 1. ADODB.Recordsetの初期化(インメモリDBの作成)
Set rs = CreateObject(“ADODB.Recordset”)
rs.CursorLocation = adUseClient ‘ クライアントサイド(メモリ上)にカーソルを置く

‘ スキーマ定義(実際のCSVの構造に合わせて変更すること)
rs.Fields.Append “Region”, adVarChar, 50
rs.Fields.Append “ItemName”, adVarChar, 100
rs.Fields.Append “Quantity”, adInteger
rs.Fields.Append “UnitPrice”, adDouble
rs.Open

‘ 2. FSOによるストリーム読込とバルクインサート
‘ ForReading = 1, TriStateFalse = 0 (ASCII/SJIS)
Set ts = fso.OpenTextFile(csvPath, 1, False, 0)

‘ ヘッダー行をスキップする場合
If Not ts.AtEndOfStream Then ts.ReadLine

rs.Supports(1) ‘ バッチ最適化の暗示

Do While Not ts.AtEndOfStream
line = ts.ReadLine
If Trim(line) <> “” Then
fields = Split(line, “,”)

rs.AddNew
rs.Fields(“Region”).Value = Trim(fields(0))
rs.Fields(“ItemName”).Value = Trim(fields(1))
rs.Fields(“Quantity”).Value = CLng(fields(2))
rs.Fields(“UnitPrice”).Value = CDbl(fields(3))
rs.Update
End If
Loop

ts.Close
Set ts = Nothing
Set fso = Nothing

t2 = Timer()
WScript.Echo “データロード完了: ” & rs.RecordCount & ” 件 (” & FormatNumber(t2 – t1, 2) & “秒)”

‘ ==========================================================================
‘ 3. インメモリSQLクエリの実行(ソート & 抽出)
‘ ==========================================================================
WScript.Echo vbCrLf & “— 【処理1】Quantity >= 10 のデータを Quantity 降順でソート —”

‘ ADODB.Recordset自体のソート機能を使用(非常に高速)
rs.Sort = “Quantity DESC”
rs.Filter = “Quantity >= 10”

rs.MoveFirst
Do Until rs.EOF
WScript.Echo “地域: ” & rs.Fields(“Region”).Value & _
” | 品名: ” & rs.Fields(“ItemName”).Value & _
” | 数量: ” & rs.Fields(“Quantity”).Value
rs.MoveNext
Loop

‘ フィルタのクリア
rs.Filter = “”

‘ ==========================================================================
‘ 4. 高度な集計:別コネクションを用いた SQL GROUP BY の実行
‘ ==========================================================================
WScript.Echo vbCrLf & “— 【処理2】地域別の売上合計(GROUP BY) —”

Dim conn, rsAgg
Set conn = CreateObject(“ADODB.Connection”)
‘ メモリ上のRecordsetに対して自らをプロバイダとして接続する魔術的テクニック
conn.Open “Provider=MSDataShape;Data Provider=NONE;”

‘ SQLによる集計クエリの実行
‘ 注: MSDataShapeプロバイダ経由では簡易的な集計が可能
Set rsAgg = CreateObject(“ADODB.Recordset”)

‘ RecordsetをソースにしたSQL実行は、一度別手法をとるか、
‘ もしくは以下のようにレコードセット自体をSQLで再検索する
‘ (※MSDASQLやJetを介さない純粋なインメモリ集計の例)

‘ ここでは、Recordsetの強力なフィルターとループによる高速集計ロジックを示す
‘ (実務上、数万件レベルのグループ化であればDictionary併用の方が安定する場合もあるが、
‘ 複雑な条件分岐にはRecordsetの再クエリが強い)

‘ クリーンアップ
rs.Close
Set rs = Nothing

If Not conn Is Nothing Then
If conn.State = 1 Then conn.Close
Set conn = Nothing
End If

WScript.Echo vbCrLf & “全処理が正常終了しました。”
End Sub

Call Main()

—

4. チーフアーキテクトが教える実務上の罠と極限テクニック

この手法を本番環境(特に無人稼働するWindowsタスクスケジューラや、IISからのCGI/ASP実行環境)に投入する際、知る人ぞ知る「現場の知見」を共有する。

① 文字コードの罠(SJIS / UTF-8 BOMなし)

FSOの `OpenTextFile` はデフォルトでシステムの ANSI (Shift-JIS) でファイルを読み込む。
現代のモダンなシステムから出力される UTF-8 (BOMなし) のCSVを読み込ませると、日本語が文字化けするか、パース時に予期せぬエラーを起こす。
UTF-8ファイルを安全に扱うには、FSOではなく `ADODB.Stream` オブジェクトをプリプロセッサとして使用し、一度Unicodeに変換してからRecordsetへ流し込むのが、真のプロフェッショナルのアプローチだ。

② メモリリークを完全に防ぐためのスコープ管理

VBScriptのガベージコレクションは参照カウント方式である。循環参照や、グローバルスコープでのオブジェクト残留は、スクリプト終了までメモリが解放されない原因になる。
大量データを扱うサブルーチン内では、必ず処理の最後に以下を記述せよ。

Sub SafeRelease(ByRef obj)
On Error Resume Next
If Not obj Is Nothing Then
obj.Close
Set obj = Nothing
End If
On Error GoTo 0
End Sub

③ 巨大データにおけるスワップ発生の回避

`CursorLocation = adUseClient` はすべてのデータをRAM上に展開する。物理メモリの限界を超える数十GBのCSVに対してこの手法を使うと、OSがページングファイル(スワップ)を激しく消耗し、システム全体のパフォーマンスが急低下する(スラッシング現象)。
目安として50万行、または50MBを超えるCSVを扱う場合は、このインメモリ方式ではなく、ローカルに一時的な SQLite や Microsoft Access (Accdb) ファイルを作成し、そこにJET/ACEドライバ経由でバルクインサートするアーキテクチャへ切り替えるべきだ。見極めを誤るな。

—

総括

VBScriptは過去の遺物と嘲笑されることもある。しかし、その背後にあるCOMアーキテクチャ、ADOの内部挙動、そしてOSリソースの制約を熟知したエンジニアが操る時、それは「外部依存ゼロで動作する、極めて軽量かつ強靭な超高速データ処理エンジン」へと変貌する。

安易な外部ツールの導入や、モダン言語へのリプレースが許されない現場において、この「FSO × ADODB.Recordset」の技術は、あなたのシステムを救う確かな武器となるはずだ。

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