从C#列表批量插入SQL Server到具有外键约束的多个表

时间:2014-08-15 12:15:20

标签: c# sql sql-server performance bulkinsert

我完全不知道这个问题,任何帮助都会受到高度赞赏:

我有两个表,一个是主数据表(Table A),另一个表(Table B)与一个条目的多个条目(具体为18)具有外键关系Table A

我在列表中获取数据并希望将其插入SQL Server数据库。

我目前正在使用以下模式,但 14分钟Table A中插入100行,在Table B中插入相应的18 * 100行。

using (SqlConnection conn = new SqlConnection(conStr))
{
    foreach (var ticket in Tickets)
    {
        sql = string.Format(@"INSERT INTO dbo.Tickets([ColumnA], [ColumnB] ,..." + @")
                              VALUES(@ColumnA, @ColumnB,@ColumnC, @ColumnD, .... +
                            @"SELECT SCOPE_IDENTITY();");

        using (cmd = new SqlCommand(sql, conn))
        {
            cmd.Parameters.AddWithValue("@ColumnA", (object)ticket.Id ?? DBNull.Value);
            cmd.Parameters.AddWithValue("@ColumnB", (object)ticket.Address ?? DBNull.Value);
            cmd.Parameters.AddWithValue("@ColumnC", (object)ticket.Status?? DBNull.Value);
            ....

            conn.Open();
            TableA_TicketId = Convert.ToInt32(cmd.ExecuteScalar());
        }
    }
} 

我使用SCOPE_IDENTITY()从表A中获取插入的每个记录的最新标识,并将其用于插入第二个表

sql = string.Format(@"INSERT INTO Tickets_Fields ([TableA_TicketId], [FieldName], [Key],[Value]) 
                      VALUES (@TableA_TicketId, @FieldName, @Key, @Value);");

using (cmd = new SqlCommand(sql, conn))
{
    foreach (var customField in ticket.CustomFields)
    {
        cmd.Parameters.Clear();
        cmd.Parameters.AddWithValue("@TableA_TicketId", (object)TicketId ?? DBNull.Value);
        cmd.Parameters.AddWithValue("@FieldName", (object)"CustomField" ?? DBNull.Value);
        ...
        cmd.ExecuteNonQuery();
    }
}

conn.Close();

请建议我是否可以通过任何方式提高此代码的性能。或者他们有更好/更快的方式吗?

3 个答案:

答案 0 :(得分:4)

一些想法:

  1. 在整批插入期间保持相同的连接处于打开状态。在开头打开它,然后在你完成时关闭它。

  2. 在每次循环迭代期间不要重新创建SqlCommand s。一开始就创建一次,然后只更新参数'值:cmd.Parameters["@x"].Value = …;

  3. 您通过插入单个记录的foreach循环插入第二个表(B)。您可以考虑将其替换为单个INSERT INTO TableB (x, y, z) SELECT x, y, z FROM @tvp,其中@tvptable-valued parameter。从本质上讲,这意味着您可以填充例如一个DataTable,其中包含要插入第二个表格的行,然后将DataTable作为@tvp传递过来。从SQL Server 2008开始支持TVP,IIRC。设置其中之一需要第一次进行一些研究。

    (我不确定上述INSERT语句是否真的有效,或者TVP是否仅作为存储过程的参数(see e.g. this example)。)

  4. 比#3更进一步,将插入的表A和B移动到DB存储过程中。该SP将具有进入表A的值作为参数,以及具有进入表B的记录的表值参数。

答案 1 :(得分:2)

SqlBulkCopy是你的朋友

using System;
using System.Data;
using System.Data.SqlClient;

namespace SqlBulkInsertExample
{
class Program
{
  static void Main(string[] args)
  {
        DataTable prodSalesData = new DataTable("ProductSalesData");

        // Create Column 1: SaleDate
        DataColumn dateColumn = new DataColumn();
        dateColumn.DataType = Type.GetType("System.DateTime");
        dateColumn.ColumnName = "SaleDate";

        // Create Column 2: ProductName
        DataColumn productNameColumn = new DataColumn();
        productNameColumn.ColumnName = "ProductName";

        // Create Column 3: TotalSales
        DataColumn totalSalesColumn = new DataColumn();
        totalSalesColumn.DataType = Type.GetType("System.Int32");
        totalSalesColumn.ColumnName = "TotalSales";

        // Add the columns to the ProductSalesData DataTable
        prodSalesData.Columns.Add(dateColumn);
        prodSalesData.Columns.Add(productNameColumn);
        prodSalesData.Columns.Add(totalSalesColumn);

        // Let's populate the datatable with our stats.
        // You can add as many rows as you want here!

        // Create a new row
        DataRow dailyProductSalesRow = prodSalesData.NewRow();
        dailyProductSalesRow["SaleDate"] = DateTime.Now.Date;
        dailyProductSalesRow["ProductName"] = "Nike";
        dailyProductSalesRow["TotalSales"] = 10;

        // Add the row to the ProductSalesData DataTable
        prodSalesData.Rows.Add(dailyProductSalesRow);

        // Copy the DataTable to SQL Server using SqlBulkCopy
        using (SqlConnection dbConnection = new SqlConnection("Data Source=ProductHost;Initial Catalog=dbProduct;Integrated Security=SSPI;Connection Timeout=60;Min Pool Size=2;Max Pool Size=20;"))
        {
            dbConnection.Open();
            using (SqlBulkCopy s = new SqlBulkCopy(dbConnection))
            {
                s.DestinationTableName = prodSalesData.TableName;

                foreach (var column in prodSalesData.Columns)
                    s.ColumnMappings.Add(column.ToString(), column.ToString());

                s.WriteToServer(prodSalesData);
            }
        }
    }
}
}

请注意,默认情况下,它会锁定表,直到完成为止,这意味着在网站上工作的任何其他人都无法写入同一个表。

要解决这个问题,您可以设置SqlBulkCopy.BatchSize,但是您必须注意,如果导入失败,您将负责删除已提交的行。

答案 2 :(得分:0)

您应该使用SqlTransaction或TransactionScope来确保两个表中的插入成功。

表A中的Grab Max(id)。 使用类似于此的内容在表A中插入记录:

using (var connection = new SqlConnection(ConfigurationManager.ConnectionStrings["SomeConnectionString"].ConnectionString))
    {
         connection.Open();
         SqlTransaction transaction = connection.BeginTransaction();

         using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.Default, transaction))
         {
            bulkCopy.BatchSize = 100;
            bulkCopy.DestinationTableName = "dbo.Person";
            try
            {
                bulkCopy.WriteToServer(listPerson.AsDataTable());
            }
            catch (Exception)
            {
                transaction.Rollback();
                connection.Close();
            }
          }

          transaction.Commit();
    }

然后将记录插入表B. 您将知道您需要从哪个ID计算ID,因为您在插入之前已选择了Max(id)。

有关BulkInsert with minimum lines of code的完整示例,请参阅此文章。