sql >> Databasteknik >  >> RDS >> Sqlserver

Hur kan jag infoga 10 miljoner poster på kortast möjliga tid?

Vänligen gör inte skapa en DataTable att ladda via BulkCopy. Det är en ok lösning för mindre uppsättningar data, men det finns absolut ingen anledning att ladda alla 10 miljoner rader i minnet innan du anropar databasen.

Din bästa insats (utanför BCP / BULK INSERT / OPENROWSET(BULK...) ) är att strömma innehållet från filen till databasen via en Table-Valued Parameter (TVP). Genom att använda en TVP kan du öppna filen, läsa en rad och skicka en rad tills den är klar, och sedan stänga filen. Denna metod har ett minnesfotavtryck på bara en enda rad. Jag skrev en artikel, Streaming Data Into SQL Server 2008 From an Application, som har ett exempel på just detta scenario.

En förenklad översikt av strukturen är följande. Jag antar samma importtabell och fältnamn som visas i frågan ovan.

Obligatoriska databasobjekt:

-- 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

C#-appkod för att använda ovanstående SQL-objekt finns nedan. Lägg märke till hur istället för att fylla upp ett objekt (t.ex. DataTable) och sedan exekvera den lagrade proceduren, i denna metod är det exekveringen av den lagrade proceduren som initierar läsningen av filinnehållet. Ingångsparametern för lagrad proc är inte en variabel; det är returvärdet för en metod, GetFileContents . Den metoden anropas när SqlCommand anropar ExecuteNonQuery , som öppnar filen, läser en rad och skickar raden till SQL Server via IEnumerable<SqlDataRecord> och yield return konstruerar och stänger sedan filen. Den lagrade proceduren ser bara en tabellvariabel, @ImportTable, som kan nås så snart data börjar komma över (observera:data kvarstår en kort tid, även om inte hela innehållet, i 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 Metoden ovan används som indataparametervärde för den lagrade proceduren enligt nedan:

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;
}

Ytterligare anmärkningar:

  1. Med viss modifiering kan ovanstående C#-kod anpassas för att batcha data in.
  2. Med mindre modifieringar kan ovanstående C#-kod anpassas för att skicka i flera fält (exemplet som visas i artikeln "Steaming Data..." länkad ovan går i 2 fält).
  3. Du kan också manipulera värdet för varje post i SELECT uttalande i proc.
  4. Du kan också filtrera bort rader genom att använda ett WHERE-villkor i proc.
  5. Du kan komma åt TVP-tabellvariabeln flera gånger; det är SKRIVBARA men inte "endast vidarebefordra".
  6. Fördelar jämfört med SqlBulkCopy :
    1. SqlBulkCopy är INSERT-bara medan användning av en TVP tillåter att data används på alla sätt:du kan anropa MERGE; du kan DELETE baserat på något villkor; du kan dela upp data i flera tabeller; och så vidare.
    2. På grund av att en TVP inte är enbart INSERT behöver du inte en separat mellanställningstabell för att dumpa data till.
    3. Du kan få tillbaka data från databasen genom att anropa ExecuteReader istället för ExecuteNonQuery . Till exempel om det fanns en IDENTITY fältet i DATAs importtabell kan du lägga till en OUTPUT sats till INSERT för att skicka tillbaka INSERTED.[ID] (förutsatt ID är namnet på IDENTITY fält). Eller så kan du skicka tillbaka resultaten av en helt annan fråga, eller båda eftersom flera resultatuppsättningar kan skickas och nås via Reader.NextResult() . Att få tillbaka information från databasen är inte möjligt när du använder SqlBulkCopy ändå finns det flera frågor här om S.O. av människor som vill göra exakt det (åtminstone med avseende på den nyskapade IDENTITY värden).
    4. För mer information om varför det ibland går snabbare för den övergripande processen, även om det är något långsammare när det gäller att överföra data från disk till SQL Server, se detta whitepaper från SQL Server Customer Advisory Team:Maximizing Throughput with TVP


  1. Android Sqlite prestanda

  2. Skala ut Moodle-databasen

  3. Konfigurera AlwaysOn-tillgänglighetsgrupper - Del 2

  4. SQL Pivot med flera kolumner