SQL Server高效地删除数百万行数据的方法

10

我最近提出了这个问题:MS SQL如何在表之间共享标识种子(许多人想知道为什么)

我有一个如下所示的表布局:

Table: Stars
starId bigint
categoryId bigint
starname varchar(200)

但我的问题是,我有数百万行。因此,当我想从Stars表中删除星星时,它对SQL Server来说太过于强烈。

我不能使用2005+内置的分区,因为我没有企业许可证。

但是,当我删除时,我总是一次性删除整个类别ID。

我想做这样的设计:

Table: Star_1
starId bigint
CategoryId bigint constraint rock=1
starname varchar(200)

Table: Star_2
starId bigint
CategoryId bigint constraint rock=2
starname varchar(200)

通过这种方式,我可以通过简单的删除表来删除整个类别,从而以O(1)的速度处理数百万行。

我的问题是:在SQL Server中拥有数十万个表是否会产生问题? O(1)的删除对我来说非常可取。也许有完全不同的解决方案我没有想到吗?

编辑:

插入后星星是否会被修改?否。

您是否必须跨星级别查询数据?我从未跨星级别查询过。

如果您正在查找特定星星的数据,您是否知道要查询哪个表?是的。

输入数据时,应用程序将如何决定将数据放入哪个表中?在创建categoryId时一次性插入星形数据。

会有多少个类别?您可以假定会有无限星级类别。每天最多可达100个星级类别,每天最多可达30个不需要的星级类别。

您真的需要删除整个类别还是仅更改了数据的星级?是整个星级类别。

您尝试过分批删除吗?是的,我们今天就这样做,但效果不够好。

另一种技术是标记要删除的记录?没有必要标记一个星级为已删除,因为我们知道整个星级类别都可以删除。

其中有多少比例从未使用过?通常情况下,我们保留每个星级类别的数据几周,但有时需要保留更久。

当您认为某个类别有用时,它永远有用吗,还是可能以后需要删除?

不是永久的,而是直到发出手动请求删除该类别为止。如果是这样,有多少时间需要删除?并不经常。

您正在使用何种磁盘排列?目前是单个文件组存储且没有分区。

您能使用 SQL 企业版吗?不行。许多人都在运行此软件,他们只有 SQL 标准版。购买 MS SQL 企业版超出了他们的预算。


7
让我猜猜,你有“亿万”条记录。 - D'Arcy Rittich
这些表格与我的不完全相同,但我认为它是一个很好的例子,可以清楚地表达我的观点 :) - Net Citizen
1
https://dev59.com/dXNA5IYBdhLWcg3wcdhk - womp
1
我看到了一些回答说"SQL Server删除数百万行应该足够快"。也许有使用SQL Server的人(可惜现在不是我,抱歉)应该真正设计一个原型,并分享性能结果以证明这一点。 - Oddthinking
1
@Oddthinking:完成。结果基本符合预期。 - Aaronaught
显示剩余6条评论
13个回答

34
问题:在SQL Server中有成千上万个表是否会造成问题?
答案:是的。在SQL Server中拥有如此多的表是一个巨大的问题。每个对象都必须被SQL Server作为元数据跟踪,一旦包括索引、引用约束、主键、默认值等等,那么您将要面对数百万个数据库对象。虽然SQL Server理论上可能能够处理2^32个对象,但它肯定会在远远少于这个数字的情况下开始崩溃。
如果数据库没有崩溃,你的开发人员和IT工作人员几乎肯定会。当我看到超过一千个表时,我就会感到紧张;展示给我一个有数十万个表的数据库,我肯定会惊慌失措地逃走。
创建数十万个表作为贫民分区策略将使你失去进行以下任何操作的能力:
1.编写高效的查询(如何选择多个类别?) 2.维护唯一的标识符(正如您已经发现的那样) 3.维护引用完整性(除非您喜欢管理300,000个外键) 4.执行范围更新 5.编写干净的应用程序代码 6.维护任何历史记录 7.强制执行适当的安全性(很明显用户必须能够启动这些创建/删除操作-非常危险) 8.适当地缓存- 100,000个表意味着100,000个不同的执行计划都在竞争相同的内存,而您可能没有足够的内存; 9.雇用一名DBA(因为请放心,只要他们看到您的数据库,他们就会辞职)。
另一方面,在单个表中拥有成千上万行甚至数百万行不是问题 - 这是SQL Server和其他SQL RDBMS设计的使用方式,并且它们已经为这种情况进行了非常良好的优化。
问题:O(1)的下降对我来说非常有吸引力。也许我没有考虑到完全不同的解决方案?
答案:解决数据库性能问题的典型方法按照偏好顺序是:
1.运行分析器以确定查询中最慢的部分; 2.如果可能的话改进查询(即通过消除非搜索谓词); 3.标准化或添加索引以消除这些瓶颈; 4.必要时反规范化(通常不适用于删除); 5.如果涉及级联约束或触发器,请在事务期间禁用该约束并手动清除级联。
但实际情况是,您不需要“解决方案”。

