2010-05-19 6 views
6

Więc zobaczyłem ten post tutaj i przeczytałem go i wygląda na to, że kopiowanie masowe może być drogą do zrobienia.Wstawianie zbiorcze najlepiej na ten temat? + Pomagając mi zrozumieć w pełni to, co znalazłem do tej pory

What’s the best way to bulk database inserts from c#?

Mam jeszcze kilka pytań, ale chcę wiedzieć, jak rzeczy faktycznie pracują.

Więc znalazłem 2 samouczki.

http://www.codeproject.com/KB/cs/MultipleInsertsIn1dbTrip.aspx#_Toc196622241

http://www.codeproject.com/KB/linq/BulkOperations_LinqToSQL.aspx

Pierwszy sposób wykorzystuje 2 ADO.NET 2.0 funkcje. BulkInsert i BulkCopy. drugi używa linq do sql i OpenXML.

Ten rodzaj odwołania do mnie, ponieważ używam linq do sql już i wolę go ponad ado.net. Jednak jak jedna osoba wskazała na stanowiskach co właśnie dzieje się wokół kwestii kosztem wydajności (w tym nic złego, że moim zdaniem)

Najpierw będę mówić o 2 sposoby w pierwszym tutorialu

Używam VS2010 Express (do testowania tutoriali użyłem VS2008 i nie wiesz jaka wersja .net właśnie załadowany tam przykładowe pliki i prowadził je), .NET 4.0, MVC 2.0, SQL Server 2005

  1. Czy ado.net 2.0 najnowsza wersja?
  2. W oparciu o technologię, której używam, czy są jakieś aktualizacje tego, co zamierzam pokazać, aby jakoś ją poprawić?
  3. Czy jest coś, czego te lekcje pominięto, o których powinienem wiedzieć?

BulkInsert

Używam tej tabeli dla wszystkich przykładów.

CREATE TABLE [dbo].[TBL_TEST_TEST] 
(
    ID INT IDENTITY(1,1) PRIMARY KEY, 
    [NAME] [varchar](50) 
) 

Kod SP

USE [Test] 
GO 
/****** Object: StoredProcedure [dbo].[sp_BatchInsert] Script Date: 05/19/2010 15:12:47 ******/ 
SET ANSI_NULLS ON 
GO 
SET QUOTED_IDENTIFIER ON 
GO 
ALTER PROCEDURE [dbo].[sp_BatchInsert] (@Name VARCHAR(50)) 
AS 
BEGIN 
      INSERT INTO TBL_TEST_TEST VALUES (@Name); 
END 

C# Code

/// <summary> 
/// Another ado.net 2.0 way that uses a stored procedure to do a bulk insert. 
/// Seems slower then "BatchBulkCopy" way and it crashes when you try to insert 500,000 records in one go. 
/// http://www.codeproject.com/KB/cs/MultipleInsertsIn1dbTrip.aspx#_Toc196622241 
/// </summary> 
private static void BatchInsert() 
{ 
    // Get the DataTable with Rows State as RowState.Added 
    DataTable dtInsertRows = GetDataTable(); 

    SqlConnection connection = new SqlConnection(connectionString); 
    SqlCommand command = new SqlCommand("sp_BatchInsert", connection); 
    command.CommandType = CommandType.StoredProcedure; 
    command.UpdatedRowSource = UpdateRowSource.None; 

    // Set the Parameter with appropriate Source Column Name 
    command.Parameters.Add("@Name", SqlDbType.VarChar, 50, dtInsertRows.Columns[0].ColumnName); 

    SqlDataAdapter adpt = new SqlDataAdapter(); 
    adpt.InsertCommand = command; 
    // Specify the number of records to be Inserted/Updated in one go. Default is 1. 
    adpt.UpdateBatchSize = 1000; 

    connection.Open(); 
    int recordsInserted = adpt.Update(dtInsertRows); 
    connection.Close(); 
} 

Więc pierwszą rzeczą jest wielkość partii. Dlaczego ustawiasz wielkość partii na inną niż liczba wysyłanych rekordów? Tak jak wysyłam 500 000 rekordów, więc zrobiłem partię o wielkości 500 000.

Dalej dlaczego to się zawiesza, kiedy to robię? Jeśli ustawię go na 1000 dla wielkości partii, działa dobrze.

