Bitte nicht Erstellen Sie eine DataTable
per BulkCopy zu laden. Das ist eine gute Lösung für kleinere Datensätze, aber es gibt absolut keinen Grund, alle 10 Millionen Zeilen in den Speicher zu laden, bevor die Datenbank aufgerufen wird.
Ihre beste Chance (außerhalb von BCP
/ BULK INSERT
/ OPENROWSET(BULK...)
) besteht darin, den Inhalt aus der Datei über einen Tabellenwertparameter (TVP) in die Datenbank zu streamen. Durch die Verwendung eines TVP können Sie die Datei öffnen, eine Zeile lesen und eine Zeile senden, bis Sie fertig sind, und dann die Datei schließen. Diese Methode hat einen Speicherbedarf von nur einer einzigen Zeile. Ich habe einen Artikel „Streaming Data into SQL Server 2008 From an Application“ geschrieben, der ein Beispiel für genau dieses Szenario enthält.
Ein vereinfachter Überblick über die Struktur ist wie folgt. Ich gehe davon aus, dass die gleiche Importtabelle und der gleiche Feldname wie in der obigen Frage gezeigt werden.
Erforderliche Datenbankobjekte:
-- 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#-App-Code zur Verwendung der oben genannten SQL-Objekte finden Sie unten. Beachten Sie, dass anstatt ein Objekt (z. B. DataTable) zu füllen und dann die gespeicherte Prozedur auszuführen, bei dieser Methode die Ausführung der gespeicherten Prozedur das Lesen des Dateiinhalts initiiert. Der Eingabeparameter der Stored Proc ist keine Variable; es ist der Rückgabewert einer Methode, GetFileContents
. Diese Methode wird aufgerufen, wenn der SqlCommand
ruft ExecuteNonQuery
auf , die die Datei öffnet, eine Zeile liest und die Zeile über IEnumerable<SqlDataRecord>
an SQL Server sendet und yield return
erstellt und schließt dann die Datei. Die gespeicherte Prozedur sieht nur eine Tabellenvariable, @ImportTable, auf die zugegriffen werden kann, sobald die Daten herüberkommen (Hinweis:Die Daten bleiben für kurze Zeit, auch wenn nicht der vollständige Inhalt, in 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();
}
}
Die GetFileContents
Die obige Methode wird als Eingabeparameterwert für die gespeicherte Prozedur verwendet, wie unten gezeigt:
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;
}
Zusätzliche Anmerkungen:
- Mit einigen Modifikationen kann der obige C#-Code angepasst werden, um die Daten per Batch einzufügen.
- Mit geringfügigen Änderungen kann der obige C#-Code so angepasst werden, dass er mehrere Felder einsendet (das im oben verlinkten Artikel „Steaming Data...“ gezeigte Beispiel passt in 2 Felder).
- Sie können auch den Wert jedes Datensatzes im
SELECT
manipulieren Anweisung in der Prozedur. - Sie können Zeilen auch herausfiltern, indem Sie eine WHERE-Bedingung in der Prozedur verwenden.
- Sie können mehrmals auf die TVP-Tabellenvariable zugreifen; es ist READONLY, aber nicht "forward only".
- Vorteile gegenüber
SqlBulkCopy
:SqlBulkCopy
ist nur INSERT, während die Verwendung eines TVP es erlaubt, die Daten auf beliebige Weise zu verwenden:Sie könnenMERGE
aufrufen; Sie könnenDELETE
basierend auf einer Bedingung; Sie können die Daten in mehrere Tabellen aufteilen; und so weiter.- Da ein TVP nicht nur INSERT-fähig ist, benötigen Sie keine separate Staging-Tabelle, in die die Daten ausgegeben werden.
- Sie können Daten aus der Datenbank zurückholen, indem Sie
ExecuteReader
aufrufen stattExecuteNonQuery
. Zum Beispiel, wenn es eineIDENTITY
gab Feld auf denDATAs
import-Tabelle, könnten Sie einenOUTPUT
hinzufügen -Klausel zumINSERT
umINSERTED.[ID]
zurückzugeben (vorausgesetztID
ist der Name derIDENTITY
Feld). Oder Sie können die Ergebnisse einer völlig anderen Abfrage zurückgeben, oder beides, da mehrere Ergebnissätze gesendet und überReader.NextResult()
abgerufen werden können . Das Abrufen von Informationen aus der Datenbank ist nicht möglich, wennSqlBulkCopy
verwendet wird dennoch gibt es hier mehrere Fragen zu S.O. der Leute, die genau das tun wollen (zumindest in Bezug auf die neu geschaffeneIDENTITY
Werte). - Weitere Informationen dazu, warum der Gesamtprozess manchmal schneller ist, wenn auch etwas langsamer beim Einlesen der Daten von der Festplatte in SQL Server, finden Sie in diesem Whitepaper des SQL Server-Kundenberatungsteams:Maximieren des Durchsatzes mit TVP