删除具有4200万行的表的相关子查询的SQL?

时间:2010-08-06 22:52:10

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

我有一张表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,并将“恢复模式”设置为“简单”。

所以,我考虑做这样的事情(从应用程序btw执行这个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日):

好的,我刚刚在再次阅读上述链接后意识到我没有这些表上的索引。另外,你们中的一些人已经在下面的评论中指出了这个问题。请记住,这是一个虚构的模式,所以即使id_cat也不是PK,因为在我的现实模式中,它不是一个独特的领域。

我将把索引放在:

  1. cats.id_cat
  2. owner_cats.id_cat
  3. owner_cats.id_owner
  4. 我想我仍然掌握着这个数据仓库,显然我需要在所有JOIN字段上建立索引吗?

    但是,我需要几个小时才能完成批量加载过程。我已经把它作为一个SqlBulkCopy(一块一块,而不是42 mil)。我有一些索引和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小时:min (是一小时!) 对比22:08 min:sec和13:21 min :sec与2:10 min:sec分别为42 mil行和13 mil行。我现在要用索引查询Tom的查询,但这是朝着错误的方向前进。请帮忙。

    更新(2010年8月9日):

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

    更新(8/10/2010):

    由于缺乏任何其他选择,我想出了一个我认为乏味的解决方案(希望是暂时的)

    1. 将数据库连接的超时时间增加到1小时(CommandTimeout=60000;默认值为30秒)
    2. 使用Tom的查询:DELETE FROM WHERE EXISTS (SELECT 1 ...)因为它执行得更快
    3. 运行删除语句(???) 之前
    4. DROP所有索引和PK
    5. 运行DELETE声明
    6. CREATE所有索引和PK
    7. 似乎很疯狂,但至少它比使用TRUNCATE更快,并且从第一个owner_id开始加载我的负载,因为我owner_id中的一个需要2:30时间: min加载与17:22 min:sec的删除过程我刚刚描述的42 mil行。 (注意:如果我的加载进程抛出异常,我会重新开始owner_id,但我不想吹走之前的owner_id,所以我不想TRUNCATE owner_cats表,这就是我尝试使用DELETE的原因。)

      不再需要帮助了。)

9 个答案:

答案 0 :(得分:6)

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

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

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

最后一点往往是最重要的。在另一个查询窗口中执行sp_who2命令以确保不会发生锁争用,从而阻止命令执行。

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

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

答案 1 :(得分:6)

如果删除将删除表中的“大量”行,则可以替换DELETE:将记录保留在其他位置,截断原始表,放回“守护者”。类似的东西:

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

答案 2 :(得分:6)

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

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

答案 3 :(得分: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)

答案 4 :(得分:3)

没有这样的阈值 - 您可以从任何表中删除所有行,并给出足够的事务日志空间 - 这是您的查询最有可能崩溃的地方。如果你从你的DELETE TOP(n)PERCENT FROM猫那里得到一些结果,那么你可以把它包装成一个循环,如下所示:

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

答案 5 :(得分:3)

正如其他人所提到的,当你删除4200万行时,db必须记录对数据库的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%仍然存在。

答案 6 :(得分:3)

可能值得尝试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;

答案 7 :(得分:1)

&LT;编辑&gt; (9/28/2011)
我的回答基本上与Thomas&#39;解决方案(8月6日和10日)。当我发布我的答案时,我错过了它,因为它使用了一个实际的CURSOR,所以我想我自己&#34;坏&#34;因为涉及的记录数量。然而,当我刚才重读他的答案时,我意识到他使用光标的方式实际上是好的&#34;。非常聪明。我刚刚投了他的答案,将来可能会使用他的方法。如果你不明白为什么,请再看一遍。如果您仍然无法看到它,请对此答案发表评论,我会回来并尝试详细解释。我决定留下我的答案,因为有人可能会有一个DBA拒绝让他们使用实际的游标,无论他们如何&#34;好&#34;它是。 :-)
&LT; /编辑&gt;

我意识到这个问题已经有一年了,但最近我遇到了类似的情况。我试图做&#34;批量&#34;更新到具有连接到不同表的大表,也相当大。问题是,加入导致了很多&#34;加入的记录&#34;处理过程需要很长时间,并且可能导致争用问题。由于这是一次性更新,我想出了以下内容&#34; hack。&#34;我创建了一个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条记录,所以创建的任何锁都是短暂的。此外,优化器意识到它不必执行整个表,因此它在选择执行计划方面做得更好。

&LT;编辑&gt; (9/28/2011)
对于这里提到的建议不止一次有一个巨大的警告,并且在网络上发布关于复制&#34;好&#34;记录到不同的表,执行TRUNCATE(或DROP和reCREATE,或DROP并重命名),然后重新填充表。

如果表是PK-FK关系(或其他CONSTRAINT)中的PK表,则无法执行此操作。当然,您可以删除关系,进行清理并重新建立关系,但您也必须清理FK表。你可以在重新建立关系之前做到这一点,这意味着更多的&#34;停机时间&#34;,或者你可以选择在创建时不执行CONSTRAINT并在之后进行清理。我猜你也可以在清理PK表之前清理FK表。最重要的是你必须以某种方式明确地清理FK表。

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

答案 8 :(得分:0)

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

“如果您的DELETE旨在消除该表中的绝大多数行,那么人们经常做的一件事就是将您要保留的行复制到重复的表中,然后使用{{ 1}}或DROP TABLE可以更快地消灭原始表格。“

Matt in this answer这样说:

“如果离线并删除大的%,可能只需构建一个包含要保留的数据的新表,删除旧表并重命名。”

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

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