web-dev-qa-db-ja.com

最短で1,000万件のレコードを挿入するにはどうすればよいですか?

以下のようなファイル(1,000万レコード)があります。

    line1
    line2
    line3
    line4
   .......
    ......
    10 million lines

したがって、基本的には1000万レコードをデータベースに挿入したいと思います。ファイルを読み取ってSQL Serverにアップロードします。

C#コード

System.IO.StreamReader file = 
    new System.IO.StreamReader(@"c:\test.txt");
while((line = file.ReadLine()) != null)
{
    // insertion code goes here
    //DAL.ExecuteSql("insert into table1 values("+line+")");
}

file.Close();

ただし、挿入には時間がかかります。 C#を使用して、最短の時間で1000万レコードを挿入するにはどうすればよいですか?

更新1:
一括挿入:

BULK INSERT DBNAME.dbo.DATAs
FROM 'F:\dt10000000\dt10000000.txt'
WITH
(

     ROWTERMINATOR =' \n'
  );

私のテーブルは以下のようなものです:

DATAs
(
     DatasField VARCHAR(MAX)
)

しかし、私は次のエラーを得ています:

メッセージ4866、レベル16、状態1、行1
一括読み込みに失敗しました。データファイルの行1、列1の列が長すぎます。フィールドターミネータと行ターミネータが正しく指定されていることを確認してください。

メッセージ7399、レベル16、状態1、行1
OLEリンクサーバー "(null)"のDBプロバイダー "BULK"がエラーを報告しました。プロバイダーはエラーに関する情報を提供しませんでした。

メッセージ7330、レベル16、状態2、行1
OLEリンクサーバー "(null)"のDBプロバイダー "BULK"から行をフェッチできません。

以下のコードは機能しました:

BULK INSERT DBNAME.dbo.DATAs
FROM 'F:\dt10000000\dt10000000.txt'
WITH
(
    FIELDTERMINATOR = '\t',
    ROWTERMINATOR = '\n'
);
27

BulkCopyを介してロードするDataTableを作成しないようにしてください。これは、より小さなデータセットに対しては問題のないソリューションですが、データベースを呼び出す前に1000万行すべてをメモリにロードする理由はまったくありません。

最善の策(BCP/_BULK INSERT_/OPENROWSET(BULK...)以外)は、テーブル値パラメーター(TVP)を介してファイルからデータベースにコンテンツをストリーミングすることです。 TVPを使用すると、ファイルを開いて行を読み取り、完了するまで行を送信して、ファイルを閉じることができます。このメソッドには、1行のみのメモリフットプリントがあります。 Streaming Data Into SQL Server 2008 From a Application という記事を書きましたが、これはまさにこのシナリオの例です。

構造の簡単な概要は次のとおりです。上記の質問と同じインポートテーブルとフィールド名を想定しています。

必要なデータベースオブジェクト:

_-- First: You need a User-Defined Table Type
CREATE TYPE ImportStructure AS TABLE (Field VARCHAR(MAX));
GO

-- Second: Use the UDTT as an input param to an import proc.
--         Hence "Tabled-Valued Parameter" (TVP)
CREATE PROCEDURE dbo.ImportData (
   @ImportTable    dbo.ImportStructure READONLY
)
AS
SET NOCOUNT ON;

-- maybe clear out the table first?
TRUNCATE TABLE dbo.DATAs;

INSERT INTO dbo.DATAs (DatasField)
    SELECT  Field
    FROM    @ImportTable;

GO
_

上記のSQLオブジェクトを利用するC#アプリのコードは次のとおりです。オブジェクト(DataTableなど)を入力してからストアドプロシージャを実行するのではなく、このメソッドでは、ファイルの内容の読み取りを開始するのがストアドプロシージャの実行であることに注意してください。 Stored Procの入力パラメーターは変数ではありません。これはメソッドGetFileContentsの戻り値です。このメソッドは、SqlCommandExecuteNonQueryを呼び出すときに呼び出され、ファイルを開き、行を読み取り、_IEnumerable<SqlDataRecord>_および_yield return_構成を介してSQL Serverに送信します。そして、ファイルを閉じます。ストアドプロシージャはテーブル変数@ImportTableを参照するだけであり、データが到着し始めるとすぐにアクセスできます(注:データは完全ではなくても、短時間持続しますtempdbの内容)。

_using System.Collections;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using Microsoft.SqlServer.Server;

