问题:在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
数据,我认为您不会损失太多。
考虑使用非关系型数据库或存储格式,例如 db4o 或 Cassandra。而不是实现已知的数据库反模式(臭名昭著的“数据转储”),使用一个真正设计用于这种存储和访问模式的工具。