web-dev-qa-db-ja.com

T-SQL:カスタム解析された数値データ、ルックアップ値を含むCSV-> tableパイプライン

問題

CSVファイルから数千のレコードをテーブル_[Child]_に挿入したい(そして、これを数十のファイルから同じテーブルに挿入する)。ただし、A。ルックアップ値をテーブル(_[Parent]_の列値)とB。挿入が行われる前に、CSVに含まれるデータに対して他の計算を実行します。これはどのようにして最良に達成されますか?

ルックアップは基本的に、_Child.P_ID_からIDENTITYベースの主キー値(_[Parent]_として)を返すことです。ここで、_[Parent].NVarCharValue,[Parent].DateTimeValue_はCSVデータの最初の2つのフィールドと一致します、しかし、_NVarCharValueDateTimeValue自体を_[Child]_に格納する手間を省きたいです。他の子レベルのテーブルとの将来の結合は_P_ID_フィールドとテキスト情報は冗長であり、スペースを消費し、挿入時に追加のラグタイムを引き起こす可能性があります(ただし、ルックアップほどではないかもしれませんが、悲しいかな…)。

たとえば、計算によって得られたデータ(B。)の場合、エンコードされたGPS座標を[d] ddmm.mmmm、C(Degrees DecimalMinutes、Direction)形式に変換して、1つの10進度フィールド(またはgeography :: Pointの半分)に変換します。たとえば、私のCSVには_3748.9729,S_などの素敵なエントリが含まれています。これは37°48.9729 ’Sとして解析され、それをLatitudeに保存される-37.816215に変換されます。

オプション?私は5つの可能性を考え出しましたが、アプローチのすべての落とし穴を考慮しない限り、それらのいずれにもコーディングしたくありません。


オプション1:基本_Insert Into_ビュー+ _BULK INSERT_

子テーブルにのみ挿入するつもりですが、ルックアップを実行するために必要なマルチテーブルジョインは、行を複数の基本テーブルに挿入するときに MS制約 に違反している可能性があります。そのビューに挿入する場合、VIEWのDDLで指定された計算または集計がない場合、確かに constraint に違反します。

そうでなければ、これは(本質的に)私がやりたいことです

