在不锁定表的情况下插入大量记录

Posted

技术标签:

【中文标题】在不锁定表的情况下插入大量记录【英文标题】:Inserting large number of records without locking the table 【发布时间】:2017-01-17 05:01:10 【问题描述】:

我正在尝试将 1,500,000 条记录插入到表中。我在插入过程中面临表锁定问题。所以我想出了下面的批量插入。

DECLARE @BatchSize INT = 50000

WHILE 1 = 1
  BEGIN
      INSERT INTO [dbo].[Destination] 
                  (proj_details_sid,
                   period_sid,
                   sales,
                   units)
      SELECT TOP(@BatchSize) s.proj_details_sid,
                             s.period_sid,
                             s.sales,
                             s.units
      FROM   [dbo].[SOURCE] s
      WHERE  NOT EXISTS (SELECT 1
                         FROM   dbo.Destination d
                         WHERE  d.proj_details_sid = s.proj_details_sid
                                AND d.period_sid = s.period_sid)

      IF @@ROWCOUNT < @BatchSize
        BREAK
  END 

我在Destination(proj_details_sid ,period_sid ) 上有一个聚集索引。 NOT EXISTS 部分只是为了限制插入的记录再次插入到表中

我做得对吗,这会避免表锁吗?或者有没有更好的方法。

注意:所用时间与批处理和不批处理插入大致相同

【问题讨论】:

在exists子句中添加with(nolock)以避免等待锁 @AksheyBhat 添加 NOLOCK 会由于脏读导致任何问题 @AksheyBhat 很好奇,SQL 会将数据插入到Destination 表中,而nolock 不会在exists clause 中生效,不是吗? @Alex with(nolock) 是一个表提示,它指定查询在执行前不应等待表上的锁。 SQL 会考虑表中所有已提交和未提交的数据。 在取出 5000 个锁后会尝试锁升级。我不知道为什么到目前为止的答案都集中在SELECT 部分。批处理中新插入的行取出的锁可能会导致任何锁升级问题,而不是SELECT 部分。在默认隔离级别,SELECT 无论如何都会在读取行锁后立即释放它。 【参考方案1】:

锁升级不太可能与您声明的SELECT 部分有关。

这是一个natural consequence of inserting a large number of rows

当未使用 ALTER TABLE SET LOCK_ESCALATION 选项对表禁用锁升级时,并且存在以下任一情况时,将触发锁升级:

单个 Transact-SQL 语句在单个非分区表或索引上获取至少 5,000 个锁。 单个 Transact-SQL 语句在分区表的单个分区上获取至少 5,000 个锁,并且 ALTER TABLE SET LOCK_ESCALATION 选项设置为 AUTO。 数据库引擎实例中的锁数超过内存或配置阈值。

如果由于锁冲突而无法升级锁,则数据库引擎会在每获得 1,250 个新锁时定期触发锁升级。

您可以通过跟踪 Profiler 中的锁升级事件或简单地尝试以下不同的批处理大小来轻松地亲自看到这一点。对我来说,TOP (6228) 显示持有 6250 个锁,但TOP (6229) 随着锁升级开始,它突然下降到 1。确切的数字可能会有所不同(取决于数据库设置和当前可用的资源)。使用试错法找到出现锁升级的阈值。

CREATE TABLE [dbo].[Destination]
  (
     proj_details_sid INT,
     period_sid       INT,
     sales            INT,
     units            INT
  )

BEGIN TRAN --So locks are held for us to count in the next statement
INSERT INTO [dbo].[Destination]
SELECT TOP (6229) 1,
                  1,
                  1,
                  1
FROM   master..spt_values v1,
       master..spt_values v2

SELECT COUNT(*)
FROM   sys.dm_tran_locks
WHERE  request_session_id = @@SPID;

COMMIT

DROP TABLE [dbo].[Destination] 

您正在插入 50,000 行,因此几乎可以肯定会尝试锁升级。