"数百万行的数据"在 SQL Server 数据库中并不算很多。如果你想从表中删除几千行数据,只需对你希望从中删除的列进行索引 - 在这个例子中是 CategoryID,那么这个过程非常快速,SQL Server 可以轻松应对。

实际上,删除操作通常具有 O(M log N) 的复杂度(N 是行数,M 是要删除的行数)。要达到 O(1) 的删除时间,你将牺牲 SQL Server 提供的几乎所有优势。

O(M log N) 的速度可能没有 O(1) 快,但你所说的这种减慢速度(需要几分钟才能删除)一定有第二个原因。数字并不相符,为了证明这一点,我已经制作了一个基准测试:


表结构:

CREATE TABLE Stars
(
    StarID int NOT NULL IDENTITY(1, 1)
        CONSTRAINT PK_Stars PRIMARY KEY CLUSTERED,
    CategoryID smallint NOT NULL,
    StarName varchar(200)
)

CREATE INDEX IX_Stars_Category
ON Stars (CategoryID)

注意,这个模式甚至没有真正针对 DELETE 操作进行优化,它是SQL服务器中常见的表模式。如果这个表没有关系,那么我们就不需要代理键或聚集索引(或者我们可以将聚集索引放在类别上)。稍后我会再回来谈论这个。

示例数据:

这将使用500个类别(即每个类别的基数为1:20,000)填充表格,生成1000万行数据。您可以调整参数以更改数据量和/或基数。

SET NOCOUNT ON

DECLARE
    @BatchSize int,
    @BatchNum int,
    @BatchCount int,
    @StatusMsg nvarchar(100)

SET @BatchSize = 1000
SET @BatchCount = 10000
SET @BatchNum = 1

WHILE (@BatchNum <= @BatchCount)
BEGIN
    SET @StatusMsg =
        N'Inserting rows - batch #' + CAST(@BatchNum AS nvarchar(5))
    RAISERROR(@StatusMsg, 0, 1) WITH NOWAIT

    INSERT Stars2 (CategoryID, StarName)
        SELECT
            v.number % 500,
            CAST(RAND() * v.number AS varchar(200))
        FROM master.dbo.spt_values v
        WHERE v.type = 'P'
        AND v.number >= 1
        AND v.number <= @BatchSize

    SET @BatchNum = @BatchNum + 1
END

个人资料脚本

最简单的一个...

DELETE FROM Stars
WHERE CategoryID = 50

结果:

这是在一台5年前的工作站上测试的,该机器运行着32位双核AMD Athlon和一个廉价的7200 RPM SATA硬盘。我使用不同的CategoryIDs运行了10次测试。最慢的时间(冷缓存)约为5秒,最快的时间为1秒。

可能不像直接删除表那么快,但远远不及您提到的需要数分钟的删除时间。而且请记住,这甚至不是在一台体面的机器上!

但我们可以做得更好...

你问题中的所有相关数据都是没有关系的。如果你没有关系,你就不需要代理键,并且可以去掉一个索引,将聚集索引移动到CategoryID列。

现在,通常来说,在非唯一/非顺序列上使用聚集索引并不是一个好习惯。但我们只是在进行基准测试,所以我们仍然这样做:

CREATE TABLE Stars
(
    CategoryID smallint NOT NULL,
    StarName varchar(200)
)

CREATE CLUSTERED INDEX IX_Stars_Category
ON Stars (CategoryID)

执行相同的测试数据生成器后(导致大量页面分裂),同样的删除只需要62毫秒,从冷高速缓存中需要190毫秒(离群值)。值得一提的是,如果索引被设为非聚集(根本没有聚集索引),则平均删除时间仅增加到606毫秒。

结论:

如果您看到删除时间需要几分钟——甚至几秒钟,那么就有非常严重的问题。

