web-dev-qa-db-ja.com

SQL Server 2016にcsvファイルをロードする最も効率的な方法は何ですか?

さて、この質問は前もって単純なようです。ファイルをロードするだけではありません。

ストーリー全体は次のようになります。何らかの理由で、クライアントはリレーショナルデータベースとしてのみ記述できるものを送信し、フラット化し、単一のcsvファイルに圧縮します(ただし、区切り文字はカンマではなくチルドです)。実際、それはちょっとひどいです。同じデータがファイル全体で無限に繰り返されます。そのため、このデータに何らかの順序を戻すために、実際のリレーショナルデータベースにロードします。データ量が多いため、データベースにロードすると、データの問題を簡単に検査できます。また、エクスポートもはるかに簡単になります。

レコードごとに53行があり、送信ごとに250,000レコードのボールパークのどこかにあります。それを6つの正規化テーブルに分割したいと思います。 C#プログラムのデータを検証するか、使用しているSQL Server 2016 LocalDbインスタンスを検証するかわかりません。

私は経験豊富なDBAではありません。私はSQLに少し手を出してきたC#プログラマーです。構文には十分満足していますが、これが正しく行われていることを確認したいと思います。

また、すべてを完全に自動化する必要があります。ファイルが入ってくると、C#プログラムはファイルを受け取ると起動し、データベースにロードします。

レイアウトについて詳しく説明します。ファイルは53フィールドで、各行にはステートメントの詳細行が含まれています(請求対象、アイテムの請求頻度、アイテムの合計コストまたはクレジットなど)。問題は、すべての行に郵送全体、支払人、居住者、財産、および送金に関する情報があることです。ありそれが知られているので、今これをどのように行っているのかを説明しましょう:

  1. ファイルを開く
  2. ファイルの各行について、郵送先、支払人、居住者、財産、および送金先を説明するテーブルのキーを取得します。
  3. そのデータをキャッシュされたデータと比較します。キャッシュされたデータが無効な場合は、DBをクエリして、そのエンティティが既に追加されているかどうかを確認します。そうでない場合は、作成してください。キャッシュします。
  4. 新しい詳細行を追加して、詳細と1対多の関係があるメーリングに関連付けます。 (郵送自体は、支払人、資産、および送金と多対1の関係があります。)
  5. 終了したらファイルを閉じます。

これは世界で最も遅いものではありませんが、すべてRAMで完全に実行されます。プログラムは現在の状態でRAMが不足する危険な状態に近づくため、データをすべてローカルRAMに保持するのではなく、データベースにロードすることにしました。うまくいけば、さらに潜在的な回答者のためにこれに光を当ててください。

4
sonicbhoc

複数のフィールドの複数の行(つまり、単純な区切られたリストではない)を渡すための私の好ましいアプローチは、テーブル値パラメーター(TVP)を使用することです。アイデアは、.NETコードでファイルを1行ずつ読み取りますが、データを一度にすべてストリーミングするか、それが1つのトランザクションに対して多すぎる場合は、TVPを使用してバッチに分割してSQL Serverにストリーミングするというものです。 TVPは本質的にはストアドプロシージャへの入力パラメーターであるテーブル変数であるため、これは行単位ではなく、セットベースの操作(SQL Serverに関する限り)になります。 TVPはパラメーターとしてアドホッククエリに送信できるため、厳密にはストアドプロシージャは必要ありませんが、いずれにしても、ストアドプロシージャを使用する方が優れています。

ファイルからの読み取り中にTVPを使用するには、主に2つのパターンがあります(各パターンは、DataTableではなくIEnumerable<SqlDataRecord>を返すメソッドを渡すことに依存しています)。

  1. ファイルのオープンと読み取りを開始する「イ​​ンポート」ストアドプロシージャを実行します。すべての行が読み込まれ(この時点で検証できます)、ストアドプロシージャにストリーミングされます。このアプローチでは、ストアドプロシージャは1回だけ実行され、すべての行が1つのセットとして送信されます。これはより簡単な方法ですが、より大きなデータセット(つまり、数百万行)の場合、単にステージングテーブルにロードするのではなく、ライブテーブルにデータを直接マージする操作では、パフォーマンスが最適にならない可能性があります。このアプローチに必要なメモリは、1レコードのサイズです。

  2. int _BatchSizeの変数を作成し、ファイルを開いて、次のいずれかを行います。

    1. レコードのバッチを保持するコレクションを作成します
      1. _BatchSizeをループするか、ファイルから読み取る行がなくなるまでループします
        1. 行を読みます
        2. 検証
        3. コレクションに有効なエントリを保存する
      2. 各ループの最後でストアドプロシージャを実行し、コレクションにストリーミングします。
      3. このアプローチに必要なメモリは、1レコードのサイズ* _BatchSizeです。
      4. 利点は、DB内のトランザクションがディスクI/Oレイテンシやビジネスロジックレイテンシに依存しないことです。
    2. ファイルから読み取る行がなくなるまで、ループしてストアドプロシージャを実行します
      1. ストアドプロシージャを実行する
        1. _BatchSizeをループするか、ファイルから読み取る行がなくなるまでループします
          1. 行を読みます
          2. 検証
          3. レコードをSQL Serverにストリーミングする
      2. このアプローチに必要なメモリは、1レコードのサイズです。
      3. 欠点は、DB内のトランザクションがディスクI/Oレイテンシやビジネスロジックレイテンシに依存するため、オープン時間が長くなり、ブロッキングの可能性が高くなることです。

