如何将缓慢的参数化插入更改为快速批量复制(甚至从内存中)

时间:2020-03-06 14:39:43  来源:igfitidea点击:

我的代码中有这样的内容(.Net 2.0,MS SQL)

SqlConnection connection = new SqlConnection(@"Data Source=localhost;Initial
Catalog=DataBase;Integrated Security=True");
  connection.Open();

  SqlCommand cmdInsert = connection.CreateCommand();
  SqlTransaction sqlTran = connection.BeginTransaction();
  cmdInsert.Transaction = sqlTran;

  cmdInsert.CommandText =
     @"INSERT INTO MyDestinationTable" +
      "(Year, Month, Day, Hour,  ...) " +
      "VALUES " +
      "(@Year, @Month, @Day, @Hour, ...) ";

  cmdInsert.Parameters.Add("@Year", SqlDbType.SmallInt);
  cmdInsert.Parameters.Add("@Month", SqlDbType.TinyInt);
  cmdInsert.Parameters.Add("@Day", SqlDbType.TinyInt);
  // more fields here
  cmdInsert.Prepare();

  Stream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read);

  StreamReader reader = new StreamReader(stream);
  char[] delimeter = new char[] {' '};
  String[] records;
  while (!reader.EndOfStream)
  {
    records = reader.ReadLine().Split(delimeter, StringSplitOptions.None);

    cmdInsert.Parameters["@Year"].Value = Int32.Parse(records[0].Substring(0, 4));
    cmdInsert.Parameters["@Month"].Value = Int32.Parse(records[0].Substring(5, 2));
    cmdInsert.Parameters["@Day"].Value = Int32.Parse(records[0].Substring(8, 2));
    // more here complicated stuff here
    cmdInsert.ExecuteNonQuery()
  }
  sqlTran.Commit();
  connection.Close();

注释掉cmdInsert.ExecuteNonQuery()后,此代码将在不到2秒的时间内执行。使用SQL执行需要1m 20秒。大约有50万条记录。表已清空。具有类似功能的SSIS数据流任务大约需要20秒钟。

  • 不能使用批量插入(请参见下文)。我在导入过程中做了一些花哨的东西。
  • 我的测试机器是具有2 GB RAM的Core 2 Duo。
  • 在任务管理器中查找时,CPU尚未完全完成。 IO似乎也没有得到充分利用。
  • 模式很简单,就像地狱一样:一个表,其中AutoInt作为主索引,并且少于10个int,tiny int和chars(10)。

经过一些回答后,我发现可以从内存中执行批量复制!我拒绝使用批量复制,因为我认为必须从文件完成...

现在,我使用它,大约需要20秒(如SSIS任务)

DataTable dataTable = new DataTable();

  dataTable.Columns.Add(new DataColumn("ixMyIndex", System.Type.GetType("System.Int32")));   
  dataTable.Columns.Add(new DataColumn("Year", System.Type.GetType("System.Int32")));   
  dataTable.Columns.Add(new DataColumn("Month", System.Type.GetType("System.Int32")));
  dataTable.Columns.Add(new DataColumn("Day", System.Type.GetType("System.Int32")));
 // ... and more to go

  DataRow dataRow;
  object[] objectRow = new object[dataTable.Columns.Count];

  Stream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read);

  StreamReader reader = new StreamReader(stream);
  char[] delimeter = new char[] { ' ' };
  String[] records;
  int recordCount = 0;
  while (!reader.EndOfStream)
  {
    records = reader.ReadLine().Split(delimeter, StringSplitOptions.None);

    dataRow = dataTable.NewRow();
    objectRow[0] = null; 
    objectRow[1] = Int32.Parse(records[0].Substring(0, 4));
    objectRow[2] = Int32.Parse(records[0].Substring(5, 2));
    objectRow[3] = Int32.Parse(records[0].Substring(8, 2));
    // my fancy stuf goes here

    dataRow.ItemArray = objectRow;         
    dataTable.Rows.Add(dataRow);

    recordCount++;
  }

  SqlBulkCopy bulkTask = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, null);
  bulkTask.DestinationTableName = "MyDestinationTable"; 
  bulkTask.BatchSize = dataTable.Rows.Count;
  bulkTask.WriteToServer(dataTable);
  bulkTask.Close();

