【问题标题】:DELETE SQL with correlated subquery for table with 42 million rows?删除具有 4200 万行表的相关子查询的 SQL?
【发布时间】:2010-08-06 22:52:09
【问题描述】:

我有一张表 cats,有 42,795,120 行。

显然这是很多行。所以当我这样做时:

/* owner_cats is a many-to-many join table */
DELETE FROM cats
WHERE cats.id_cat IN (
SELECT owner_cats.id_cat FROM owner_cats
WHERE owner_cats.id_owner = 1)

查询超时:(

(编辑:我需要增加我的CommandTimeout值,默认只有30秒)

我不能使用TRUNCATE TABLE cats,因为我不想从其他主人那里吹走猫。

我正在使用 SQL Server 2005,并将“恢复模型”设置为“简单”。

所以,我想过做这样的事情(顺便说一句,从应用程序执行此 SQL):

DELETE TOP (25) PERCENT FROM cats
WHERE cats.id_cat IN (
SELECT owner_cats.id_cat FROM owner_cats
WHERE owner_cats.id_owner = 1)

DELETE TOP(50) PERCENT FROM cats
WHERE cats.id_cat IN (
SELECT owner_cats.id_cat FROM owner_cats
WHERE owner_cats.id_owner = 1)

DELETE FROM cats
WHERE cats.id_cat IN (
SELECT owner_cats.id_cat FROM owner_cats
WHERE owner_cats.id_owner = 1)

我的问题是:SQL Server 2005 中我可以DELETE 的行数阈值是多少?

或者,如果我的方法不是最优的,请提出更好的方法。谢谢。

这篇文章对我的帮助不够:

编辑(2010 年 8 月 6 日):

好的,我在再次阅读上面的链接后才意识到我在这些表上没有索引。此外,你们中的一些人已经在下面的 cmets 中指出了这个问题。请记住,这是一个虚构的架构,所以即使 id_cat 也不是 PK,因为在我的真实架构中,它不是一个唯一字段。

我会把索引放在:

  1. cats.id_cat
  2. owner_cats.id_cat
  3. owner_cats.id_owner

我想我仍然掌握这个数据仓库的窍门,显然我需要所有 JOIN 字段的索引,对吧?

但是,我需要几个小时才能完成此批量加载过程。我已经以SqlBulkCopy 的身份进行操作(以块的形式,而不是一次全部 4200 万)。我有一些索引和 PK。我阅读了以下帖子,这些帖子证实了我的理论,即即使是批量复制,索引也会减慢速度:

所以我要在复制之前DROP我的索引,然后在完成后重新CREATE它们。

由于加载时间较长,我需要一段时间来测试这些建议。我会报告结果。

更新(2010 年 8 月 7 日):

汤姆建议:

DELETE
FROM cats c
WHERE EXISTS (SELECT 1
FROM owner_cats o
WHERE o.id_cat = c.id_cat
AND o.id_owner = 1)

仍然没有索引,对于 4200 万行,它需要 13:21 分:秒,而上述方式需要 22:08。然而,对于 1300 万行,他用了 2:13 而不是我的老方法 2:10。这是个好主意,但我仍然需要使用索引!

更新(2010 年 8 月 8 日):

出了点大问题!现在打开索引,我上面的第一个删除查询花费了 1:9 hrs:min (是一个小时!),而 22:08 min:sec 和 13:21 min :sec 与 2:10 min:sec 分别用于 42 mil 行和 13 mil 行。我现在要尝试使用索引进行 Tom 的查询,但这是朝着错误的方向发展。请帮忙。

更新(2010 年 8 月 9 日):

Tom 的删除耗时 1:06 小时:分钟(42 百万行)和 10:50 分钟:秒(有索引的 13 百万行),而分别为 13:21 分钟:秒和 2:13 分钟:秒。 当我使用一个数量级的索引时,删除我的数据库所花费的时间更长!我想我知道为什么,我的数据库 .mdf 和 .ldf 从 3.5 GB 增长到 40.6 GB期间第一次(4200万)删除! 我做错了什么?

更新(2010 年 8 月 10 日):

由于没有任何其他选择,我想出了一个我觉得乏善可陈的解决方案(希望是暂时的)

  1. 将数据库连接超时时间增加到 1 小时(CommandTimeout=60000; 默认为 30 秒)
  2. 使用 Tom 的查询:DELETE FROM WHERE EXISTS (SELECT 1 ...),因为它的执行速度更快
  3. DROP 运行删除语句之前的所有索引和 PK (???)
  4. 运行DELETE 语句
  5. CREATE 所有索引和 PK

看起来很疯狂,但至少它比使用 TRUNCATE 并从第一个 owner_id 开始加载我的负载要快,因为我的一个 owner_id 需要 2:30 hrs:min 来加载,而 17: 22 min:sec 用于我刚刚描述的 42 百万行的删除过程。 (注意:如果我的加载过程抛出异常,我会重新开始那个owner_id,但我不想吹走之前的owner_id,所以我不想TRUNCATEowner_cats 表,这就是我尝试使用DELETE的原因。)

我们将不胜感激:)