StackOverflowでのパターン#1の完全な例を次の回答に示しています: 1000万レコードを可能な限り最短時間で挿入するにはどうすればよいですか?

パターン#2.1(非常にスケーラブルなアプローチ)の場合、以下に部分的な例を示します。

必要なデータベースオブジェクト(不自然な構造を使用):

まず、ユーザー定義のテーブルタイプ(UDTT)が必要です。

UNIQUEDEFAULTCHECKの制約を使用して、レコードがSQL Serverにヒットする前にデータの整合性を適用することに注意してください。一意の制約は、テーブル変数にインデックスを作成する方法でもあります:)。

CREATE TYPE [ImportStructure] AS TABLE
(
    BatchRecordID INT IDENTITY(1, 1) NOT NULL,
    Name NVARCHAR(200) NOT NULL,
    SKU VARCHAR(50) NOT NULL UNIQUE,
    LaunchDate DATETIME NULL,
    Quantity INT NOT NULL DEFAULT (0),
    CHECK ([Quantity] >= 0)
);
GO

次に、UDTTをインポートストアドプロシージャへの入力パラメーターとして使用します(したがって、「テーブル値パラメーター」)。

CREATE PROCEDURE dbo.ImportData (
   @CustomerID     INT,
   @ImportTable    dbo.ImportStructure READONLY
)
AS
SET NOCOUNT ON;

UPDATE prod
SET    prod.[Name] = imp.[Name],
       prod.[LaunchDate] = imp.[LaunchDate],
       prod.[Quantity] = imp.[Quantity]
FROM   [Inventory].[Products] prod
INNER JOIN @ImportTable imp
        ON imp.[SKU] = prod.[SKU]
WHERE  prod.CustomerID = @CustomerID;

INSERT INTO [Inventory].[Products] ([CustomerID], [SKU], [Name], [LaunchDate], [Quantity])
    SELECT  @CustomerID, [SKU], [Name], [LaunchDate], [Quantity]
    FROM    @ImportTable imp
    WHERE   NOT EXISTS (SELECT prod.[SKU]
                        FROM   [Inventory].[Products] prod
                        WHERE  prod.[SKU] = imp.[SKU]
                       );
GO

アプリコード:

最初に、レコードのバッチを格納するために使用されるクラスを定義します。

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

private class ImportBatch
{
  string Name;
  string SKU;
  DateTime LaunchDate;
  int Quantity;
}

次に、コレクションからSQL Serverにデータをストリーミングするために使用するメソッドを定義します。ご注意ください:

  • SqlMetaDataフィールドであっても、BatchRecordIDIDENTITYエントリが定義されています。ただし、値がサーバー生成であることを示すように定義されています。
  • yield returnはレコードを返しますが、コントロールは次の行に戻ります(ループの先頭に戻ります)。
private static IEnumerable<SqlDataRecord> SendImportBatch(List<ImportBatch> RecordsToSend)
{
   SqlMetaData[] _TvpSchema = new SqlMetaData[] {
      new SqlMetaData("BatchRecordID", SqlDbType.Int, true, false,
                      SortOrder.Unspecified, -1),
      new SqlMetaData("Name", SqlDbType.NVarChar, 200),
      new SqlMetaData("SKU", SqlDbType.VarChar, 50),
      new SqlMetaData("LaunchDate", SqlDbType.DateTime),
      new SqlMetaData("Quantity", SqlDbType.Int)
   };

   SqlDataRecord _DataRecord = new SqlDataRecord(_TvpSchema);

   // Stream the collection into SQL Server without first
   // copying it into a DataTable.
   foreach (ImportBatch _RecordToSend in RecordsToSend)
   {
      // we don't set field 0 as that is the IDENTITY field
      _DataRecord.SetString(1, _RecordToSend.Name);
      _DataRecord.SetString(2, _RecordToSend.SKU);
      _DataRecord.SetDateTime(3, _RecordToSend.LaunchDate);
      _DataRecord.SetInt32(4, _RecordToSend.Quantity);

      yield return _DataRecord;
   }
}

