Proszę nie utwórz DataTable
do załadowania za pomocą BulkCopy. Jest to dobre rozwiązanie dla mniejszych zestawów danych, ale nie ma absolutnie żadnego powodu, aby ładować wszystkie 10 milionów wierszy do pamięci przed wywołaniem bazy danych.
Twój najlepszy zakład (poza BCP
/ BULK INSERT
/ OPENROWSET(BULK...)
) ma na celu strumieniowe przesyłanie zawartości pliku do bazy danych za pośrednictwem parametru z wartościami tabelarycznymi (TVP). Korzystając z TVP możesz otworzyć plik, odczytać wiersz i wysłać wiersz, aż skończysz, a następnie zamknąć plik. Ta metoda ma ślad w pamięci wynoszący tylko jeden wiersz. Napisałem artykuł, Strumieniowanie danych do SQL Server 2008 z aplikacji, który zawiera przykład tego właśnie scenariusza.
Uproszczony przegląd struktury jest następujący. Zakładam tę samą nazwę tabeli i pola importu, jak pokazano w powyższym pytaniu.
Wymagane obiekty bazy danych:
-- 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
Kod aplikacji C# do korzystania z powyższych obiektów SQL znajduje się poniżej. Zauważ, że zamiast wypełniania obiektu (np. DataTable), a następnie wykonywania procedury składowanej, w tej metodzie to wykonanie procedury składowanej inicjuje odczyt zawartości pliku. Parametr wejściowy Stored Proc nie jest zmienną; jest to wartość zwracana przez metodę, GetFileContents
. Ta metoda jest wywoływana, gdy SqlCommand
wywołuje ExecuteNonQuery
, który otwiera plik, odczytuje wiersz i wysyła go do SQL Server za pośrednictwem IEnumerable<SqlDataRecord>
i yield return
konstrukcje, a następnie zamyka plik. Procedura składowana po prostu widzi zmienną tabeli @ImportTable, do której można uzyskać dostęp, gdy tylko dane zaczną się pojawiać (uwaga:dane utrzymują się przez krótki czas, nawet jeśli nie pełna zawartość, w 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
Powyższa metoda jest używana jako wartość parametru wejściowego dla procedury składowanej, jak pokazano poniżej:
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;
}
Dodatkowe uwagi:
- Z pewnymi modyfikacjami, powyższy kod C# można dostosować do wsadu danych.
- Z drobną modyfikacją powyższy kod C# można dostosować do wysyłania w wielu polach (przykład pokazany w artykule „Steaming Data...” połączonym powyżej przechodzi w 2 pola).
- Możesz także manipulować wartością każdego rekordu w
SELECT
oświadczenie w proc. - Możesz także odfiltrować wiersze, używając warunku WHERE w procedurze.
- Możesz wielokrotnie uzyskiwać dostęp do zmiennej tabeli TVP; jest TYLKO DO ODCZYTU, ale nie „tylko do przodu”.
- Przewagi nad
SqlBulkCopy
:SqlBulkCopy
jest tylko INSERT, podczas gdy korzystanie z TVP pozwala na wykorzystanie danych w dowolny sposób:możesz wywołaćMERGE
; możeszDELETE
na podstawie pewnego warunku; możesz podzielić dane na wiele tabel; i tak dalej.- Ponieważ TVP nie jest tylko INSERT, nie potrzebujesz oddzielnej tabeli pomostowej, aby zrzucić dane.
- Możesz odzyskać dane z bazy danych, wywołując
ExecuteReader
zamiastExecuteNonQuery
. Na przykład, jeśli istniejeIDENTITY
pole naDATAs
importuj tabelę, możesz dodaćOUTPUT
klauzula doINSERT
aby przekazaćINSERTED.[ID]
(zakładającID
to nazwaIDENTITY
pole). Możesz też przekazać wyniki zupełnie innego zapytania, albo jedno i drugie, ponieważ wiele zestawów wyników można wysłać i uzyskać do nich dostęp za pomocą funkcjiReader.NextResult()
. Pobieranie informacji z bazy danych nie jest możliwe przy użyciuSqlBulkCopy
jednak jest kilka pytań tutaj na S.O. ludzi, którzy chcą to zrobić (przynajmniej w odniesieniu do nowo utworzonejIDENTITY
wartości). - Aby uzyskać więcej informacji na temat tego, dlaczego czasami cały proces jest szybszy, nawet jeśli jest nieco wolniejszy przy pobieraniu danych z dysku do SQL Server, zapoznaj się z tym dokumentem zespołu doradztwa dla klientów SQL Server:Maksymalizacja przepustowości dzięki TVP