_CREATE VIEW [dbo].[ivChild] as 
SELECT 
  P.[ NVarCharValue],
  P.[ DateTimeValue],
  CAST(CAST((ABS(C.Latitude) as Integer) as VarChar(3))+STR(ABS(C.Latitude-CAST(C.Latitude as Integer))*60.0,9,6) as LatitudeMagnitude --not quite correct (single digit minutes), but close enough for the example
CASE
  WHEN SIGN(C.Latitude)>0 THEN ‘N’
  ELSE ‘S’
END as LatitudeDir,
CAST(CAST((ABS(C.Longitude) as Integer) as VarChar(3))+STR(ABS(C.Longitude-CAST(C.Longitude as Integer))*60.0,9,6) as LongitudeMagnitude
CASE
  WHEN SIGN(C.Latitude)>0 THEN ‘E’
  ELSE ‘W’
END as LongitudeDir
FROM Child as C join Parent as P on P.ID=C.P_id
_

しかし、おそらく最初の2つのサブクエリを使用して、Childに対してのみ挿入を有効にすることができますか?例えば.

_SELECT
  (SELECT P.NVarCharValue from Parent as P on P.ID=C.P_id) as NVarCharValue
  (SELECT P.DateTimeValue from Parent as P on P.ID=C.P_id) as DateTimeValue
    …
_

オプション2:_BULK INSERT_をステージングテーブルに、ストアドプロシージャmergeを実際のデータテーブルに?

CSVのNVarCharValue + DateTimeValue列をステージングテーブルに挿入し、次に_MERGE INTO_または_INSERT INTO_クエリを実行して、値をデータテーブルに移行します。可能な場合はインラインで計算を行い、検索を実行して文字列の入力値を解析し、最終的な格納値(たとえば、文字列からの10進度、次にdecimal(10,6)として格納)に変換します。

オプション3:GPSMagnitudeのカスタムCLRを作成します

これは、C#で着信文字列を解析し、内部的に10進数としてクラスに格納し、10進数データ型関数に自動変換します。何かのようなもの

_public static implicit operator Decimal(GPSMagnitude c) # GPS->decimal
    { return c._decimalMagnitude; }

Public static implicit operator GPSMagnitude(Decimal c) # decimal->GPS
    { return new GPSMagnitude(c); }
Public static implicit operator ToString(GPSMagnitude c)# GPS->string
    {
      d = (int)c;
      m = (c-(int)c)*60.0;
      return d.ToString()+m.ToString();
    }
[SqlMethod(OnNullCall = false)]
public GPSMagnitude Parse(SqlString s)           # string->GPS
    {
      Decimal f;
      int Deg;
      float Mn;
      Int32.TryParse(s, out f);
      Deg = ((int)f)/100
      Mn = f – Deg*100.0
      return new GPSMagnitude(Deg + Mn/60)
    }
_

次に、CAST(LatitudeMagnitude as GPSMagnitude)が指定されたビューを使用し、LatitudeMagnitudedecimal(10,6)として指定された基になるテーブルを作成します。 2つの列をマージしてマグニチュードを署名させる(S/Wは負)ために、まだいくつかの面白いビジネスを行う必要があります。また、P_IDルックアップの問題を個別に処理する必要があります。

オプション4:_BULK INSERT_と_FIRE_TRIGGERS_

文字列入力を受け取り、それを10進数としてテーブルに送信するカスタムトリガーで解析を行います。 BULK INSERTを聞くとかなり遅くなりますが、検索とGPSテキストの解析の両方ができるはずです。 TRIGGERコードがどうなるかについては、あまり考えていません。

オプション5:CSVを事前に解析する

たとえばPython GPS計算を実行し、マグニチュードと符号列を組み合わせます。ただし、P_IDルックアップの問題には対処する必要がありますが、

オプション6:醜い入力データセットに対するソリューション。

私が持っているものに混乱している場合は、これらのオプションをさらに具体化できます。P


ソロモンの提案に基づいて、私はC#ストリームパーサーを実装し始めています。

テーブルから既存の構造を取得するには、次のコードが機能する必要があります(いくつかの調整が必要です)。これは_else ifs_ではなく_switch case_を使用していることに注意してください。どちらがより効率的かわかりません。また、すべての不測の事態をカバーするためにすべての列を取得したわけではありませんが、最も基本的なタイプでは問題なく機能するはずです。繰り返しますが、これはSQLサーバーで既に設定したテーブルフィールドを概算するためのものです。

_    public SqlMetaData[] ServerLookupFields()
    {
        using (SqlConnection conn = new SqlConnection(ParserDict.ConnectionString))
        {
            SqlCommand command;
            int siz;
            Console.WriteLine("Activating SQL connection");
            conn.Open();
            Console.WriteLine("SQL connection open");
            string num = "select count(*) as cnt from sys.columns as sc join sys.tables as st on sc.object_id = st.object_id where st.name=@table_name;";
            command = new SqlCommand(num, conn);
            command.Parameters.AddWithValue("@table_name", this.tableDestination);
            string result = command.ExecuteScalar().ToString();
            Int32.TryParse(result, out siz);
            command.Dispose();
            SqlMetaData[] smd = new SqlMetaData[siz];
            string cmd = "exec sp_columns @table_name=@table_name, @table_owner=dbo";
            command = new SqlCommand(cmd, conn);
            command.Parameters.AddWithValue("@table_name", this.tableDestination);
            SqlDataReader rd = command.ExecuteReader();
            int i = 0;
            while (rd.Read())
            {
                smd[i] = ReadSingleMetaRow((IDataRecord)rd);
                Console.WriteLine(smd[i].Name.ToString()+","+smd[i].DbType.ToString());
                i++;
            }
            rd.Close();
            command.Dispose();
            return smd;
        }
    }
    private SqlMetaData ReadSingleMetaRow(IDataRecord meta)
    {
        bool nullable;
        byte precision;
        byte length;
        byte scale;
        SqlMetaData smd;
        string columnName = meta[3].ToString(); //Column_name
        string dataType = meta[5].ToString(); //Type_name
        string dt_fw_lower = dataType.ToLower().Split(new Char[] { ' ' }, 2)[0]; //dataType_firstWord_ToLower
        byte.TryParse(meta[6].ToString(), out precision); //Precision
        byte.TryParse(meta[7].ToString(), out length); //Length
        byte.TryParse(meta[8].ToString(), out scale); //Scale
        Boolean.TryParse(meta[10].ToString(), out nullable); //Nullable
        string datetimesub = meta[14].ToString(); //DateTimeSub(type)
        if (dt_fw_lower == "datetime2") { smd = new SqlMetaData(columnName, SqlDbType.DateTime2); }
        else if (dt_fw_lower == "nvarchar") { smd = new SqlMetaData(columnName, SqlDbType.NVarChar, length); }
        else if (dt_fw_lower == "varchar") { smd = new SqlMetaData(columnName, SqlDbType.VarChar, length); }
        else if (dt_fw_lower == "bigint") { smd = new SqlMetaData(columnName, SqlDbType.BigInt); }
        else if (dt_fw_lower == "float") { smd = new SqlMetaData(columnName, SqlDbType.Float, precision, length); }
        else if (dt_fw_lower == "decimal") { smd = new SqlMetaData(columnName, SqlDbType.Decimal, precision, scale); }
        else if (dt_fw_lower == "binary") { smd = new SqlMetaData(columnName, SqlDbType.Binary, length); }
        else if (dt_fw_lower == "varbinary") { smd = new SqlMetaData(columnName, SqlDbType.VarBinary, length); }
        else if (dt_fw_lower == "bit") { smd = new SqlMetaData(columnName, SqlDbType.Bit); }
        else if (dt_fw_lower == "char") { smd = new SqlMetaData(columnName, SqlDbType.Char, length); }
        else if (dt_fw_lower == "nchar") { smd = new SqlMetaData(columnName, SqlDbType.NChar, length); }
        else if (dt_fw_lower == "datetimeoffset") { smd = new SqlMetaData(columnName, SqlDbType.DateTimeOffset); }
        else if (dt_fw_lower == "datetime") { smd = new SqlMetaData(columnName, SqlDbType.DateTime); }
        else if (dt_fw_lower == "date") { smd = new SqlMetaData(columnName, SqlDbType.Date, length); }
        else if (dt_fw_lower == "image") { smd = new SqlMetaData(columnName, SqlDbType.Image, length); }
        else if (dt_fw_lower == "int") { smd = new SqlMetaData(columnName, SqlDbType.Int); }
        else if (dt_fw_lower == "money") { smd = new SqlMetaData(columnName, SqlDbType.Money, length); }
        else if (dt_fw_lower == "ntext") { smd = new SqlMetaData(columnName, SqlDbType.NText, length); }
        else if (dt_fw_lower == "real") { smd = new SqlMetaData(columnName, SqlDbType.Real, precision, scale); }
        else if (dt_fw_lower == "smalldatetime") { smd = new SqlMetaData(columnName, SqlDbType.SmallDateTime); }
        else if (dt_fw_lower == "smallint") { smd = new SqlMetaData(columnName, SqlDbType.SmallInt); }
        else if (dt_fw_lower == "smallmoney") { smd = new SqlMetaData(columnName, SqlDbType.SmallMoney); }
        else if (dt_fw_lower == "structured") { smd = new SqlMetaData(columnName, SqlDbType.Structured, length); }
        else if (dt_fw_lower == "text") { smd = new SqlMetaData(columnName, SqlDbType.Text, -1); }
        else if (dt_fw_lower == "timestamp") { smd = new SqlMetaData(columnName, SqlDbType.Timestamp); }
        else if (dt_fw_lower == "time") { smd = new SqlMetaData(columnName, SqlDbType.Time); }
        else if (dt_fw_lower == "tinyint") { smd = new SqlMetaData(columnName, SqlDbType.TinyInt); }
        else if (dt_fw_lower == "udt") { smd = new SqlMetaData(columnName, SqlDbType.Udt, length); }
        else if (dt_fw_lower == "uniqueidentifier") { smd = new SqlMetaData(columnName, SqlDbType.UniqueIdentifier, length); }
        else if (dt_fw_lower == "variant") { smd = new SqlMetaData(columnName, SqlDbType.Variant, length); }
        else if (dt_fw_lower == "xml") { smd = new SqlMetaData(columnName, SqlDbType.Xml, length); }
        else { smd = new SqlMetaData(columnName, 0); }
        return smd;
    }
_
3
mpag

これをバラバラに行う必要はありません。このすべてを、セットベースのアプローチで、バッチで実行できます(そのため、入力ファイルのサイズは関係ありません)。ファイルから行を取得して、最初から最後まで処理する方法について考えてください。次に、各ステップが単一の行ではなく行のセットで動作していることを確認します。

これはSQLCLRを必要としませんが、XMLまたはテーブル値パラメーター(TVP)を使用して、行のバッチをSQL Serverに転送しますafterローカルで処理されました。

基本的なワークフローは次のとおりです(これについては別の回答で説明していますが、現時点では見つかりません)。ストアドプロシージャに各行を転送するには、ユーザー定義のテーブルタイプを作成する必要があります(これはTVPであり、SQL Server 2008以降で使用できます)。

  1. ファイルを開く
  2. 行がなくなるまでN行(バッチ)をループします
    1. 検証を実行する
    2. エンコードされたGPS座標を10進形式に変換する
    3. 行を(IEnumerable<SqlDataRecord>を実装するメソッドを使用して、nota DataTable!)をストアドプロシージャに送信します。 ____。]
      1. tVP列plus列を持つローカル一時テーブルを作成して、生成されたIDENTITY値を保持します。
      2. tVPから一時テーブルを作成する
      3. [Parent]に挿入します。ここで、[Parent].NVarCharValueおよび[Parent].DateTimeValueは存在しません。 IDENTITY値を[Parent].NVarCharValueおよび[Parent].DateTimeValueとともに別の一時テーブルにキャプチャして、メインの一時テーブルを更新し、新しいIDENTITY値を取得できるようにするか、またはそれをスキップして、最後のINSERTを実行するときに[Parent]テーブルにJOINするだけです。
      4. [Parent]の新しいIDENTITY値を2番目の一時テーブルにキャプチャした場合は、NVarCharValueDateTimeValueに一致するそれらの値でメインの一時テーブルを更新します。
      5. 必要に応じて、メインの一時テーブルのUPDATE[Child]テーブル、行を一意に識別する1つ以上の列(つまり、代替キー)に結合します。これは実際にはWHERE EXISTSです。
      6. メインの一時テーブルからのINSERT INTO [Child]、行を一意に識別する列でWHERE NOT EXISTSを使用
  3. ファイルを閉じる

私はこのアプローチを数回使用し、大きな成功を収めました。フォールトトレラントであるため、中断されたときに中断したところから簡単に再開できます。また、ローカル一時テーブルがその役割を果たして自動的にクリーンアップされるため、ステージングテーブル(クリーンアップしてインポートをマルチスレッド化する場合は乱雑にする必要がある)は必要ありませんandは本質的にスレッドセーフです:-)。これにより、アプリレイヤーでの処理に最も適した部分を処理し、残りをデータレイヤーで処理することもできます。また、メモリに関する限り、メモリ内にインポートファイルが一度に1バッチ分しか存在しないため、インポートファイルが10 kBでも10 GBでもかまいません。