最後に、インポート処理全体を定義します。 SQL Serverとファイルへの接続を開いてから、ファイルを循環させ、各サイクルの_BatchSizeレコード数を検証します。ストアドプロシージャのパラメーターは、変更されないため一度だけ定義されます。CustomerID値は変更されず、TVPパラメーターの値は、ストアドプロシージャが次を介して実行されたときにのみ呼び出されるメソッド-SendImportBatch-への参照にすぎません。 ExecuteNonQuery。 TVP値として渡されるメソッドへの入力パラメーターは参照型であるため、常にその変数/オブジェクトの現在の値を反映する必要があります。

public static void ProcessImport(int CustomerID)
{
   int _BatchSize = GetBatchSize();
   string _ImportFilePath = GetImportFileForCustomer(CustomerID);

   List<ImportBatch> _CurrentBatch = new List<ImportBatch>();
   ImportBatch _CurrentRecord;

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

   // Parameters do not require leading "@" when using CommandType.StoredProcedure
   SqlParameter _ParamCustomerID = new SqlParameter("CustomerID", SqlDbType.Int);
   _ParamCustomerID.Value = CustomerID;
   _Command.Parameters.Add(_ParamCustomerID);

   SqlParameter _ParamImportTbl = new SqlParameter("ImportTable", SqlDbType.Structured);
   // TypeName is not needed when using CommandType.StoredProcedure
   //_ParamImportTbl.TypeName = "dbo.ImportStructure";
   // Parameter value is method that returns streamed data (IEnumerable)
   _ParamImportTbl.Value = SendImportBatch(_CurrentBatch);
   _Command.Parameters.Add(_ParamImportTbl);

   StreamReader _FileReader = null;

   try
   {
      int _RecordCount;
      string[] _InputLine = new string[4];

      _Connection.Open();

      _FileReader = new StreamReader(_ImportFilePath);

       // process the file
       while (!_FileReader.EndOfStream)
       {
          _RecordCount = 1;

          // process a batch
          while (_RecordCount <= _BatchSize
                  && !_FileReader.EndOfStream)
          {
             _CurrentRecord = new ImportBatch();

             _InputLine = _FileReader.ReadLine().Split(new char[]{','});

             _CurrentRecord.Name = _InputLine[0];
             _CurrentRecord.SKU = _InputLine[1];
             _CurrentRecord.LaunchDate = DateTime.Parse(_InputLine[2]);
             _CurrentRecord.Quantity = Int32.Parse(_InputLine[3]);

             // Do validations, transformations, etc
             if (record is not valid)
             {
                _CurrentRecord = null;
                continue; // skip to next line in the file
             }

             _CurrentBatch.Add(_CurrentRecord);
             _RecordCount++; // only increment for valid records
          }

          _Command.ExecuteNonQuery(); // send batch to SQL Server

          _CurrentBatch.Clear();
       }
   }
   finally
   {
      _FileReader.Close();

      _Connection.Close();
   }

   return;
}

TVPのREADONLYの性質により、宛先テーブルにマージされる前に必要な検証や変換が禁止されている場合、TVPのデータは、ストアドプロシージャの開始時にローカル一時テーブルに簡単に転送できます。

5
Solomon Rutzky

SQL Server Integration Services(SSIS)を使用してステージングテーブルにデータを読み込み、そこでデータを確認/マッサージします。 SSISパッケージでフラットファイル接続を作成し、独自のカスタム区切り文字(〜)を設定します。私が見てきたことから、SSISは、特にフラットファイルソースからのデータのインポートに効率的です。

SSISパッケージは、Visual StudioのIntegration Servicesプロジェクトで設計および開発されます。それらは本質的に、コードとSQ​​Lの外にある独自のオブジェクトです(ただし、SQLまたはVBまたはC#を利用できます)。

それらをサーバーまたはSQL内に保存でき、簡単に実行できます(SQLまたはSQLジョブなど)。 SSISは、いくつかの異なるデータソースからデータを取得してSQLにインポートし、使用するために操作するように設計されています。これはあなたのシナリオを処理するために最適化されているとさえ言えます。

1
Jason B.

あなたはおそらく このPowerShellスクリプトでクリッシーが大きなCSVファイルをSQL Serverにインポートするために行った を使用して、それをC#に変換するか、C#コードでこのスクリプトを呼び出すだけです。

データをステージングテーブルにダンプし、必要に応じて分割します。複数のテーブルにインポートする必要があるとは述べていません。これをすべてC#から行う必要がある場合は、最初のデータをSQL Serverテーブルに最初に取得すると、管理が容易になります。

1
user507