可能的因素包括:

  • 统计信息不是最新的(虽然在这里不应该是问题,但如果是,请运行sp_updatestats);

  • 缺乏索引(尽管奇怪的是,在第一个示例中删除IX_Stars_Category索引实际上会导致更快的整体删除,因为聚集索引扫描比非聚集索引删除更快);

  • 选择不当的数据类型。如果您只有数百万行,而不是数十亿行,则不需要在StarID上使用bigint。在CategoryID绝对不需要它——如果您的类别少于32,768个,则甚至可以使用smallint。每行中的不必要数据字节都会增加I/O成本。

  • 锁争用。也许问题实际上并不是删除速度;也许其他脚本或进程正在持有Star行的锁定,而DELETE只是在等待这些锁释放。

  • 非常低劣的硬件。我能够在一台相当糟糕的机器上运行它而不会遇到任何问题,但是如果您将此数据库运行在90年代的Presario或某种类似的机器上,这些机器根本不适合托管SQL Server实例,并且负载很重,那么显然会遇到问题。

  • 非常昂贵的外键、触发器、约束或其他数据库对象,您在示例中没有包含它们,可能会增加很高的成本。您的执行计划应清楚地显示这一点(在上面的优化示例中,它只是单个聚集索引删除)。

老实说,我想不出其他可能性了。 SQL Server中的删除操作就是不会如此缓慢


如果您能够运行这些基准测试并看到与我看到的大致相同的性能(或更好),那么这意味着问题在于您的数据库设计和优化策略,而不是SQL Server或删除的渐进复杂度。我建议作为一个起点,阅读一些优化方面的文章:

如果这些还不能帮助你,我可以提供以下额外建议:

  • 升级到 SQL Server 2008,该版本提供了众多 压缩选项,可以大大提高 I/O 性能;

  • 考虑将每个分类的 Star 数据预先压缩成紧凑的序列化列表(使用 .NET 中的 BinaryWriter 类),并将其存储在一个 varbinary 列中。这样,您可以每个分类只保存一行数据。虽然这违反了 1NF 规则,但由于您似乎也不从数据库中查询单个 Star 数据,我认为您不会损失太多。

  • 考虑使用非关系型数据库或存储格式,例如 db4oCassandra。而不是实现已知的数据库反模式(臭名昭著的“数据转储”),使用一个真正设计用于这种存储和访问模式的工具。


很棒的回答。加一点赞因为使用了"sargable"这个词。我不得不查了一下,但它是"Search ARGument Able"的缩写,意思是DBMS可以利用索引来提高速度。太喜欢了。 - Noah Heldman
1
这个答案太棒了,如果我能给未决赏金添加分数的话,我一定会这么做。谢谢您,先生。 - SqlRyan
3
有时候Stack Overflow需要一个“超级双倍额外点赞”。这就是其中之一。 - Oddthinking

4

你必须删除它们吗?通常最好只需将 IsDeleted 位列设置为 1,然后在非工作时间异步进行实际删除。

编辑:

这是一个不确定的想法,但在 CategoryId 上添加一个聚集索引可能会加速删除。这也可能对其他查询产生不利影响。您可以测试一下吗?


是的。Category表上的IsDeleted意味着只需要更新一条记录。 - Martin Smith
我们今天已经异步地执行了删除操作,但我们希望提高性能。 - Net Citizen
@OrbMan:对于那个编辑评论点赞,但这是我们今天已经在做的事情。当我们将集群放在categoryId列上时,确实大大提高了性能。但是,categoryId的非行内删除仍然会导致我们应用程序中的瓶颈问题。 - Net Citizen
@网民 - 好的,你还有什么没有告诉我们的呢 ;) - D'Arcy Rittich
@网友,如果你要删除所有这些单独的表格,那么你将如何保持你的表格? - Cade Roux
显示剩余3条评论

2
这是SQL 2000中的旧技术,分区视图,对于SQL 2005仍然是一个有效的选择。但当涉及到大量表和与之相关的维护开销时,问题就出现了。
正如您所说,分区是一种企业级功能,但是它是为了这种大规模数据删除/滚动窗口效应而设计的。
另一个选择是运行批量删除,以避免创建一个非常大的事务,而是创建数百个更小的事务,以避免锁升级并保持每个事务的大小较小。

我们目前使用批量删除,但是我们希望提高性能并减少SQL工作量,因为它也被广泛用于其他方面。 - Net Citizen
然后,您可以承担PV的管理开销,或者对于如此大量的数据,让某人升级到企业版 - 我已经实施了每个分区350百万行的系统,在这种数量级下,这是最佳选择。 - Andrew
企业对我的客户来说不是一个选择。 - Net Citizen
分区视图及其相关的管理开销是将其降至O(1)的唯一途径 - 这是2000年的有效技术,对于2005年仍然如此。(在2005年及以后的组合中也是有效的) - Andrew
我不知道PV在处理成百上千张表格时的表现如何?! - Martin Smith
那需要进行严格的测试,我怀疑没有人特别尝试过这些量级 - 分区仅限于1k,有充分的理由(尽管您可以跨多个1k分区表进行PV以扩展限制)。 - Andrew

2

