【问题标题】:How should I multiple insert multiple records?我应该如何多次插入多条记录?
【发布时间】:2010-06-04 09:45:06
【问题描述】:

我有一个名为Entry 的类声明如下:

class Entry{
    string Id {get;set;}
    string Name {get;set;}
}  

然后是一个接受多个此类Entry 对象以使用ADO.NET 插入数据库的方法:

static void InsertEntries(IEnumerable<Entry> entries){
    //build a SqlCommand object
    using(SqlCommand cmd = new SqlCommand()){
        ...
        const string refcmdText = "INSERT INTO Entries (id, name) VALUES (@id{0},@name{0});";
        int count = 0;
        string query = string.Empty;
        //build a large query
        foreach(var entry in entries){
            query += string.Format(refcmdText, count);
            cmd.Parameters.AddWithValue(string.Format("@id{0}",count), entry.Id);
            cmd.Parameters.AddWithValue(string.Format("@name{0}",count), entry.Name);
            count++;
        }
        cmd.CommandText=query;
        //and then execute the command
        ...
    }
}  

我的问题是:我应该继续使用上述发送多个插入语句的方式(构建一个巨大的插入语句及其参数字符串并通过网络发送),还是应该保持开放连接并发送一个每个 Entry 的单个插入语句如下:

using(SqlCommand cmd = new SqlCommand(){
    using(SqlConnection conn = new SqlConnection(){
        //assign connection string and open connection
        ...
        cmd.Connection = conn;
        foreach(var entry in entries){
            cmd.CommandText= "INSERT INTO Entries (id, name) VALUES (@id,@name);";
            cmd.Parameters.AddWithValue("@id", entry.Id);
            cmd.Parameters.AddWithValue("@name", entry.Name);
            cmd.ExecuteNonQuery();
        }
    }
 }  

你怎么看?两者之间的Sql Server会不会有性能差异?还有其他我应该注意的后果吗?

【问题讨论】:

  • 感谢您的所有建议!我会接受@Giorgi 的回答,因为它或多或少地回答了原始问题
  • 可以在SQl server中使用user-definedtable类型将DataTable传递给SQL serverfourthbottle.com/2014/09/…

标签: c# sql-server-2005 ado.net multiple-insert


【解决方案1】:
static void InsertSettings(IEnumerable<Entry> settings) {
    using (SqlConnection oConnection = new SqlConnection("Data Source=(local);Initial Catalog=Wip;Integrated Security=True")) {
        oConnection.Open();
        using (SqlTransaction oTransaction = oConnection.BeginTransaction()) {
            using (SqlCommand oCommand = oConnection.CreateCommand()) {
                oCommand.Transaction = oTransaction;
                oCommand.CommandType = CommandType.Text;
                oCommand.CommandText = "INSERT INTO [Setting] ([Key], [Value]) VALUES (@key, @value);";
                oCommand.Parameters.Add(new SqlParameter("@key", SqlDbType.NChar));
                oCommand.Parameters.Add(new SqlParameter("@value", SqlDbType.NChar));
                try {
                    foreach (var oSetting in settings) {
                        oCommand.Parameters[0].Value = oSetting.Key;
                        oCommand.Parameters[1].Value = oSetting.Value;
                        if (oCommand.ExecuteNonQuery() != 1) {
                            //'handled as needed, 
                            //' but this snippet will throw an exception to force a rollback
                            throw new InvalidProgramException();
                        }
                    }
                    oTransaction.Commit();
                } catch (Exception) {
                    oTransaction.Rollback();
                    throw;
                }
            }
        }
    }
}

【讨论】:

  • 我宁愿通过参数名称来引用参数,而不是 int 索引。仍然,+1。旧答案,仍然适用。
  • 是的,但是在这种情况下,添加参数然后对它们进行索引的语句在同一个执行流程中紧密相连,因此不会混淆。此外,使用索引是访问参数的最快方式。我写这个会考虑性能,并基于一些现有的 Microsoft 库源代码。
【解决方案2】:

如果我是你,我不会使用其中任何一个。

第一个的缺点是如果列表中有相同的值,参数名称可能会发生冲突。

第二个的缺点是你要为每个实体创建命令和参数。

最好的方法是让命令文本和参数构造一次(使用Parameters.Add 添加参数)在循环中更改它们的值并执行命令。这样,该语句将只准备一次。您还应该在开始循环之前打开连接并在之后关闭它。

【讨论】:

  • SQLCommand 针对 Giorgi 描述的过程进行了优化。正如蒂姆指出的那样,底层的连接将被保持。我也会使用 Tim 推荐的 Transaction
  • 那么,cmd.ExecuteNonQuery(); 也应该在循环内?
  • @Giorgi 参数个数可以超过2100的限制怎么办?
  • 如果可以使用SqlBulkCopy,谁会做多次插入???请参阅下面的answer
  • 这种方法的缺点是每次循环都在执行命令。这意味着对于 n 行,您将调用 .ExecuteNonQuery n 次。这对于性能来说是可怕。至少,使用VALUES 子句将 5 行参数化为一批。见鬼,即使生成一个包含 5 个插入的批处理也比执行 5 个单独的 INSERT 语句要好。
【解决方案3】:

真正可怕的方法是将每个INSERT语句作为自己的批处理执行:

第 1 批:

INSERT INTO Entries (id, name) VALUES (1, 'Ian Boyd);

第 2 批:

INSERT INTO Entries (id, name) VALUES (2, 'Bottlenecked);

第 3 批:

INSERT INTO Entries (id, name) VALUES (3, 'Marek Grzenkowicz);

第 4 批:

INSERT INTO Entries (id, name) VALUES (4, 'Giorgi);

第 5 批:

INSERT INTO Entries (id, name) VALUES (5, 'AMissico);

注意:参数化、错误检查和任何其他出于说明目的而省略的挑剔。

这是真正、可怕、可怕的做事方式。它提供了真正糟糕的性能,因为您每次都会受到网络往返时间的影响。

一个更好的解决方案是将所有INSERT 语句批处理为一个批处理:

第 1 批:

INSERT INTO Entries (id, name) VALUES (1, 'Ian Boyd');
INSERT INTO Entries (id, name) VALUES (2, 'Bottlenecked');
INSERT INTO Entries (id, name) VALUES (3, 'Marek Grzenkowicz');
INSERT INTO Entries (id, name) VALUES (4, 'Giorgi');
INSERT INTO Entries (id, name) VALUES (5, 'AMissico');

这样你只需要往返一次。此版本具有巨大的性能优势;大约快 5 倍。

更好的是使用VALUES 子句:

INSERT INTO Entries (id, name)
VALUES 
(1, 'Ian Boyd'),
(2, 'Bottlenecked'),
(3, 'Marek Grzenkowicz'),
(4, 'Giorgi'),
(5, 'AMissico');

与 5 个单独的 INSERTs 版本相比,这为您提供了一些性能改进;它让服务器做它擅长的事情:在 sets 上操作:

  • 每个触发器只需操作一次
  • 外键检查一次
  • 唯一约束检查​​一次

SQL Sever 喜欢对 集合 数据进行操作;这是维京人的地方!

参数限制

为清楚起见,上述 T-SQL 示例删除了所有参数化内容。但实际上你想参数化查询

  • 对于节省服务器必须编译每个 T-SQL 批处理的性能奖励并没有那么多(尽管在高速批量导入期间,节省解析时间确实可以加起来。)
  • 与其说是避免 SQL 注入,不如说是避免 SQL 注入;因为你已经是使用QuotedString(firstName) 的优秀开发者了
  • 但为了避免在数千兆字节的临时查询计划上淹没服务器的查询计划缓存。 (我见过 SQL Server 的工作集,即 RAM,是 2 GB 的未参数化 SQL 查询计划)

但布鲁诺有一个重要的观点; SQL Server 的驱动程序只允许您在批处理中包含 2,100 个参数。上面的查询有两个值:

@id, @name

如果您在单个批次中导入 1,051 行,即 2,102 个参数 - 您将收到错误:

此 RPC 请求中提供的参数过多

这就是为什么我通常一次插入 5 或 10 行。每批添加更多行并不会提高性能 - 收益会递减。

它将参数数量保持在较低水平,不会接近 T-SQL 批处理大小限制。还有一个事实是,VALUES 子句无论如何都限制为 1000 个元组。

实现它

您的第一种方法很好,但您确实存在以下问题:

  • 参数名称冲突
  • 行数不限(可能达到 2100 参数限制)

所以目标是生成一个字符串如:

INSERT INTO Entries (id, name) VALUES
(@p1, @p2),
(@p3, @p4),
(@p5, @p6),
(@p7, @p8),
(@p9, @p10)

我会在我的裤子座位上更改你的代码

IEnumerable<Entry> entries = GetStuffToInsert();

SqlCommand cmd = new SqlCommand();
StringBuilder sql = new StringBuilder();
Int32 batchSize = 0; //how many rows we have build up so far
Int32 p = 1; //the current paramter name (i.e. "@p1") we're going to use

foreach(var entry in entries)
{
   //Build the names of the parameters
   String pId =   String.Format("@p{0}", p);   //the "Id" parameter name (i.e. "p1")
   String pName = String.Format("@p{0}", p+1); //the "Name" parameter name (i.e. "p2")
   p += 2;

   //Build a single "(p1, p2)" row
   String row = String.Format("({0}, {1})", pId, pName); //a single values tuple

   //Add the row to our running SQL batch
   if (batchSize > 0)
      sb.AppendLine(",");
   sb.Append(row);
   batchSize += 1;

   //Add the parameter values for this row
   cmd.Parameters.Add(pID,   System.Data.SqlDbType.Int   ).Value = entry.Id;
   cmd.Parameters.Add(pName, System.Data.SqlDbType.String).Value = entry.Name;

   if (batchSize >= 5)
   {
       String sql = "INSERT INTO Entries (id, name) VALUES"+"\r\n"+
                    sb.ToString();
       cmd.CommandText = sql;
       cmd.ExecuteNonQuery();
       cmd.Parameters.Clear();
       sb.Clear();
       batchSize = 0;
       p = 1;
   }
}

//handle the last few stragglers
if (batchSize > 0)
{
    String sql = "INSERT INTO Entries (id, name) VALUES"+"\r\n"+
                 sb.ToString();
    cmd.CommandText = sql;
    cmd.ExecuteNonQuery();
}

【讨论】:

  • 嘿,非常感谢您不厌其烦地回答这么老的问题!从那以后,我变得有经验,批处理也是我的第一直觉。另外,您对批量限制项目的建议也很适合我从我们的 DBA 那里得到的回击:他们坚持发送尽可能少的语句以避免更长的交易时间,并为所有阅读的客户提供更公平的时间份额/在相同的表和行上书写。不真的知道有多少水(不太熟悉 SQL 内部),但听起来是个好建议,所以我遵循它:)
  • @bottlenecked 如果您一次插入几千行,这不会是一个很长的事务。但是如果你 i) 开始一个事务,ii) 插入或更新几百万行,花费 6 或 7 个小时来完成,那就太糟糕了。您不仅会(可能)锁定其他用户(由于阻塞),而且您将拥有一个事务日志,该日志必须不断增长以适应所有内容。但通常你应该写这样的事情的方式是导入到 staging 表中,在那里进行所有数据按摩,一旦一切准备就绪:将 MERGE 合并到读取表中。
  • 这对我来说是正确的解决方案。谢谢你写下来!虽然一旦我开始将 ado.net 编写到这种复杂程度,我的目光就会开始转向 dapper
  • 很好的解释!!只是按预期工作。
【解决方案4】:

跟进@Tim Mahy - 有两种可能的方式来提供 SqlBulkCopy:DataReader 或通过 DataTable。这里是DataTable的代码:

DataTable dt = new DataTable();
dt.Columns.Add(new DataColumn("Id", typeof(string)));
dt.Columns.Add(new DataColumn("Name", typeof(string)));
foreach (Entry entry in entries)
    dt.Rows.Add(new string[] { entry.Id, entry.Name });

using (SqlBulkCopy bc = new SqlBulkCopy(connection))
{   // the following 3 lines might not be neccessary
    bc.DestinationTableName = "Entries";
    bc.ColumnMappings.Add("Id", "Id");
    bc.ColumnMappings.Add("Name", "Name");

    bc.WriteToServer(dt);
}

【讨论】:

    【解决方案5】:

    您应该在每个循环上执行命令,而不是构建一个巨大的命令文本(顺便说一句,StringBuilder 是为此而制作的) 底层连接不会为每个循环关闭和重新打开,让连接池管理器处理这个问题。查看此链接以获取更多信息:Tuning Up ADO.NET Connection Pooling in ASP.NET Applications

    如果您想确保每个命令都成功执行,您可以使用Transaction 并在需要时回滚,

    【讨论】:

    • 或者最好使用Linq2Sql,让Linq2Sql来处理。
    • SQLCommand 针对 Giorgi 描述的过程进行了优化。正如蒂姆指出的那样,底层的连接将被保持。我还会使用 Tim 推荐的 Transaction
    【解决方案6】:

    如果条目很多,请考虑使用SqlBulkCopy。性能比一系列单次插入要快得多。

    【讨论】:

    • 什么是“很多”?
    【解决方案7】:

    如果创建正确,可以直接插入DataTable

    首先确保访问表列具有相同的列名和相似的类型。然后你就可以使用这个我认为非常快速和优雅的功能了。

    public void AccessBulkCopy(DataTable table)
    {
        foreach (DataRow r in table.Rows)
            r.SetAdded();
    
        var myAdapter = new OleDbDataAdapter("SELECT * FROM " + table.TableName, _myAccessConn);
    
        var cbr = new OleDbCommandBuilder(myAdapter);
        cbr.QuotePrefix = "[";
        cbr.QuoteSuffix = "]";
        cbr.GetInsertCommand(true);
    
        myAdapter.Update(table);
    }
    

    【讨论】:

      【解决方案8】:

      只需格式化查询字符串以添加要插入的所有值集。

      类似的东西-

      for (int i = 0; i < nimbusUserIds.Count; i++)
                  {
                      parameterValues[i] =
                          $"(0, 0, SYSDATETIME(),0, SYSDATETIME(), SYSDATETIME(),SYSDATETIME(), '{nimbusUserIds[i]}')";
                  }
      
                  string query =
                      string.Format(@"INSERT INTO [dbo].[NimbusUserEmailInviteStatus] 
      ([AdRegistrationStatus],
      [EmailSentStatus],
      [EmailSentDateTime],
      [InvitationStatus],
      [InvitationAcceptedDateTime],
      [CreatedDateTime],
      [UpdatedDateTime],
      [NimbusUserId]) VALUES {0}", string.Join(", ", parameterValues));
      

      【讨论】:

        【解决方案9】:

        使用单次插入插入多条记录的存储过程:

        ALTER PROCEDURE [dbo].[Ins]
        @i varchar(50),
        @n varchar(50),
        @a varchar(50),
        @i1 varchar(50),
        @n1 varchar(50),
        @a1 varchar(50),
        @i2 varchar(50),
        @n2 varchar(50),
        @a2 varchar(50) 
        AS
        INSERT INTO t1
        SELECT     @i AS Expr1, @i1 AS Expr2, @i2 AS Expr3
        UNION ALL
        SELECT     @n AS Expr1, @n1 AS Expr2, @n2 AS Expr3
        UNION ALL
        SELECT     @a AS Expr1, @a1 AS Expr2, @a2 AS Expr3
        RETURN
        

        后面的代码:

        protected void Button1_Click(object sender, EventArgs e)
        {
            cn.Open();
            SqlCommand cmd = new SqlCommand("Ins",cn);
            cmd.CommandType = CommandType.StoredProcedure;
            cmd.Parameters.AddWithValue("@i",TextBox1.Text);
            cmd.Parameters.AddWithValue("@n",TextBox2.Text);
            cmd.Parameters.AddWithValue("@a",TextBox3.Text);
            cmd.Parameters.AddWithValue("@i1",TextBox4.Text);
            cmd.Parameters.AddWithValue("@n1",TextBox5.Text);
            cmd.Parameters.AddWithValue("@a1",TextBox6.Text);
            cmd.Parameters.AddWithValue("@i2",TextBox7.Text);
            cmd.Parameters.AddWithValue("@n2",TextBox8.Text);
            cmd.Parameters.AddWithValue("@a2",TextBox9.Text);
            cmd.ExecuteNonQuery();
            cn.Close();
            Response.Write("inserted");
            clear();
        }
        

        【讨论】:

        • 如果我有 200 万个条目怎么办?
        【解决方案10】:
        ClsConectaBanco bd = new ClsConectaBanco();
        
        StringBuilder sb = new StringBuilder();
        sb.Append("  INSERT INTO FAT_BALANCETE ");
        sb.Append(" ([DT_LANCAMENTO]           ");
        sb.Append(" ,[ID_LANCAMENTO_CONTABIL]  ");
        sb.Append(" ,[NR_DOC_CONTABIL]         ");
        sb.Append(" ,[TP_LANCAMENTO_GERADO]    ");
        sb.Append(" ,[VL_LANCAMENTO]           ");
        sb.Append(" ,[TP_NATUREZA]             ");
        sb.Append(" ,[CD_EMPRESA]              ");
        sb.Append(" ,[CD_FILIAL]               ");
        sb.Append(" ,[CD_CONTA_CONTABIL]       ");
        sb.Append(" ,[DS_CONTA_CONTABIL]       ");
        sb.Append(" ,[ID_CONTA_CONTABIL]       ");
        sb.Append(" ,[DS_TRIMESTRE]            ");
        sb.Append(" ,[DS_SEMESTRE]             ");
        sb.Append(" ,[NR_TRIMESTRE]            ");
        sb.Append(" ,[NR_SEMESTRE]             ");
        sb.Append(" ,[NR_ANO]                  ");
        sb.Append(" ,[NR_MES]                  ");
        sb.Append(" ,[NM_FILIAL])              ");
        sb.Append(" VALUES                     ");
        sb.Append(" (@DT_LANCAMENTO            ");
        sb.Append(" ,@ID_LANCAMENTO_CONTABIL   ");
        sb.Append(" ,@NR_DOC_CONTABIL          ");
        sb.Append(" ,@TP_LANCAMENTO_GERADO     ");
        sb.Append(" ,@VL_LANCAMENTO            ");
        sb.Append(" ,@TP_NATUREZA              ");
        sb.Append(" ,@CD_EMPRESA               ");
        sb.Append(" ,@CD_FILIAL                ");
        sb.Append(" ,@CD_CONTA_CONTABIL        ");
        sb.Append(" ,@DS_CONTA_CONTABIL        ");
        sb.Append(" ,@ID_CONTA_CONTABIL        ");
        sb.Append(" ,@DS_TRIMESTRE             ");
        sb.Append(" ,@DS_SEMESTRE              ");
        sb.Append(" ,@NR_TRIMESTRE             ");
        sb.Append(" ,@NR_SEMESTRE              ");
        sb.Append(" ,@NR_ANO                   ");
        sb.Append(" ,@NR_MES                   ");
        sb.Append(" ,@NM_FILIAL)               ");
        
        SqlCommand cmd = new SqlCommand(sb.ToString(), bd.CriaConexaoSQL());
        bd.AbrirConexao();
        
        cmd.Parameters.Add("@DT_LANCAMENTO", SqlDbType.Date);
        cmd.Parameters.Add("@ID_LANCAMENTO_CONTABIL", SqlDbType.Int);
        cmd.Parameters.Add("@NR_DOC_CONTABIL", SqlDbType.VarChar,255);
        cmd.Parameters.Add("@TP_LANCAMENTO_GERADO", SqlDbType.VarChar,255);
        cmd.Parameters.Add("@VL_LANCAMENTO", SqlDbType.Decimal);
        cmd.Parameters["@VL_LANCAMENTO"].Precision = 15;
        cmd.Parameters["@VL_LANCAMENTO"].Scale = 2;
        cmd.Parameters.Add("@TP_NATUREZA", SqlDbType.VarChar, 1);
        cmd.Parameters.Add("@CD_EMPRESA",SqlDbType.Int);
        cmd.Parameters.Add("@CD_FILIAL", SqlDbType.Int);
        cmd.Parameters.Add("@CD_CONTA_CONTABIL", SqlDbType.VarChar, 255);
        cmd.Parameters.Add("@DS_CONTA_CONTABIL", SqlDbType.VarChar, 255);
        cmd.Parameters.Add("@ID_CONTA_CONTABIL", SqlDbType.VarChar,50);
        cmd.Parameters.Add("@DS_TRIMESTRE", SqlDbType.VarChar, 4);
        cmd.Parameters.Add("@DS_SEMESTRE", SqlDbType.VarChar, 4);
        cmd.Parameters.Add("@NR_TRIMESTRE", SqlDbType.Int);
        cmd.Parameters.Add("@NR_SEMESTRE", SqlDbType.Int);
        cmd.Parameters.Add("@NR_ANO", SqlDbType.Int);
        cmd.Parameters.Add("@NR_MES", SqlDbType.Int);
        cmd.Parameters.Add("@NM_FILIAL", SqlDbType.VarChar, 255);
        cmd.Prepare();
        
         foreach (dtoVisaoBenner obj in lista)
         {
             cmd.Parameters["@DT_LANCAMENTO"].Value = obj.CTLDATA;
             cmd.Parameters["@ID_LANCAMENTO_CONTABIL"].Value = obj.CTLHANDLE.ToString();
             cmd.Parameters["@NR_DOC_CONTABIL"].Value = obj.CTLDOCTO.ToString();
             cmd.Parameters["@TP_LANCAMENTO_GERADO"].Value = obj.LANCAMENTOGERADO;
             cmd.Parameters["@VL_LANCAMENTO"].Value = obj.CTLANVALORF;
             cmd.Parameters["@TP_NATUREZA"].Value = obj.NATUREZA;
             cmd.Parameters["@CD_EMPRESA"].Value = obj.EMPRESA;
             cmd.Parameters["@CD_FILIAL"].Value = obj.FILIAL;
             cmd.Parameters["@CD_CONTA_CONTABIL"].Value = obj.CONTAHANDLE.ToString();
             cmd.Parameters["@DS_CONTA_CONTABIL"].Value = obj.CONTANOME.ToString();
             cmd.Parameters["@ID_CONTA_CONTABIL"].Value = obj.CONTA;
             cmd.Parameters["@DS_TRIMESTRE"].Value = obj.TRIMESTRE;
             cmd.Parameters["@DS_SEMESTRE"].Value = obj.SEMESTRE;
             cmd.Parameters["@NR_TRIMESTRE"].Value = obj.NRTRIMESTRE;
             cmd.Parameters["@NR_SEMESTRE"].Value = obj.NRSEMESTRE;
             cmd.Parameters["@NR_ANO"].Value = obj.NRANO;
             cmd.Parameters["@NR_MES"].Value = obj.NRMES;
             cmd.Parameters["@NM_FILIAL"].Value = obj.NOME;
             cmd.ExecuteNonQuery();
             rowAffected++;
         }
        

        【讨论】:

        • 毕加索会很自豪
        • 你被录用了!!
        猜你喜欢
        • 1970-01-01
        • 2015-10-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-12-16
        • 2015-05-10
        • 1970-01-01
        • 2016-12-31
        相关资源
        最近更新 更多