Por favor, não crie uma
DataTable
para carregar via BulkCopy. Essa é uma solução aceitável para conjuntos de dados menores, mas não há absolutamente nenhuma razão para carregar todas as 10 milhões de linhas na memória antes de chamar o banco de dados. Sua melhor aposta (fora do
BCP
/ BULK INSERT
/ OPENROWSET(BULK...)
) é transmitir o conteúdo do arquivo para o banco de dados por meio de um parâmetro com valor de tabela (TVP). Ao usar um TVP, você pode abrir o arquivo, ler uma linha e enviar uma linha até terminar e, em seguida, fechar o arquivo. Esse método tem um volume de memória de apenas uma única linha. Eu escrevi um artigo, Streaming Data Into SQL Server 2008 From an Application, que tem um exemplo desse cenário. Uma visão geral simplista da estrutura é a seguinte. Estou assumindo a mesma tabela de importação e nome de campo, conforme mostrado na pergunta acima.
Objetos de banco de dados necessários:
-- 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
O código do aplicativo C# para fazer uso dos objetos SQL acima está abaixo. Observe como ao invés de preencher um objeto (por exemplo, DataTable) e então executar o Stored Procedure, neste método é a execução do Stored Procedure que inicia a leitura do conteúdo do arquivo. O parâmetro de entrada do Stored Proc não é uma variável; é o valor de retorno de um método,
GetFileContents
. Esse método é chamado quando o SqlCommand
chama ExecuteNonQuery
, que abre o arquivo, lê uma linha e envia a linha para o SQL Server por meio do IEnumerable<SqlDataRecord>
e yield return
constrói e, em seguida, fecha o arquivo. O procedimento armazenado apenas vê uma variável de tabela, @ImportTable, que pode ser acessada assim que os dados começarem a chegar (nota:os dados persistem por um curto período de tempo, mesmo que não seja o conteúdo completo, em 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();
}
}
O
GetFileContents
método acima é usado como o valor do parâmetro de entrada para o procedimento armazenado conforme mostrado abaixo: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;
}
Notas Adicionais:
- Com algumas modificações, o código C# acima pode ser adaptado para agrupar os dados.
- Com pequenas modificações, o código C# acima pode ser adaptado para enviar em vários campos (o exemplo mostrado no artigo "Steaming Data..." vinculado acima passa em 2 campos).
- Você também pode manipular o valor de cada registro no
SELECT
instrução no proc. - Você também pode filtrar linhas usando uma condição WHERE no proc.
- Você pode acessar a variável de tabela TVP várias vezes; é READONLY, mas não "somente encaminhamento".
- Vantagens sobre
SqlBulkCopy
:SqlBulkCopy
é somente INSERT, enquanto o uso de um TVP permite que os dados sejam usados de qualquer maneira:você pode chamarMERGE
; você podeDELETE
com base em alguma condição; você pode dividir os dados em várias tabelas; e assim por diante.- Devido a um TVP não ser somente INSERT, você não precisa de uma tabela de preparação separada para despejar os dados.
- Você pode obter dados do banco de dados chamando
ExecuteReader
em vez deExecuteNonQuery
. Por exemplo, se houver umaIDENTITY
campo noDATAs
import tabela, você pode adicionar umOUTPUT
cláusula para oINSERT
para retornarINSERTED.[ID]
(assumindoID
é o nome daIDENTITY
campo). Ou você pode repassar os resultados de uma consulta completamente diferente, ou ambos, pois vários conjuntos de resultados podem ser enviados e acessados viaReader.NextResult()
. Não é possível obter informações do banco de dados ao usarSqlBulkCopy
ainda há várias perguntas aqui no S.O. de pessoas querendo fazer exatamente isso (pelo menos no que diz respeito ao recém-criadoIDENTITY
valores). - Para obter mais informações sobre por que às vezes é mais rápido para o processo geral, mesmo que um pouco mais lento ao obter os dados do disco para o SQL Server, consulte este whitepaper da equipe de consultoria ao cliente do SQL Server:Maximizing Throughput with TVP >