System.Data.SqlClient.SqlException was unhandled 
    Message="A transport-level error has occurred when sending the request to the server. (provider: Shared Memory Provider, error: 0 - No process is on the other end of the pipe.)" 
    Source=".Net SqlClient Data Provider" 
    ErrorCode=-2146232060 
    Class=20 
    LineNumber=0 
    Number=233 
    Server="" 
    State=0 
    StackTrace: 
     at System.Data.Common.DbDataAdapter.UpdatedRowStatusErrors(RowUpdatedEventArgs rowUpdatedEvent, BatchCommandInfo[] batchCommands, Int32 commandCount) 
     at System.Data.Common.DbDataAdapter.UpdatedRowStatus(RowUpdatedEventArgs rowUpdatedEvent, BatchCommandInfo[] batchCommands, Int32 commandCount) 
     at System.Data.Common.DbDataAdapter.Update(DataRow[] dataRows, DataTableMapping tableMapping) 
     at System.Data.Common.DbDataAdapter.UpdateFromDataTable(DataTable dataTable, DataTableMapping tableMapping) 
     at System.Data.Common.DbDataAdapter.Update(DataTable dataTable) 
     at TestIQueryable.Program.BatchInsert() in C:\Users\a\Downloads\TestIQueryable\TestIQueryable\TestIQueryable\Program.cs:line 124 
     at TestIQueryable.Program.Main(String[] args) in C:\Users\a\Downloads\TestIQueryable\TestIQueryable\TestIQueryable\Program.cs:line 16 
    InnerException: 

czasu zajęło, aby wstawić 500000 rekordy z rozmiarem wkładki wsadowym 1000 miały „2 minut i 54 sekund”

Oczywiście nie jest to oficjalny czas Siedziałem tam z stopera (I Jestem pewien, że istnieją lepsze sposoby, ale był zbyt leniwy, aby spojrzeć na to, co mają gdzie)

Więc uważam, że trochę powolne w porównaniu do wszystkich innych (oczekuj linq do sql wstawić jeden) i nie jestem naprawdę pewien dlaczego.

Następny Spojrzałem na bulkcopy

/// <summary> 
/// An ado.net 2.0 way to mass insert records. This seems to be the fastest. 
/// http://www.codeproject.com/KB/cs/MultipleInsertsIn1dbTrip.aspx#_Toc196622241 
/// </summary> 
private static void BatchBulkCopy() 
{ 
    // Get the DataTable 
    DataTable dtInsertRows = GetDataTable(); 

    using (SqlBulkCopy sbc = new SqlBulkCopy(connectionString, SqlBulkCopyOptions.KeepIdentity)) 
    { 
     sbc.DestinationTableName = "TBL_TEST_TEST"; 

     // Number of records to be processed in one go 
     sbc.BatchSize = 500000; 

     // Map the Source Column from DataTabel to the Destination Columns in SQL Server 2005 Person Table 
     // sbc.ColumnMappings.Add("ID", "ID"); 
     sbc.ColumnMappings.Add("NAME", "NAME"); 

     // Number of records after which client has to be notified about its status 
     sbc.NotifyAfter = dtInsertRows.Rows.Count; 

     // Event that gets fired when NotifyAfter number of records are processed. 
     sbc.SqlRowsCopied += new SqlRowsCopiedEventHandler(sbc_SqlRowsCopied); 

     // Finally write to server 
     sbc.WriteToServer(dtInsertRows); 
     sbc.Close(); 
    } 

} 

Ten wydawało się bardzo szybko i nawet nie potrzebują SP (można użyć SP z luzem kopię? Jeśli można byłoby lepiej?)

BatchCopy nie miał problemu z wielkością 500 000 sztuk. Więc ponownie, dlaczego jest mniejsza niż liczba rekordów, które chcesz wysłać?

Znalazłem, że za pomocą BatchCopy i 500 000 partii wymagało to tylko 5 sekund. Następnie próbowałem z partią o wielkości 1000 i zajęło to tylko 8 sekund.

O wiele szybciej niż bulkinsert powyżej.

Teraz wypróbowałem drugi samouczek.

USE [Test] 
GO 
/****** Object: StoredProcedure [dbo].[spTEST_InsertXMLTEST_TEST] Script Date: 05/19/2010 15:39:03 ******/ 
SET ANSI_NULLS ON 
GO 
SET QUOTED_IDENTIFIER ON 
GO 
ALTER PROCEDURE [dbo].[spTEST_InsertXMLTEST_TEST](@UpdatedProdData nText) 
AS 
DECLARE @hDoc int 

exec sp_xml_preparedocument @hDoc OUTPUT,@UpdatedProdData 

INSERT INTO TBL_TEST_TEST(NAME) 
SELECT XMLProdTable.NAME 
    FROM OPENXML(@hDoc, 'ArrayOfTBL_TEST_TEST/TBL_TEST_TEST', 2) 
     WITH (
       ID Int,     
       NAME varchar(100) 
      ) XMLProdTable 