解决方案

需要交易吗?使用事务比简单的命令需要更多的资源。

另外,如果确定插入的值是corect,则可以使用BulkInsert。

1分钟听起来很合理,可以记录50万条记录。这是每0.00012秒一次的记录。

该表有索引吗?如果可以的话,将其删除并在大容量插入之后重新应用它们可以提高插入的性能。

对我而言,每秒处理8,333条记录似乎不是不合理的……我们期望什么样的吞吐量?

如果需要更高的速度,则可以考虑实现批量插入:

http://msdn.microsoft.com/en-us/library/ms188365.aspx

我假设大约需要58秒的时间是物理插入500,000条记录,因此我们每秒获得大约10,000次插入。在不了解数据库服务器机器规格的情况下(我看到我们使用的是localhost,因此网络延迟不应该成为问题),很难说这是好是坏。

我看一下数据库架构,表上是否有一堆索引,每次插入后都必须更新?这可能来自具有外键的其他表,这些表引用了我们正在处理的表。 SQL Server中内置有SQL分析工具和性能监视工具,但我从未使用过它们。但是它们可能会出现诸如锁之类的问题。

首先在所有记录上对数据进行处理。然后将它们批量插入。

(因为插入后我们没有执行选择操作。。我看不到在BulkInsert之前对数据应用所有操作的问题

如果我不得不猜测,我会寻找的第一件事是tbTrafficLogTTL表上的索引太多或者类型错误。如果不查看表的架构定义,我无法真正说出,但是在以下情况下,我遇到了类似的性能问题:

  • 主键是GUID,主索引是CLUSTERED。
  • 在一组字段上存在某种UNIQUE索引。
  • 表上的索引太多。

当开始为五十万行数据建立索引时,创建和维护索引所花费的时间加起来。

我还将注意到,如果我们可以将"年","月","天","小时","分钟","秒"字段转换为单个datetime2或者"时间戳"字段,则应该这样做。毫无疑问,我们为数据架构增加了很多复杂性。我什至打算使用这样的拆分字段结构的唯一原因是,如果我们要处理的是由于任何原因都无法更改的现有数据库模式。在这种情况下,成为我们真是太糟糕了。

我在上一份合同中遇到了类似的问题。我们要进行500,000次SQL插入记录。为了显着提高性能,我们想研究SQL名称空间中的BulkInsert方法。实现批量导入后,我的"重新加载"过程从2个多小时开始,将十几个表还原到31秒。

最好使用bcp命令之类的方法来完成。如果不可用,那么上面关于使用BULK INSERT的建议是最好的选择。我们要往返数据库500,000次,并将500,000个条目写入日志文件,更不用说需要分配给日志文件,表和索引的任何空间。

如果要插入的顺序与聚簇索引不同,则还必须处理重新组织磁盘上的物理数据所需的时间。这里有很多变量可能会使查询的运行速度比我们希望的慢。

对于来自代码/

而不是单独插入每个记录,请尝试使用SqlBulkCopy类一次批量插入所有记录。

创建一个DataTable并将所有记录添加到DataTable,然后使用SqlBulkCopy.WriteToServer一次批量插入所有数据。

如果某种形式的批量插入不是一种选择,那么另一种方式将是多个线程,每个线程都有自己的数据库连接。

当前系统的问题是我们有500,000次往返数据库的往返,并在开始下一次任何延迟(例如,机器之间的网络)之前等待第一次往返完成,这意味着大多数人花时间等待。

如果我们可以拆分工作(也许使用某种形式的生产者/消费者设置),则可能会发现可以充分利用所有资源。

但是,要做到这一点,我们将不得不丢失一个重要的事务,否则第一个编写器线程将阻塞所有其他事务,直到其事务完成为止。我们仍然可以使用事务,但是必须使用很多小事务,而不是1个大事务。

SSIS将很快,因为它使用批量插入方法首先执行所有复杂的处理,生成要插入的最终数据列表,然后将所有数据同时批量插入。

BULK INSERT =来自权限的bcp

我们可以批处理INSERT,以减少往返次数
SQLDataAdaptor.UpdateBatchSize = 10000进行50次往返

我们仍然有50万个插入内容...

文章

MSDN