また、必要に応じて、DB側のプロセスをできます。通常は、挿入、更新、またはスキップを判別するための状況列があります。次に、メインの一時テーブルにデータを入力した後、既存の宛先テーブルと比較します。行はあるが1つ以上の列が異なる場合、その行は更新としてマークされます。行はあるがすべての列が同じ場合、スキップとマークされます(変更されていない行を更新する必要はありません)。そうでない場合は、挿入としてマークされます。次に、最後にI UPDATE where Status = Update、次にI INSERT where Status = Insertとします。もちろん、私はそれぞれが独自の「ファンキーな」ルールを持っているいくつかのテーブルにインポートしていました;-)。

Stack Overflowの私の別の回答でいくつかのサンプルコードを見ることができます: 1000万レコードを可能な限り最短時間で挿入するにはどうすればよいですか? そのコードの構造はバッチ処理されません。ストアドプロシージャを1回実行し、ファイルを開き、行の読み取りと送信を行い、最後に閉じるプロセスを開始します。バッチ処理するには、次のように変更する必要があります。

  1. ファイルを開く
  2. while(!file.EOF)
    1. ストアドプロシージャの実行: "SendRows"メソッドにはファイルハンドルの入力パラメーターがあります
      1. 「SendRows」メソッド=
      2. while(rowCount <batchSize)//これはワークフローの「N行をループする」部分です
        1. file.ReadLine()
        2. if(file.EOF)break;
        3. 行を処理して送信する//検証を実行し、エンコードされたGPS座標を変換する
        4. rowCount++;
  3. ファイルを閉じる
1
Solomon Rutzky