How to resolve blocking problems that are caused by lock escalation in SQL Server的文章已经很老了,但很多建议仍然有效。

    将大批量操作分解为几个较小的操作(即使用较小的批量) 如果不同的 SPID 当前持有不兼容的表锁,则不会发生锁升级 - 他们给出的示例是执行不同的会话

BEGIN TRAN
SELECT * FROM mytable (UPDLOCK, HOLDLOCK) WHERE 1=0
WAITFOR DELAY '1:00:00'
COMMIT TRAN 
    通过启用跟踪标志 1211 禁用锁升级 - 但是这是一个全局设置,可能会导致严重问题。有一个较新的选项 1224 问题较少,但仍然是全球性的。

另一个选项是ALTER TABLE blah SET (LOCK_ESCALATION = DISABLE),但这仍然不是很有针对性,因为它会影响针对表的所有查询,而不仅仅是您这里的单一场景。

所以我会选择选项 1 或可能的选项 2,并打折其他选项。

【讨论】:

【参考方案2】:

与其检查Destination中的数据是否存在,不如先将所有数据存储在temp表中,然后批量插入Destination

参考:Using ROWLOCK in an INSERT statement (SQL Server)

DECLARE @batch int = 100
DECLARE @curRecord int = 1
DECLARE @maxRecord int

-- remove (nolock) if you don't want to have dirty read
SELECT row_number over (order by s.proj_details_sid, s.period_sid) as rownum,
       s.proj_details_sid,
       s.period_sid,
       s.sales,
       s.units
INTO #Temp
FROM   [dbo].[SOURCE] s WITH (NOLOCK)
WHERE  NOT EXISTS (SELECT 1
                   FROM   dbo.Destination d WITH (NOLOCK)
                   WHERE  d.proj_details_sid = s.proj_details_sid
                          AND d.period_sid = s.period_sid)

-- change this maxRecord if you want to limit the records to insert
SELECT @maxRecord = count(1) from #Temp

WHILE @maxRecord >= @curRecord
   BEGIN
       INSERT INTO [dbo].[Destination] 
              (proj_details_sid,
               period_sid,
               sales,
               units)
       SELECT proj_details_sid, period_sid, sales, units
       FROM #Temp
       WHERE rownum >= @curRecord and rownum < @curRecord + @batch

       SET @curRecord = @curRecord + @batch
   END

DROP TABLE #Temp

【讨论】:

周一前试试 这个比我的方法快。【参考方案3】:

我添加了 (NOLOCK) 您的目标表 -> dbo.Destination(NOLOCK)。 现在,你不会锁定你的桌子。

WHILE 1 = 1
  BEGIN
      INSERT INTO [dbo].[Destination] 
                  (proj_details_sid,
                   period_sid,
                   sales,
                   units)
      SELECT TOP(@BatchSize) s.proj_details_sid,
                             s.period_sid,
                             s.sales,
                             s.units
      FROM   [dbo].[SOURCE] s
      WHERE  NOT EXISTS (SELECT 1
                         FROM   dbo.Destination(NOLOCK) d
                         WHERE  d.proj_details_sid = s.proj_details_sid
                                AND d.period_sid = s.period_sid)

      IF @@ROWCOUNT < @BatchSize
        BREAK
  END 

【讨论】:

添加NOLOCK会因为脏读导致任何问题? 如果你的程序不并行运行,脏读也没问题。 以上情况,NOLOCK不会造成任何问题。【参考方案4】:

为此,您可以在选择语句中使用 WITH (NOLOCK)。 但不建议在 OLTP 数据库上使用 NOLOCK。

【讨论】:

以上是关于在不锁定表的情况下插入大量记录的主要内容,如果未能解决你的问题,请参考以下文章

如何在不锁定表的情况下向 Postgres 中的 ENUM 添加新值?

如何在不锁定活动方向的情况下锁定片段方向?

在不锁定 UI 的情况下禁用滚动视图

在不锁定集合的情况下从通用集合中获取 Count 值是不是安全?

插入记录时锁定

如何在不丢失未保存工作的情况下锁定 Web 应用程序进行维护?