EXEC sp_xml_removedocument @hDoc 

Kod C#.

/// <summary> 
/// This is using linq to sql to make the table objects. 
/// It is then serailzed to to an xml document and sent to a stored proedure 
/// that then does a bulk insert(I think with OpenXML) 
/// http://www.codeproject.com/KB/linq/BulkOperations_LinqToSQL.aspx 
/// </summary> 
private static void LinqInsertXMLBatch() 
{ 
    using (TestDataContext db = new TestDataContext()) 
    { 
     TBL_TEST_TEST[] testRecords = new TBL_TEST_TEST[500000]; 
     for (int count = 0; count < 500000; count++) 
     { 
      TBL_TEST_TEST testRecord = new TBL_TEST_TEST(); 
      testRecord.NAME = "Name : " + count; 
      testRecords[count] = testRecord; 
     } 

     StringBuilder sBuilder = new StringBuilder(); 
     System.IO.StringWriter sWriter = new System.IO.StringWriter(sBuilder); 
     XmlSerializer serializer = new XmlSerializer(typeof(TBL_TEST_TEST[])); 
     serializer.Serialize(sWriter, testRecords); 
     db.insertTestData(sBuilder.ToString()); 
    } 
} 

Podoba mi się to, ponieważ używam obiektów, mimo że jest to zbyteczne. Nie rozumiem, jak działa SP. Jakbym nie zrozumiał tego. Nie wiem, czy OPENXML ma kilka wsadowych wstawek pod maską, ale nie wiem nawet, jak wziąć ten przykład SP i zmienić go tak, by pasował do moich tabel, ponieważ tak jak powiedziałem, nie wiem, co się dzieje.

Nie wiem też, co by się stało, gdyby obiekt zawierał więcej tabel. Tak jak mówię, że mam tabelę ProductName, co ma związek z tabelą produktów lub coś podobnego.

W linq do sql można uzyskać obiekt nazwy produktu i wprowadzić zmiany w tabeli produktów w tym samym obiekcie. Nie wiem, jak to uwzględnić. Nie jestem pewien, czy musiałbym zrobić osobne wkładki, czy co.

Czas było całkiem dobre dla 500.000 zapisów zajęło 52 sekund

ostatni sposób oczywiście był tylko przy użyciu LINQ zrobić to wszystko i to było całkiem źle.

/// <summary> 
/// This is using linq to sql to to insert lots of records. 
/// This way is slow as it uses no mass insert. 
/// Only tried to insert 50,000 records as I did not want to sit around till it did 500,000 records. 
/// http://www.codeproject.com/KB/linq/BulkOperations_LinqToSQL.aspx 
/// </summary> 
private static void LinqInsertAll() 
{ 
    using (TestDataContext db = new TestDataContext()) 
    { 
     db.CommandTimeout = 600; 
     for (int count = 0; count < 50000; count++) 
     { 
      TBL_TEST_TEST testRecord = new TBL_TEST_TEST(); 
      testRecord.NAME = "Name : " + count; 
      db.TBL_TEST_TESTs.InsertOnSubmit(testRecord); 
     } 
     db.SubmitChanges(); 
    } 
} 

Zrobiłem tylko 50 000 rekordów i zajęło mi to minutę.

Naprawdę zminimalizowałem to zrobić z linq do sql luzem wstawić drogę lub masową kopię. Po prostu nie jestem pewien, jak to zrobić, gdy masz związek w obie strony. Nie jestem pewien, jak obaj się podnoszą, robiąc aktualizacje zamiast wstawek, ponieważ nie mam okazji, aby spróbować jeszcze.

Nie sądzę, że kiedykolwiek będę musiał wstawić/zaktualizować ponad 50 000 rekordów w jednym typie, ale jednocześnie wiem, że będę musiał dokonać sprawdzania poprawności rekordów przed wstawieniem, aby spowolnić to i ten rodzaj sprawia, że ​​linq do sql ładniejszy jak twoje obiekty, zwłaszcza jeśli twoje pierwsze parsowanie danych z pliku xml przed wstawić do bazy danych.

Pełny kod C#

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Xml.Serialization; 
using System.Data; 
using System.Data.SqlClient; 

namespace TestIQueryable 
{ 
    class Program 
    { 
     private static string connectionString = ""; 
     static void Main(string[] args) 
     { 
      BatchInsert(); 
      Console.WriteLine("done"); 
     } 

