🎃

CSVや固定長ファイルをバルクインサートするため、SqlBulkCopyを調査してみた

2024/12/16に公開

CSVは固定長ファイルを手軽かつ柔軟にバルクインサートする仕組みを構築しよ言うと思って、はや300年。

その準備のために主にコードを読んで調べものをしてみました。

  1. 型変換は自前でやったほうが良いのか?お任せが良いのか?
  2. DB列とファイル列のマッピング
  3. 値の取得

型変換は自前でやったほうが良いのか?お任せが良いのか?

結論:お任せでよい

CSVにしろ固定長にしろ、ソースはテキストなので文字列なのですが、これをバルクインサートする際に型変換を手でするのが良いのか?それともお任せで良いのか?

機能的にはお任せで動作するけど、性能的にどちらがベターなのかはまた別なので調べてみた。

結果的に内部でSQL Serverへの送信前に型変換を実施していることが分かった。

SQL Serverのクエリーパラメーターのような形で送られて、SQL Serverで変換掛けられていた場合、変換効率的に変換してから送ったほうが良い可能性があるかと思ったけど、送る前に変換をかけている。

バルクインサートはTDS(Tabular Data Stream)というプロトコルでデータ送信されていて、これに合わせた型に変換する必要がある。

自前で型変換すると、SqlBulkCopy内では変換されないため、SqlBulkCopyより高速に変換できるなら自前で変換する価値はある。しかし、汎用的に作った場合、漏れなく正しく変換するのは困難だし、列暗号化されていた場合の対応なども考慮する必要がある。

ボトルネックになっていることが判明したら、考慮する形でよいと判断した。

DB列とファイル列のマッピング

SqlBulkCopyでは、デフォルトではDBの列番号とファイルの列番号(正確にはファイルを読み込むIDataReaderからみた列番号)は同じ値となる。

もちろん変更は可能で、名称と名称または列番号と列番号の指定以外にも、名称と番号・番号と名称の、都合4つのパターンが指定できる。

using SqlBulkCopy sqlBulkCopy = new(connection);

sqlBulkCopy.ColumnMappings.Add("source", "destination");
sqlBulkCopy.ColumnMappings.Add(1, 2);
sqlBulkCopy.ColumnMappings.Add("source", 2);
sqlBulkCopy.ColumnMappings.Add(1, "destination");

この時、ソース(つまりCSVや固定長のファイル)側を名称で指定した場合、IDataReader(の親のIDataRecord)のGetOrdinalが呼び出されるので、CSVや固定長ファイルのIDataReaderを作成するときに、何らかのマッピングが必要となる。

値の取得

IDataReader#GetValue(int column)が呼ばれるので、これを実装する必要がある。

先に記述した通り文字列を返せばよい。

またマッピングが名称で定義されていた場合は、前述のGetOrdinalで名称から番号に変換した値で取得される。

Discussion