Doe alsjeblieft niet maak een DataTable
te laden via BulkCopy. Dat is een prima oplossing voor kleinere datasets, maar er is absoluut geen reden om alle 10 miljoen rijen in het geheugen te laden voordat de database wordt aangeroepen.
Uw beste gok (buiten BCP
/ BULK INSERT
/ OPENROWSET(BULK...)
) is om de inhoud van het bestand naar de database te streamen via een Table-Valued Parameter (TVP). Door een TVP te gebruiken, kunt u het bestand openen, een rij lezen en een rij verzenden totdat u klaar bent en vervolgens het bestand sluiten. Deze methode heeft een geheugenvoetafdruk van slechts een enkele rij. Ik heb een artikel geschreven, Gegevens streamen naar SQL Server 2008 vanuit een toepassing, waarin een voorbeeld van dit scenario staat.
Een simplistisch overzicht van de structuur is als volgt. Ik ga uit van dezelfde importtabel en veldnaam als getoond in de bovenstaande vraag.
Vereiste database-objecten:
-- 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 om gebruik te maken van de bovenstaande SQL-objecten staat hieronder. Merk op dat in plaats van een object (bijv. DataTable) te vullen en vervolgens de Opgeslagen Procedure uit te voeren, het bij deze methode de uitvoering van de Opgeslagen Procedure is die het lezen van de bestandsinhoud initieert. De invoerparameter van de Stored Proc is geen variabele; het is de retourwaarde van een methode, GetFileContents
. Die methode wordt aangeroepen wanneer de SqlCommand
roept ExecuteNonQuery
. aan , die het bestand opent, een rij leest en de rij naar SQL Server stuurt via de IEnumerable<SqlDataRecord>
en yield return
constructies en sluit vervolgens het bestand. De opgeslagen procedure ziet alleen een tabelvariabele, @ImportTable, die toegankelijk is zodra de gegevens binnenkomen (opmerking:de gegevens blijven een korte tijd bestaan, zelfs als niet de volledige inhoud, 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();
}
}
De GetFileContents
bovenstaande methode wordt gebruikt als de invoerparameterwaarde voor de Opgeslagen Procedure zoals hieronder getoond:
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;
}
Aanvullende opmerkingen:
- Met enige aanpassing kan de bovenstaande C#-code worden aangepast om de gegevens in batches te verwerken.
- Met een kleine wijziging kan de bovenstaande C#-code worden aangepast om in meerdere velden te worden verzonden (het voorbeeld dat wordt getoond in het artikel "Steaming Data..." waarnaar hierboven is gelinkt, wordt in 2 velden doorgegeven).
- U kunt ook de waarde van elk record manipuleren in de
SELECT
verklaring in het proces. - Je kunt rijen ook uitfilteren door een WHERE-voorwaarde in het proces te gebruiken.
- Je hebt meerdere keren toegang tot de TVP-tabelvariabele; het is ALLEEN LEZEN maar niet "alleen doorsturen".
- Voordelen boven
SqlBulkCopy
:SqlBulkCopy
is alleen INSERT, terwijl het gebruik van een TVP ervoor zorgt dat de gegevens op elke manier kunnen worden gebruikt:u kuntMERGE
aanroepen; je kuntDELETE
gebaseerd op een bepaalde voorwaarde; u kunt de gegevens in meerdere tabellen splitsen; enzovoort.- Omdat een TVP niet alleen INSERT is, heb je geen aparte faseringstabel nodig om de gegevens in te dumpen.
- U kunt gegevens uit de database terughalen door
ExecuteReader
te bellen in plaats vanExecuteNonQuery
. Als er bijvoorbeeld eenIDENTITY
veld op deDATAs
importtabel, kunt u eenOUTPUT
. toevoegen clausule toe aan deINSERT
omINSERTED.[ID]
terug te geven (ervan uitgaande datID
is de naam van deIDENTITY
veld). Of u kunt de resultaten van een geheel andere zoekopdracht doorgeven, of beide, aangezien meerdere resultatensets kunnen worden verzonden en geopend viaReader.NextResult()
. Informatie terughalen uit de database is niet mogelijk bij gebruik vanSqlBulkCopy
toch zijn er verschillende vragen hier op S.O. van de mensen die precies dat willen doen (tenminste met betrekking tot de nieuw gecreëerdeIDENTITY
waarden). - Voor meer informatie over waarom het soms sneller is voor het algehele proces, zelfs als het iets langzamer is om de gegevens van schijf naar SQL Server te krijgen, raadpleegt u deze whitepaper van het SQL Server Customer Advisory Team:Maximizing Throughput with TVP