【问题标题】:Why is an insert of 1M records slower without a transaction than inside a transaction?为什么在没有事务的情况下插入 1M 记录比在事务中要慢?
【发布时间】:2009-06-25 10:01:27
【问题描述】:

我正在使用 .Net 3.5 针对 SQL Server 进行一些性能测试。我正在插入 100 万条记录。当我将它包装在一个事务(可序列化、RepeatabelRead 或 ReadUncommited)中时,它在我的系统上运行时间不到 80 秒。当我删除事务时,它会在大约 300 秒内运行。我希望不使用事务将是向数据库中插入行的最快方法,因为 DBMS 不需要考虑潜在的回滚。这里会发生什么?这对于 SQL Server、SQL Server ADO.Net 提供程序、一般的 ADO.Net、一般的 DBMS 来说是典型的吗?

我有 iSeries/DB2 数据库方面的背景。在 DB2 中,您必须先启用日志,然后才能获得提交控制和事务,而且日志相对昂贵。

我真正想做的是比较 SqlCommand 插入与 Entity Framework 插入,但我对这些结果感到非常惊讶,所以我想先看看这里发生了什么。

在我用来运行测试的代码下方。当我运行下面的代码时,大约需要 74 秒(在 AtStart 日志和 AtEnd 日志行之间测量)

using (SqlConnection sqlConnection = new SqlConnection(connectionString))
{
    sqlConnection.Open();
    SqlCommand deleteCommand = new SqlCommand("DELETE FROM LockTest");
    deleteCommand.Connection = sqlConnection;
    deleteCommand.ExecuteNonQuery();

    using (SqlTransaction transaction = sqlConnection.BeginTransaction(System.Data.IsolationLevel.Serializable))
    {
        try
        {
            if (DEBUG) LOG.Debug("AtStart");

            SqlCommand insertCommand = new SqlCommand();
            insertCommand.Connection = sqlConnection;
            insertCommand.Transaction = transaction;

            insertCommand.CommandText = "INSERT INTO LockTest (Id, Name, Description, Type) "  + 
                "VALUES (@id, @name, @description, @type)";
            SqlParameter idParameter = new SqlParameter("@id", System.Data.SqlDbType.UniqueIdentifier);
            insertCommand.Parameters.Add(idParameter);
            SqlParameter nameParameter = new SqlParameter("@name", System.Data.SqlDbType.NVarChar, 50);
            insertCommand.Parameters.Add(nameParameter);
            SqlParameter descriptionParameter = new SqlParameter("@description", System.Data.SqlDbType.NVarChar, Int32.MaxValue);
            insertCommand.Parameters.Add(descriptionParameter);
            SqlParameter typeParameter = new SqlParameter("@type", System.Data.SqlDbType.NChar, 20);
            insertCommand.Parameters.Add(typeParameter);

            insertCommand.Prepare();

            for (int i= 0; i < 1000000; i++)
            {
                Guid g = Guid.NewGuid();
                string s = g.ToString();
                insertCommand.Parameters["@id"].Value = g;
                insertCommand.Parameters["@name"].Value = s;
                insertCommand.Parameters["@description"].Value = DateTime.UtcNow.Ticks.ToString();
                insertCommand.Parameters["@type"].Value = "test";
                insertCommand.ExecuteNonQuery();
            }
            transaction.Commit();
        }
        catch
        {
            transaction.Rollback();
            throw;
        }

    }
    sqlConnection.Close();
}
if (DEBUG) LOG.Debug("AtEnd");

【问题讨论】:

  • 根据定义,事务隔离级别只影响读取。写入(即 INSERTS)在所有隔离级别上的行为都相同。
  • 每次运行之间是否清除表?每次都一致?
  • 您可以使用 TRUNCATE TABLE 而不是 DELETE FROM 来准备测试。为了完全准确,您应该每次都从头开始创建数据库,确保预先增长数据和日志文件(分配足够大的初始大小以进行测试)。在其中一次运行期间的单个数据库或日志增长事件将丢弃该运行的所有结果。
  • @Remus:如果我发现运行之间的差异相对较小,你是对的,我现在发现的是一个数量级的差异。

标签: .net sql-server database transactions


【解决方案1】:

日志刷新。

在没有显式事务的情况下,每个语句(即 INSERT)启动的隐式事务必须提交。 commit不能返回,直到日志中的数据写入磁盘,这意味着每个INSERT语句都必须等待日志磁盘写操作。

显式事务只能在发出 COMMIT 语句时等待,到那时每个完整的日志页面都已提交,并且最后一个日志页面可能包含多个 INSERT,因此写入成本被摊销。

更新:

您可以在性能计数器中验证日志刷新时间:http://msdn.microsoft.com/en-us/library/ms189883.aspx

  • 日志刷新等待时间 刷新日志的总等待时间(以毫秒为单位)。
  • Log Flush Waits/sec 每秒等待日志刷新的提交次数。
  • Log Flushes/sec 每秒的日志刷新次数。

【讨论】:

  • 感谢到目前为止的信息:我的问题的第二部分怎么样。这是 SQL Server 的典型情况还是其他 DBMS 的行为是否相同,例如MySQL、甲骨文?
  • 这是典型的所有基于预写日志的数据库 (en.wikipedia.org/wiki/Write_ahead_logging)。 MySQL 与 InnoDB 引擎和 Oracle 的行为也相同(在 Oracle 中可能有各种旋钮来控制它,我绝不是 Oracle 专家)。 WAL 的替代方案是版本分页 (en.wikipedia.org/wiki/Shadow_paging),但我知道部署的唯一商业数据库是 Informix。不使用 WAL 或版本控制页面的系统不提供 ACID,因此它们通常不提供事务(例如 MySQL 的 ISAM 引擎)。
【解决方案2】:

因为每个命令(如果未明确设置事务)都隐式地包装了事务,即您有 1M 事务。至少对于 sqLite

【讨论】:

    【解决方案3】:

    如果您不是事务性的,它必须在每次插入时获取并释放锁。通过事务,它可以为多个插入保持打开锁。更少的开销。

    【讨论】:

    • 两种情况下获取/释放的锁数相同。
    • 这不取决于获取的锁的粒度吗?
    • @Remus:可能不会。它会将锁升级到页面/表。
    • @gbn true 但这是一个“特殊情况”,可能会或可能不会实际发生(可能会在 1M 时发生)。所需的纯“理论”锁是相同的(插入记录的键锁)。根据我的经验,日志刷新的成本是批处理事务中性能差异的压倒性因素。与日志页面 I/O 等待时间相比,锁定获取时间极短(不到 100 个 CPU 周期)。
    • @gbn:不。 msdn.microsoft.com/en-us/library/ms184286(SQL.90).aspx: 当 Transact-SQL 语句在表或索引的单个引用上获取至少 5,000 个锁时触发锁升级,或者,如果表已分区,则在表分区或索引分区的单个引用上获取锁升级. 必须在单个statement 和单个partition reference 中获取锁。 +5k insert ... values ... 语句不会触发它。一条语句 insert ... select ... +5k 插入将触发它。
    【解决方案4】:

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-06-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-09-13
      • 2018-08-21
      相关资源
      最近更新 更多