【问题讨论】:

  • 你能解释一下你的表索引有什么吗?
  • 我不是讨厌猫的人,但这不是很多行,而是很多猫 :) 而且,这让我很伤心“我不想把猫从其他人身上吹走业主”
  • 这在 CrazyOldLady 数据库中吗?
  • owner_cats.id_catowner_cats.id_ownercats.id_cat 列上是否有索引? owner_cats.id_cat 是主键吗?
  • 当您选择一个所有者时,如何删除行?听起来只有几行。这使得这些索引非常重要。我怀疑你可以在几秒钟内删除一个主人的猫。

标签: sql sql-server sql-delete correlated-subquery


【解决方案1】:

没有实际的门槛。这取决于您在连接上设置的命令超时时间。

请记住,删除所有这些行所需的时间取决于:

  • 找到感兴趣的行所需的时间
  • 在事务日志中记录事务所需的时间
  • 删除感兴趣的索引条目所需的时间
  • 删除实际感兴趣的行所需的时间
  • 等待其他进程停止使用该表所需的时间,以便您获得在这种情况下很可能是排他表锁

最后一点通常可能是最重要的。在另一个查询窗口中执行 sp_who2 命令以确保没有发生锁争用,从而阻止您的命令执行。

配置不当的 SQL Server 在这种类型的查询中表现不佳。处理大行时,太小和/或与数据文件共享相同磁盘的事务日志通常会导致严重的性能损失。

至于解决方案,好吧,就像所有事情一样,这取决于。这是你打算经常做的事情吗?根据您剩下的行数,最快的方法可能是将表重建为另一个名称,然后重命名它并重新创建它的约束,所有这些都在一个事务中。如果这只是临时性的事情,请确保您的 ADO CommandTimeout 设置得足够高,并且您可以承担这个大删除的成本。

【讨论】:

  • 好吧,我应该对这个盒子有很多争论。我没有设置CommandTimeout,所以我想我使用的是默认值 30 秒。此外,.ldf 与 .mdf 共享相同的磁盘,但我可能可以更改它。这是一个批量加载过程,DELETE 仅在 Web 服务调用超时并且我需要为当前加载的所有者重新加载猫时才完成。
【解决方案2】:

如果删除会从表中删除“大量”行,这可以作为删除的替代方法:将记录放在其他地方,截断原始表,放回“管理员”。比如:

SELECT *
INTO #cats_to_keep
FROM cats
WHERE cats.id_cat NOT IN (    -- note the NOT
SELECT owner_cats.id_cat FROM owner_cats
WHERE owner_cats.id_owner = 1)

TRUNCATE TABLE cats

INSERT INTO cats
SELECT * FROM #cats_to_keep