将表分开是分区 - 您只是手动管理它,没有获得任何管理帮助或统一访问(没有视图或分区视图)。

企业版的成本是否比单独构建和维护分区方案的成本更高?

长时间运行的删除操作的替代方法还包括使用具有相同模式的替换表填充并仅排除要删除的行,然后使用sp_rename交换表。

我不明白为什么会定期删除整个类别的星级?您可能正在不断创建新的类别,这意味着您的类别数量必须很大,并且在(手动或否则)进行分区将非常密集。


“企业版的成本比单独构建和维护分区方案的成本更昂贵吗?” - 是的,因为我有数千个客户都在运行该软件,对他们来说成本太高了。 - Net Citizen
“除了长期运行的删除操作之外,替代方案还包括使用相同模式填充替换表并简单地排除要删除的行,然后使用sp_rename交换表。”- 我们已经探索过这个方法,但是我们不能让任何程度上的表空置无物。 - Net Citizen
那么你将它插入到主表中,然后发现不需要它,于是删除它? - Martin Smith
1
@网民,也许重新考虑分区的想法更合理,不是根据数据内容,而是根据其状态。即使您正在考虑按categoryid进行分区,也许真正有意义的“分区”是基于某种已处理或已评估状态 - 如未处理、已处理。然后创建跨越状态的视图(这些状态要少得多)。 - Cade Roux
2
我和Cade的想法类似。不过,如果要提出具体建议,了解以下信息会很有用:
  1. 这些类别被插入的频率是多少?
  2. 其中有多少比例从未被使用过?
  3. 当您决定某个类别很有用时,它是否永久保留,还是可能稍后需要删除?如果需要删除,发生的概率是多少?
  4. 您使用什么样的磁盘排列方式?
- Martin Smith
显示剩余3条评论

1
当你说删除数百万行对于SQL服务器来说“太过强烈”时,你是什么意思?你是指在删除期间日志文件增长太多吗?
你所需要做的就是按固定大小分批执行删除操作:
DECLARE @i INT
SET @i = 1

WHILE @i > 0
BEGIN
    DELETE TOP 10000 FROM dbo.SuperBigTable
        WHERE CategoryID = 743
    SELECT @i = @@ROWCOUNT
END

如果您的数据库处于完整恢复模式,则在此过程中必须运行频繁的事务日志备份,以便它可以重用日志中的空间。如果数据库处于简单模式,则不需要做任何事情。

我唯一的建议是确保在CategoryId中有适当的索引。我甚至可能建议将其作为聚集索引。


1

如果你想要优化一个类别,删除具有类别在首位的聚集复合索引可能会比造成伤害更有好处。

此外,您还可以描述表格之间的关系。


1

看起来事务日志正在处理删除操作的大小问题。 事务日志按单位增长,而在分配更多磁盘空间时需要时间。

没有办法在不注册事务的情况下从表中删除行,尽管可以使用TRUNCATE命令截断表。 但是,这将无条件地删除表中的所有行。

我可以提供以下建议:

  1. 切换到非事务性数据库或可能的平面文件。 看起来您不需要事务性数据库的原子性。

  2. 尝试以下方法。 每x个删除(取决于大小),发出以下语句

BACKUP LOG WITH TRUNCATE_ONLY;

这只是截断事务日志,空间保留给日志以重新填充。然而,我不确定这会为操作增加多少时间。


1

或许可以将 Stars 表的主键设置为非聚集索引,并在 categoryid 上添加一个聚集索引。

除此之外,服务器的性能最佳实践方面的设置是否良好?例如使用独立的物理磁盘来存储数据和日志,不使用 RAID5 等。


我们已经有了一个聚集索引。但是删除仍然需要几分钟,而我的建议可以在1秒钟内完成。 - Net Citizen

0

我在原帖中没有得到我的评论的答复,所以我正在根据一些假设进行...

这是我的想法:使用多个数据库,每个类别一个。

您可以免费使用随每个Windows版本一起提供的托管ESE数据库

使用PersistentDictionary对象,并以此方式跟踪starid、starname对。如果您需要删除一个类别,只需删除该类别的PersistentDictionary对象即可。

PersistentDictionary<int, string> starsForCategory = new PersistentDictionary<int, string>("Category1");

这将创建名为“Category1”的数据库,您可以在其中使用标准的.NET字典方法(add、exists、foreach等)。


0

我知道这有点离题,但是SQL Server(或任何关系型数据库)真的是这项工作的好工具吗?你实际上使用了哪些关系数据库功能?

如果您一次删除整个类别,那么您不能依赖它来保持参照完整性。数据是只读的,因此您不需要ACID进行数据更新。

对我来说,听起来像您正在使用基本的SELECT查询功能?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接