使用 Sql 插入语句和 SqlBulkCopy 插入数据有什么区别?

What is the difference between inserting data using Sql insert statements and SqlBulkCopy?

我在向 SQL 服务器插入大量数据时遇到问题。

之前我使用的是 Entity framework,但是对于仅 100K 根级记录(分别包含两个不同的集合,其中每个集合进一步操作大约 200K 条记录)来说速度太慢了 = 大约 500K-600K 条记录在记忆中。 在这里我应用了所有优化(例如 AutoDetectChangesEnabled = false,并在每批之后重新创建和处理上下文。)

我拒绝了该解决方案,并使用了 BulkInsert,它非常快速且高效。只能在一分钟左右插入 100K 条记录。

但主要问题是从新插入的记录中取回主键。为此,我正在考虑编写可以在 TVP 上运行的存储过程(即内存数据table 持有所有根级 100K 记录)。 在里面我会使用 OUTPUT INSERTED.Id 来获取应用程序中的所有主键)。

那么,我如何比较这种方法(即 Sql 在存储过程中插入查询)与 SqlBulkCopy 方法。

知道我是否可以在 SqlBulkCopy 操作后取回所有主键吗?或者关于 OUTPUT Inserted.Id 的具体内容 return 应用程序中所有正确的新密钥。

PS : 我不想在此过程中创建任何暂存 table。这只是开销。

这是一个基于评论中的讨论/扩展此处提到的想法的示例:Possible to get PrimayKey IDs back after a SQL BulkCopy?

  • 从 C# 批量上传到 SQL
  • 中的临时文件 table
  • 使用Sql将数据从临时table复制到实际table(此时生成ID),return ID。

我还没有机会对此进行测试,但希望这会有所帮助:

//using System.Data.SqlClient;
//using System.Collections.Generic;

public DataTable CreatePersonDataTable(IEnumerable<PersonDTO> people) 
{
    //define the table
    var table = new DataTable("People");
    table.Columns.Add(new DataColumn("Name", typeof(string)));
    table.Columns.Add(new DataColumn("DOB", typeof(DateTime)));
    //populate it
    foreach (var person in people)
    {
        table.Rows.Add(person.Name, person.DOB);
    }
    return table;
}

readonly string ConnectionString; //set this in the constructor
readonly int BulkUploadPeopleTimeoutSeconds = 600; //default; could override in constructor
public IEnumerable<long> BulkUploadPeople(IEnumerable<PersonDTO> people) //you'd want to break this up a bit; for simplicty I've bunged everything into one big method
{
    var data = CreatePersonDataTable(people);
    using(SqlConnection con = new SqlConnection(ConnectionString)) 
    {
        con.Open(); //keep same connection open throughout session
        RunSqlNonQuery(con, "select top 0 Name, DOB into #People from People");
        BulkUpload(con, data, "#People");
        var results = TransferFromTempToReal(con, "#People", "People", "Name, DOB", "Id");
        RunSqlNonQuery(con, "drop table #People");  //not strictly required since this would be removed when the connection's closed as it's session scoped; but best to keep things clean.
    }
    return results;
}
private void RunSqlNonQuery(SqlConnection con, string sql)
{
    using (SqlCommand command = con.CreateCommand())
    {
        command.CommandText = sql;
        command.ExecuteNonQuery();      
    }
}
private void BulkUpload(SqlConnection con, DataTable data, string targetTable)
{
    using(SqlBulkCopy bulkCopy = new SqlBulkCopy(con))
    {
        bulkCopy.BulkCopyTimeout = 600; //define this in your config 
        bulkCopy.DestinationTableName = targetTable; 
        bulkCopy.WriteToServer(data);         
    }
}
private IEnumerable<long> TransferFromTempToReal(SqlConnection con, string tempTable, string realTable, string columnNames, string idColumnName)
{
    using (SqlCommand command = con.CreateCommand())
    {
        command.CommandText = string.Format("insert into {0} output inserted.{1} select {2} from {3}", realTable, idColumnName, columnNames, tempTable);
        using (SqlDataReader reader = command.ExecuteReader()) 
        {
            while(reader.Read()) 
            {
                yield return r.GetInt64(0);
            }
        }
    }
}

虽然在您的问题中您添加了您不想使用分期 table,因为它是 "overhead"...请尝试。您可能会发现创建暂存的小开销 table 小于使用此方法的性能增益。

显然,它不会像插入和忽略 returned id 那样快;但如果这是你的要求,在没有其他答案的情况下,这可能是最好的选择。

Any idea if somehow, I can get all primary keys back after SqlBulkCopy operation

你不能。无法直接从 SqlBulkCopy 执行此操作。

PS : I don't want to create any staging table during the process. This is just an overhead.

不幸的是,如果您想取回主键,您将需要这样做或使用其他方法(如您建议的 TVP)。

免责声明:我是Entity Framework Extensions

的所有者

另一种解决方案是使用已经支持 Entity Framework 的 BulkInsert 的库。在幕后,它使用 SqlBulkCopy + 暂存表。

默认情况下,BulkInsert 方法已经输出了主键值。

图书馆不是免费的,但是,它为您的公司增加了一些灵活性,您将不需要 code/support 任何东西。

示例:

// Easy to use
context.BulkSaveChanges();

// Easy to customize
context.BulkSaveChanges(bulk => bulk.BatchSize = 100);

// Perform Bulk Operations
context.BulkDelete(customers);
context.BulkInsert(customers);
context.BulkUpdate(customers);

// Customize Primary Key
context.BulkMerge(customers, operation => {
   operation.ColumnPrimaryKeyExpression = 
        customer => customer.Code;
});