private static IEnumerable<SqlDataRecord> GetFileContents()
{
   SqlMetaData[] _TvpSchema = new SqlMetaData[] {
      new SqlMetaData("Field", SqlDbType.VarChar, SqlMetaData.Max)
   };
   SqlDataRecord _DataRecord = new SqlDataRecord(_TvpSchema);
   StreamReader _FileReader = null;

   try
   {
      _FileReader = new StreamReader("{filePath}");

      // read a row, send a row
      while (!_FileReader.EndOfStream)
      {
         // You shouldn't need to call "_DataRecord = new SqlDataRecord" as
         // SQL Server already received the row when "yield return" was called.
         // Unlike BCP and BULK INSERT, you have the option here to create a string
         // call ReadLine() into the string, do manipulation(s) / validation(s) on
         // the string, then pass that string into SetString() or discard if invalid.
         _DataRecord.SetString(0, _FileReader.ReadLine());
         yield return _DataRecord;
      }
   }
   finally
   {
      _FileReader.Close();
   }
}
_

上記のGetFileContentsメソッドは、以下に示すように、ストアドプロシージャの入力パラメーター値として使用されます。

_public static void test()
{
   SqlConnection _Connection = new SqlConnection("{connection string}");
   SqlCommand _Command = new SqlCommand("ImportData", _Connection);
   _Command.CommandType = CommandType.StoredProcedure;

   SqlParameter _TVParam = new SqlParameter();
   _TVParam.ParameterName = "@ImportTable";
   _TVParam.TypeName = "dbo.ImportStructure";
   _TVParam.SqlDbType = SqlDbType.Structured;
   _TVParam.Value = GetFileContents(); // return value of the method is streamed data
   _Command.Parameters.Add(_TVParam);

   try
   {
      _Connection.Open();

      _Command.ExecuteNonQuery();
   }
   finally
   {
      _Connection.Close();
   }

   return;
}
_

その他の注意事項:

  1. いくつかの変更を加えると、上記のC#コードをデータをバッチ処理するように適合させることができます。
  2. マイナーな変更により、上記のC#コードを複数のフィールドで送信するように適合させることができます(上記のリンクされた「スチームデータ...」の記事に示されている例は、2つのフィールドに渡されます)。
  3. プロシージャのSELECTステートメントで各レコードの値を操作することもできます。
  4. プロシージャでWHERE条件を使用して行をフィルタリングすることもできます。
  5. TVPテーブル変数には複数回アクセスできます。 READONLYですが、「転送のみ」ではありません。
  6. SqlBulkCopyを超える利点:
    1. SqlBulkCopyはINSERT専用ですが、TVPを使用するとデータを任意の方法で使用できます。MERGE;を呼び出すことができます。いくつかの条件に基づいてDELETEを実行できます。データを複数のテーブルに分割できます。等々。
    2. TVPはINSERT専用ではないため、データをダンプするための個別のステージングテーブルは必要ありません。
    3. ExecuteReaderの代わりにExecuteNonQueryを呼び出すことで、データベースからデータを取得できます。たとえば、IDENTITYインポートテーブルにDATAsフィールドがある場合、OUTPUT句をINSERTに追加して、_INSERTED.[ID]_(IDIDENTITYフィールドの名前であると想定)。または、完全に異なるクエリの結果、または両方を返すことができます。複数の結果セットを送信してReader.NextResult()経由でアクセスできるためです。 SqlBulkCopyを使用している場合、データベースから情報を取得することはできませんが、S.O。に関する質問がいくつかあります。 (少なくとも新しく作成されたIDENTITY値に関して)それを正確に実行したい人々の。
    4. ディスクからSQL Serverへのデータの取り込みが少し遅い場合でも、プロセス全体で時々高速になる理由の詳細については、SQL Serverカスタマーアドバイザリーチームの次のホワイトペーパーを参照してください: Maximizing Throughput with TVP
41
Solomon Rutzky

C#では、SqlBulkCopyにファイルを読み取らせることをお勧めします。これを行うには、IDataReader directをSqlBulkCopy.WriteToServerメソッドに渡す必要があります。次に例を示します。 http://www.codeproject.com/Articles/228332/IDataReader-implementation-plus-SqlBulkCopy

6
playful

最良の方法は、1番目のソリューションと2番目のソリューションを組み合わせる方法です。DataTableを作成し、ループに行を追加してから、BulkCopyを使用して1つの接続でDBにアップロードします これを使用するには一括コピーのヘルプ

一括コピーは非常にデリケートな操作であるため、dataTableで列名を「テキスト」として宣言し、DBで「テキスト」として列名を宣言すると例外がスローされるなど、注意が必要です。 、 幸運を。

3
Liran