     /// <summary> 
     /// This is using linq to sql to to insert lots of records. 
     /// This way is slow as it uses no mass insert. 
     /// Only tried to insert 50,000 records as I did not want to sit around till it did 500,000 records. 
     /// http://www.codeproject.com/KB/linq/BulkOperations_LinqToSQL.aspx 
     /// </summary> 
     private static void LinqInsertAll() 
     { 
      using (TestDataContext db = new TestDataContext()) 
      { 
       db.CommandTimeout = 600; 
       for (int count = 0; count < 50000; count++) 
       { 
        TBL_TEST_TEST testRecord = new TBL_TEST_TEST(); 
        testRecord.NAME = "Name : " + count; 
        db.TBL_TEST_TESTs.InsertOnSubmit(testRecord); 
       } 
       db.SubmitChanges(); 
      } 
     } 

     /// <summary> 
     /// This is using linq to sql to make the table objects. 
     /// It is then serailzed to to an xml document and sent to a stored proedure 
     /// that then does a bulk insert(I think with OpenXML) 
     /// http://www.codeproject.com/KB/linq/BulkOperations_LinqToSQL.aspx 
     /// </summary> 
     private static void LinqInsertXMLBatch() 
     { 
      using (TestDataContext db = new TestDataContext()) 
      { 
       TBL_TEST_TEST[] testRecords = new TBL_TEST_TEST[500000]; 
       for (int count = 0; count < 500000; count++) 
       { 
        TBL_TEST_TEST testRecord = new TBL_TEST_TEST(); 
        testRecord.NAME = "Name : " + count; 
        testRecords[count] = testRecord; 
       } 

       StringBuilder sBuilder = new StringBuilder(); 
       System.IO.StringWriter sWriter = new System.IO.StringWriter(sBuilder); 
       XmlSerializer serializer = new XmlSerializer(typeof(TBL_TEST_TEST[])); 
       serializer.Serialize(sWriter, testRecords); 
       db.insertTestData(sBuilder.ToString()); 
      } 
     } 

     /// <summary> 
     /// An ado.net 2.0 way to mass insert records. This seems to be the fastest. 
     /// http://www.codeproject.com/KB/cs/MultipleInsertsIn1dbTrip.aspx#_Toc196622241 
     /// </summary> 
     private static void BatchBulkCopy() 
     { 
      // Get the DataTable 
      DataTable dtInsertRows = GetDataTable(); 

      using (SqlBulkCopy sbc = new SqlBulkCopy(connectionString, SqlBulkCopyOptions.KeepIdentity)) 
      { 
       sbc.DestinationTableName = "TBL_TEST_TEST"; 

       // Number of records to be processed in one go 
       sbc.BatchSize = 500000; 

       // Map the Source Column from DataTabel to the Destination Columns in SQL Server 2005 Person Table 
       // sbc.ColumnMappings.Add("ID", "ID"); 
       sbc.ColumnMappings.Add("NAME", "NAME"); 

       // Number of records after which client has to be notified about its status 
       sbc.NotifyAfter = dtInsertRows.Rows.Count; 

       // Event that gets fired when NotifyAfter number of records are processed. 
       sbc.SqlRowsCopied += new SqlRowsCopiedEventHandler(sbc_SqlRowsCopied); 

       // Finally write to server 
       sbc.WriteToServer(dtInsertRows); 
       sbc.Close(); 
      } 

     } 


     /// <summary> 
     /// Another ado.net 2.0 way that uses a stored procedure to do a bulk insert. 
     /// Seems slower then "BatchBulkCopy" way and it crashes when you try to insert 500,000 records in one go. 
     /// http://www.codeproject.com/KB/cs/MultipleInsertsIn1dbTrip.aspx#_Toc196622241 
     /// </summary> 
     private static void BatchInsert() 
     { 
      // Get the DataTable with Rows State as RowState.Added 
      DataTable dtInsertRows = GetDataTable(); 

      SqlConnection connection = new SqlConnection(connectionString); 
      SqlCommand command = new SqlCommand("sp_BatchInsert", connection); 
      command.CommandType = CommandType.StoredProcedure; 
      command.UpdatedRowSource = UpdateRowSource.None; 

      // Set the Parameter with appropriate Source Column Name 
      command.Parameters.Add("@Name", SqlDbType.VarChar, 50, dtInsertRows.Columns[0].ColumnName); 

      SqlDataAdapter adpt = new SqlDataAdapter(); 
      adpt.InsertCommand = command; 
      // Specify the number of records to be Inserted/Updated in one go. Default is 1. 
      adpt.UpdateBatchSize = 500000; 

      connection.Open(); 
      int recordsInserted = adpt.Update(dtInsertRows); 
      connection.Close(); 
     } 



