6

我有一张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)

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

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

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

编辑(2010 年 8 月 6 日):

好的,我在再次阅读上面的链接后才意识到我在这些表上没有索引。此外,你们中的一些人已经在下面的评论中指出了这个问题。请记住,这是一个虚构的模式,所以甚至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 百万行和分别为 13 百万行。我现在要尝试使用索引进行 Tom 的查询,但这是朝着错误的方向发展。请帮忙。

更新(2010 年 8 月 9 日):

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

更新(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从 first 开始使用和重新开始我的负载要快owner_id,因为我的一个owner_id需要 2:30 hrs:min 来加载,而我刚刚描述的删除过程需要 17:22 min:sec 42 百万行。(注意:如果我的加载过程抛出异常,我会重新开始owner_id,但我不想吹走之前的owner_id,所以我不想上桌,这就是我尝试使用的原因。)TRUNCATEowner_catsDELETE

任何帮助仍将不胜感激:)

4

9 回答 9

6

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

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

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

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

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

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

于 2010-08-06T23:01:22.227 回答
6

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

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
于 2010-08-06T23:29:30.840 回答
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
于 2010-08-10T21:06:29.097 回答
4

如果您使用 anEXISTS而不是 an 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)
于 2010-08-07T10:06:18.577 回答
3

没有这样的阈值 - 如果有足够的事务日志空间,您可以从任何表中删除所有行- 这是您的查询最有可能失败的地方。如果您从 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
于 2010-08-06T23:02:12.287 回答
3

正如其他人所提到的,当您删除 4200 万行时,数据库必须针对数据库记录 4200 万次删除。因此,事务日志必须大幅增长。您可能会尝试将删除分成块。在以下查询中,我使用 NTile 排名函数将行分成 100 个桶。如果这太慢,您可以扩大存储桶的数量,以便每次删除都更小。owner_cats.id_owner如果,owner_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% 仍将存在。

于 2010-08-06T23:15:53.630 回答
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;
于 2011-09-30T07:42:21.693 回答
1

<Edit> (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 并重新创建,或 DROP 并重命名),然后重新填充表。

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

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

于 2011-08-04T22:49:21.120 回答
0

Bill Karwin's answer to another question applies to my situation also:

"If your DELETE is intended to eliminate a great majority of the rows in that table, one thing that people often do is copy just the rows you want to keep to a duplicate table, and then use DROP TABLE or TRUNCATE to wipe out the original table much more quickly."

Matt in this answer says it this way:

"If offline and deleting a large %, may make sense to just build a new table with data to keep, drop the old table, and rename."

ammoQ in this answer (from the same question) recommends (paraphrased):

  • issue a table lock when deleting a large amount of rows
  • put indexes on any foreign key columns
于 2010-08-11T23:18:39.537 回答