【讨论】:

    【解决方案3】:

    您是否尝试过不使用子查询而使用连接?

    DELETE cats 
    FROM
     cats c
     INNER JOIN owner_cats oc
     on c.id_cat = oc.id_cat
    WHERE
       id_owner =1
    

    如果你也尝试过不同的加入提示,例如

    DELETE cats 
    FROM
     cats c
     INNER HASH JOIN owner_cats oc
     on c.id_cat = oc.id_cat
    WHERE
       id_owner =1
    

    【讨论】:

      【解决方案4】:

      如果您使用EXISTS 而不是IN,您应该会获得更好的性能。试试这个:

      DELETE
        FROM cats c
       WHERE EXISTS (SELECT 1
                       FROM owner_cats o
                      WHERE o.id_cat = c.id_cat
                        AND o.id_owner = 1)
      

      【讨论】:

      • +1 有帮助!有 4200 万行,仍然没有索引,我的老方法:22:8 min:sec。你的方式:13:21。但是,我的旧方式有 1300 万行(我有 2 个所有者):2:10。你的方式:2:13。很好的提示,你能解释一下它是如何工作的吗?
      • 这一切都与优化器真正处理事情有关,但基本上对于 IN 子句,必须完全评估子选择,而对于 EXISTS,只需要第一行。
      • 没有索引你总是会被塞在这里。至少你需要在 owner_cats.id_cat 上建立一个索引,那么这个 EXISTS 子句应该很快。
      • 它只需要一个小改动。外部表中不接受别名。
      【解决方案5】:

      没有这样的阈值 - 您可以删除任何表中的所有行如果有足够的事务日志空间 - 这是您的查询最有可能失败的地方。如果您从 DELETE TOP (n) PERCENT FROM cats WHERE ... 中得到一些结果,那么您可以将其包装在一个循环中,如下所示:

      SELECT 1
      WHILE @@ROWCOUNT <> 0
      BEGIN
       DELETE TOP (somevalue) PERCENT FROM cats
       WHERE cats.id_cat IN (
       SELECT owner_cats.id_cat FROM owner_cats
       WHERE owner_cats.id_owner = 1)
      END
      

      【讨论】:

        【解决方案6】:

        正如其他人所提到的,当您删除 4200 万行时,数据库必须针对数据库记录 4200 万次删除。因此,事务日志必须大幅增长。您可能会尝试将删除分成块。在以下查询中,我使用 NTile 排名函数将行分成 100 个桶。如果这太慢,您可以扩大存储桶的数量,以便每次删除更小。如果在owner_cats.id_ownerowner_cats.id_catscats.id_cat(我假设主键和数字)上有一个索引,这将非常有帮助。

        Declare @Cats Cursor
        Declare @CatId int  --assuming an integer PK here
        Declare @Start int
        Declare @End int
        Declare @GroupCount int
        
        Set @GroupCount = 100
        
        Set @Cats = Cursor Fast_Forward For
            With CatHerd As
                (
                Select cats.id_cat
                    , NTile(@GroupCount) Over ( Order By cats.id_cat ) As Grp
                From cats
                    Join owner_cats
                        On owner_cats.id_cat = cats.id_cat
                Where owner_cats.id_owner = 1
                )
                Select Grp, Min(id_cat) As MinCat, Max(id_cat) As MaxCat
                From CatHerd
                Group By Grp
        Open @Cats
        Fetch Next From @Cats Into @CatId, @Start, @End
        
        While @@Fetch_Status = 0
        Begin
            Delete cats
            Where id_cat Between @Start And @End
        
            Fetch Next From @Cats Into @CatId, @Start, @End
        End 
        
        Close @Cats
        Deallocate @Cats
        

        上述方法的显着问题是它不是事务性的。因此,如果它在第 40 个块上失败,您将删除 40% 的行,而其他 60% 的行仍然存在。

        【讨论】:

        • 谢谢,我可能要试试这个。但是你觉得我的TOP (25) PERCENT 想法怎么样?
        • @John B - TOP X% 解决方案的缺点是您必须在每次迭代中重新查询/重新评估 TOP X%,而不是像我在这里所做的那样。
        【解决方案7】:

        可能值得一试MERGE 例如

        MERGE INTO cats 
           USING owner_cats
              ON cats.id_cat = owner_cats.id_cat
                 AND owner_cats.id_owner = 1
        WHEN MATCHED THEN DELETE;
        

        【讨论】:

        • 我不知道MERGE T-SQL。感谢您的建议;有机会我会尝试并发布结果。
        【解决方案8】:

        (9/28/2011)
        我的回答与 Thomas 的解决方案(2010 年 8 月 6 日)的执行方式基本相同。当我发布我的答案时我错过了它,因为它使用了一个实际的 CURSOR,所以我认为自己“不好”,因为涉及的记录数。但是,当我刚才重读他的答案时,我意识到他使用光标的方式实际上是“好”的。非常聪明。我刚刚对他的回答投了赞成票,将来可能会使用他的方法。如果你不明白为什么,请再看一遍。如果您仍然看不到它,请对此答案发表评论,我会回来尝试详细解释。我决定留下我的答案,因为有人可能有一个 DBA 拒绝让他们使用实际的 CURSOR,不管它有多“好”。 :-)
        编辑>

        我意识到这个问题已经有一年了,但我最近遇到了类似的情况。我试图对一个大表进行“批量”更新,并连接到另一个表,这个表也相当大。问题是加入导致了太多的“加入记录”,以至于处理时间过长并且可能导致争用问题。由于这是一次性更新,我想出了以下“hack”。我创建了一个 WHILE LOOP,它遍历要更新的表,并一次选择 50,000 条记录进行更新。它看起来像这样:

        DECLARE @RecId bigint
        DECLARE @NumRecs bigint
        SET @NumRecs = (SELECT MAX(Id) FROM [TableToUpdate])
        SET @RecId = 1
        WHILE @RecId < @NumRecs
        BEGIN
            UPDATE [TableToUpdate]
            SET UpdatedOn = GETDATE(),
                SomeColumn = t2.[ColumnInTable2]
            FROM    [TableToUpdate] t
            INNER JOIN [Table2] t2 ON t2.Name = t.DBAName 
                AND ISNULL(t.PhoneNumber,'') = t2.PhoneNumber 
                AND ISNULL(t.FaxNumber, '') = t2.FaxNumber
            LEFT JOIN [Address] d ON d.AddressId = t.DbaAddressId 
                AND ISNULL(d.Address1,'') = t2.DBAAddress1
                AND ISNULL(d.[State],'') = t2.DBAState
                AND ISNULL(d.PostalCode,'') = t2.DBAPostalCode
            WHERE t.Id BETWEEN @RecId AND (@RecId + 49999)
            SET @RecId = @RecId + 50000
        END
        

        没什么花哨的,但它完成了工作。因为它一次只处理 50,000 条记录,所以创建的任何锁都是短暂的。此外,优化器意识到它不必处理整个表,因此它在选择执行计划方面做得更好。

        (9/28/2011)
        这里不止一次提到的建议有一个巨大的警告,并在网络上到处张贴关于将“好”记录复制到不同的表,执行 TRUNCATE(或 DROP 和 reCREATE,或 DROP 和重命名),然后重新填充表。

        如果表是 PK-FK 关系(或其他 CONSTRAINT)中的 PK 表,则无法执行此操作。当然,您可以删除关系、进行清理并重新建立关系,但您也必须清理 FK 表。您可以在重新建立关系之前执行此操作,这意味着更多的“停机时间”,或者您可以选择不在创建时强制执行约束并在之后进行清理。我想你也可以在清理 PK 表之前清理 FK 表。底线是您必须以一种或另一种方式显式清理 FK 表。

        我的答案是基于 SET/准CURSOR 的混合过程。这种方法的另一个好处是,如果将 PK-FK 关系设置为 CASCADE DELETES,则您不必执行我上面提到的清理工作,因为服务器会为您处理。如果您的公司/DBA 不鼓励级联删除,您可以要求仅在此进程运行时启用它,然后在完成时禁用它。根据运行清理的帐户的权限级别,可以将用于启用/禁用级联删除的 ALTER 语句附加到 SQL 语句的开头和结尾。 编辑>

        【讨论】:

          【解决方案9】:

          Bill Karwin's answer 另一个问题也适用于我的情况:

          “如果您的DELETE 旨在消除该表中的绝大多数行,人们经常做的一件事是将您想要保留的行复制到重复表中,然后使用DROP TABLETRUNCATE 以更快地清除原始表。”

          Matt in this answer 这样说:

          “如果脱机并删除了很大的百分比,那么构建一个包含要保留的数据的新表、删除旧表并重命名可能是有意义的。”

          ammoQ in this answer(来自同一个问题)推荐(转述):

          • 删除大量行时发出表锁
          • 在任何外键列上放置索引

          【讨论】:

          • Matt & Bill 的建议和类似概念的问题在于,我认为复制 4200 万行可能需要很长时间。
          猜你喜欢
          • 2021-11-17
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-12-19
          相关资源
          最近更新 更多