     private static DataTable GetDataTable() 
     { 
      // You First need a DataTable and have all the insert values in it 
      DataTable dtInsertRows = new DataTable(); 
      dtInsertRows.Columns.Add("NAME"); 

      for (int i = 0; i < 500000; i++) 
      { 
       DataRow drInsertRow = dtInsertRows.NewRow(); 
       string name = "Name : " + i; 
       drInsertRow["NAME"] = name; 
       dtInsertRows.Rows.Add(drInsertRow); 


      } 
      return dtInsertRows; 

     } 


     static void sbc_SqlRowsCopied(object sender, SqlRowsCopiedEventArgs e) 
     { 
      Console.WriteLine("Number of records affected : " + e.RowsCopied.ToString()); 
     } 


    } 
} 

Odpowiedz

2

rozmiar Wsad jest tam, aby zmniejszyć wpływ opóźnienia w sieci. Nie musi to być więcej niż kilka tysięcy. Wiele instrukcji jest zbieranych razem i wysyłanych jako jednostka, więc dostajesz hit jednej podróży sieciowej raz na każde N, a nie raz na jedno zdanie.

+0

Więc co będzie prefekt wielkość partii w głowie wtedy? Tam, gdzie nie wpływasz na opóźnienia sieci, ale nie robisz 10.000 podróży. – chobo2

+0

Chcesz spróbować uniknąć opóźnień sieci, tj. Maksymalnie zredukować wpływ opóźnień. porcjowanie 1000-10000 jest ogólnie rzecz biorąc w porządku, ale zależy to od tego, ile masz latencji w sieci i ile pamięci możesz sobie pozwolić na użycie w instrukcjach grupowania. Na przykład. jeśli masz N instrukcji i opóźnienie Lms, całkowite opóźnienie wynosi N * L ms. Jeśli partia jest w rozmiarze B, to opóźnienie wynosi N * L/B, więc zmniejsza się opóźnienie o wielkość partii. Zmodyfikuj te zmienne, aby uzyskać całkowite opóźnienie akceptowalne w Twojej sytuacji - np. mały procent czasu wykonania zapytania. – mdma

+0

Czy masz na myśli, że wielkość partii nie ma wpływu na rejestr transakcji i nie jest zatwierdzana? –

1

Czy jest to jednorazowa kopia lub zwykła rzecz?

Jeśli jest to jednorazowa, lub tylko raz dziennie, na przykład, użyj BCP, jest znacznie szybciej, ponieważ używa specjalnych API, które są szybsze niż ado.net.

+0

Byłaby to jednorazowa kopia zbiorcza w przeważającej części (do wstawiania 10 000-50 000 rekordów). W przeciwnym razie zależy to od użytkownika i od tego, co robi. Mogli to robić codziennie lub nigdy, ale byłyby to aktualizacje i prawdopodobnie rozejrzeli się nie więcej niż 50 w jednej wysyłce wsadowej. – chobo2

+0

Użyłem wczoraj BCP do przekazania prawie miliona rekordów z bardzo szerokiego stołu, który ma około 400 pól. Tak więc jest to dokładnie to, czego potrzebujesz do swojego zadania, jeśli jest to jednorazowy z 50 tys. Wierszy. – Chris

+0

Jak długo ci to zajęło? Cóż, patrzę również na cały obraz.Tak jak biorę dokument xml i używając serializacji na nim. Jeśli jest dobry, mógłbym dodać go do innej tablicy obiektów i wywołać ją przy pomocy linq do sql. – chobo2

0

Och, cieszę się, że nie jesteśmy jedynymi, którzy cierpią z powodu tego problemu InsertOnSubmit().

Niedawno nasza firma przeniosła centra danych, a następnie nasze maszyny SQL Server znajdowały się 6 000 mil dalej niż w tym samym kraju.

I nagle zapisanie partii 1800 rekordów zajęło 3,5 minuty, a nie 3-4 sekundy. Nasi użytkownicy nie byli zadowoleni !!

Rozwiązaniem było zastąpienie wywołań InsertOnSubmit za pomocą biblioteki Bulk Insert.

Przeczytaj sekcję "Wstawianie rekordów za pomocą wkładu zbiorczego" na tej stronie. Pokazuje on (bardzo nieliczne) zmiany, które faktycznie trzeba wprowadzić w kodzie, aby obejść ten problem z opóźnieniem.

Trzeba tylko dodać trzy linie kodu i użyć kilku klas C#, które są dostępne na tej stronie.

http://mikesknowledgebase.com/pages/LINQ/InsertAndDeletes.htm

Powiązane problemy