Ale*_*min 19 performance xml sql-server blob
我有一个相当大的表,其中一列是 XML 数据,XML 条目的平均大小约为 15 KB。所有其他列都是常规整数、大整数、GUID 等。为了获得一些具体数字,假设该表有一百万行,大小约为 15 GB。
我注意到的是,如果我想选择所有列,这个表选择数据的速度真的很慢。当我做
SELECT TOP 1000 * FROM TABLE
Run Code Online (Sandbox Code Playgroud)
从磁盘读取数据大约需要 20-25 秒 - 即使我没有对结果强加任何排序。我使用冷缓存(即 after DBCC DROPCLEANBUFFERS
)运行查询。IO统计结果如下:
扫描计数 1,逻辑读 364,物理读 24,预读 7191,lob 逻辑读 7924,lob 物理读 1690,lob 预读 3968。
它抓取了大约 15 MB 的数据。执行计划如我所料显示聚集索引扫描。
除了我的查询外,磁盘上没有任何 IO;我还检查了聚集索引碎片是否接近 0%。这是一个消费级 SATA 驱动器,但我仍然认为 SQL Server 能够以比 ~100-150 MB/min 更快的速度扫描表。
XML 字段的存在导致大部分表数据位于 LOB_DATA 页上(实际上约 90% 的表页是 LOB_DATA)。
我想我的问题是 - 我认为 LOB_DATA 页面会导致扫描缓慢不仅是因为它们的大小,还因为当表中有很多 LOB_DATA 页面时,SQL Server 无法有效扫描聚集索引,我是否正确?
更广泛地说 - 拥有这样的表结构/数据模式是否合理?使用 Filestream 的建议通常说明更大的字段大小,所以我真的不想走那条路。我还没有真正找到关于这个特定场景的任何好的信息。
我一直在考虑 XML 压缩,但它需要在客户端或使用 SQLCLR 完成,并且需要在系统中实现相当多的工作。
我尝试了压缩,因为 XML 是高度冗余的,所以我可以(在 ac# app 中)将 XML 从 20KB 压缩到 ~2.5KB 并将其存储在 VARBINARY 列中,从而防止使用 LOB 数据页。在我的测试中,这将 SELECT 速度提高了 20 倍。
Sol*_*zky 11
XML 字段的存在导致大部分表数据位于 LOB_DATA 页上(实际上约 90% 的表页是 LOB_DATA)。
仅在表中包含 XML 列不会产生这种效果。它是XML的存在数据的是,在某些条件下,使行的数据的某一部分要被存储断列,LOB_DATA页。虽然一个(或者可能几个 ;-)可能会争辩说,该XML
列暗示确实会有 XML 数据,但不能保证 XML 数据需要存储在行外:除非该行已经被填满除了它们是任何 XML 数据之外,小文档(最多 8000 字节)可能适合行内并且永远不会进入 LOB_DATA 页。
我是否认为 LOB_DATA 页会导致扫描缓慢,不仅因为它们的大小,还因为当表中有很多 LOB_DATA 页时,SQL Server 无法有效地扫描聚集索引?
扫描是指查看所有行。当然,读取数据页时,会读取所有行内数据,即使您选择了列的子集。与 LOB 数据的区别在于,如果不选择该列,则不会读取行外数据。因此,就 SQL Server 扫描此聚集索引的效率得出结论是不公平的,因为您没有完全测试(或测试了一半)。您选择了所有列,其中包括 XML 列,正如您所提到的,这是大部分数据所在的位置。
所以我们已经知道SELECT TOP 1000 *
测试不仅仅是连续读取一系列 8k 数据页,而是每行跳转到其他位置。该 LOB 数据的确切结构可能因它的大小而异。根据此处显示的研究(Varchar、Varbinary 等 (MAX) 类型的 LOB 指针的大小是多少?),有两种类型的行外 LOB 分配:
每次检索超过 8000 字节或只是不适合行内的 LOB 数据时,就会发生这两种情况之一。我在 PasteBin.com 上发布了一个测试脚本(用于测试 LOB 分配和读取的 T-SQL 脚本),该脚本显示了 3 种类型的 LOB 分配(基于数据大小)以及每种分配对逻辑和物理读取。在您的情况下,如果 XML 数据确实每行少于 42,000 个字节,那么其中任何一个(或很少)都不应该位于效率最低的 TEXT_TREE 结构中。
如果您想测试 SQL Server 扫描该聚集索引的速度,请执行此操作,SELECT TOP 1000
但指定一个或多个不包括该 XML 列的列。这对你的结果有何影响?它应该快一点。
拥有这样的表结构/数据模式是否合理?
鉴于我们对实际表结构和数据模式的描述不完整,根据那些缺失的细节,任何答案都可能不是最佳的。考虑到这一点,我会说您的表结构或数据模式没有明显不合理的地方。
我可以(在 ac# 应用程序中)将 XML 从 20KB 压缩到 ~2.5KB 并将其存储在 VARBINARY 列中,防止使用 LOB 数据页。在我的测试中,这将 SELECT 速度提高了 20 倍。
这使得选择所有列,甚至只是选择 XML 数据(现在在 中VARBINARY
)更快,但它实际上损害了不选择“XML”数据的查询。假设您在其他列中有大约 50 个字节并且 aFILLFACTOR
为 100,那么:
无压缩:15k 的XML
数据应该需要 2 个 LOB_DATA 页,然后需要 2 个指向内联根的指针。第一个指针是 24 个字节,第二个是 12 个字节,总共 36 个字节存储在行中用于 XML 数据。总行大小为 86 字节,您可以将其中的 93 行放入一个 8060 字节的数据页。因此,100 万行需要 10,753 个数据页。
自定义压缩:2.5k 的VARBINARY
数据将适合行内。总行大小为 2610 (2.5 * 1024 = 2560) 字节,并且您只能将其中的 3 行放入 8060 字节的数据页。因此,100 万行需要 333,334 个数据页。
因此,实施自定义压缩会使聚集索引的数据页增加30 倍。这意味着,所有使用聚集索引扫描的查询现在都需要读取大约 322,500个数据页。请参阅下面的详细部分,了解进行此类压缩的其他后果。
我会告诫不要根据SELECT TOP 1000 *
. 这不太可能是应用程序甚至会发出的查询,并且不应用作潜在不必要优化的唯一基础。
有关更多详细信息和更多要尝试的测试,请参阅以下部分。
这个问题无法给出明确的答案,但我们至少可以取得一些进展并提出额外的研究建议,以帮助我们更接近于弄清楚确切的问题(最好是基于证据)。
我们所知道的:
XML
列和其他几列类型:INT
, BIGINT
, UNIQUEIDENTIFIER
, "etc"XML
列“大小”平均约为 15kDBCC DROPCLEANBUFFERS
,完成以下查询需要 20 - 25 秒:SELECT TOP 1000 * FROM TABLE
我们认为我们知道的:
XML 压缩可能会有所帮助。您将如何在 .NET 中进行压缩?通过GZipStream或DeflateStream类?这不是零成本的选择。它肯定会大比例地压缩一些数据,但它也需要更多的 CPU,因为您每次都需要一个额外的过程来压缩/解压缩数据。该计划还将完全消除您的以下能力:
.nodes
,.value
,.query
,和.modify
XML功能。索引 XML 数据。
请记住(因为您提到 XML 是“高度冗余的”)XML
数据类型已经过优化,因为它将元素和属性名称存储在字典中,为每个项目分配一个整数索引 ID,然后使用该整数 ID在整个文档中(因此它不会在每次使用时重复全名,也不会作为元素的结束标记再次重复)。实际数据还删除了无关的空白。这就是为什么提取的 XML 文档不保留其原始结构以及为什么空元素提取为<element />
即使它们作为<element></element>
. 因此,通过 GZip(或其他任何东西)压缩的任何收益只能通过压缩元素和/或属性值才能找到,这是一个比大多数人预期的要改进的小得多的表面积,并且很可能不值得损失上面直接提到的能力。
还请记住,压缩 XML 数据并存储VARBINARY(MAX)
结果不会消除 LOB 访问,只会减少它。根据行上其余数据的大小,压缩值可能适合行内,或者可能仍需要 LOB 页。
这些信息虽然有用,但还远远不够。影响查询性能的因素有很多,因此我们需要更详细地了解正在发生的事情。
我们不知道,但需要:
SELECT *
重要?这是您在代码中使用的模式吗?如果是这样,为什么?SELECT TOP 1000 XmlColumn FROM TABLE;
?返回这 1000 行所需的 20 - 25 秒中有多少与网络因素(通过网络获取数据)有关,多少与客户端因素有关(呈现大约 15 MB 加上其余的非XML 数据到 SSMS 中的网格,或可能保存到磁盘)?
有时可以通过简单地不返回数据来考虑操作的这两个方面。现在,人们可能会想选择一个临时表或表变量,但这只会引入一些新变量(即磁盘 I/O tempdb
、事务日志写入、tempdb 数据和/或日志文件的可能自动增长,需要缓冲池中的空间等)。所有这些新因素实际上都会增加查询时间。相反,我通常将列存储到变量(适当的数据类型;不是SQL_VARIANT
)中,这些变量会被每个新行(即SELECT @Column1 = tab.Column1,...
)覆盖。
然而,正如@PaulWhite 在这个 DBA.StackExchange Q & A 中指出的那样,访问相同的 LOB 数据时逻辑读取不同,我自己在 PasteBin 上进行了额外的研究(用于测试 LOB 读取的各种场景的 T-SQL 脚本)) ,LOB的不被访问一贯之间SELECT
,SELECT INTO
,SELECT @XmlVariable = XmlColumn
,SELECT @XmlVariable = XmlColumn.query(N'/')
,和SELECT @NVarCharVariable = CONVERT(NVARCHAR(MAX), XmlColumn)
。所以我们的选择在这里有点有限,但这里是可以做的:
-o NUL:
。返回的列的实际数据大小是多少?如果“TOP 1000”行包含不成比例的总数据部分,则整个表中该列的平均大小并不重要。如果您想了解 TOP 1000 行,请查看这些行。请运行以下命令:XML
XML
SELECT TOP 1000 tab.*,
SUM(DATALENGTH(tab.XmlColumn)) / 1024.0 AS [TotalXmlKBytes],
AVG(DATALENGTH(tab.XmlColumn)) / 1024.0 AS [AverageXmlKBytes]
STDEV(DATALENGTH(tab.XmlColumn)) / 1024.0 AS [StandardDeviationForXmlKBytes]
FROM SchemaName.TableName tab;
Run Code Online (Sandbox Code Playgroud)CREATE TABLE
声明,包括所有索引。以下查询的确切结果是什么:
SELECT * FROM sys.dm_db_index_physical_stats(DB_ID(),
OBJECT_ID(N'dbo.SchemaName.TableName'), 1, 0, N'LIMITED');
Run Code Online (Sandbox Code Playgroud)更新
我突然想到我应该尝试重现这个场景,看看我是否遇到了类似的行为。因此,我创建了一个包含多列的表(类似于问题中的模糊描述),然后用 100 万行填充它,XML 列每行大约有 15k 数据(参见下面的代码)。
我发现SELECT TOP 1000 * FROM TABLE
第一次在 8 秒内完成,此后每次 2 - 4 秒(是的,DBCC DROPCLEANBUFFERS
在每次运行SELECT *
查询之前执行)。我的几年前的笔记本电脑是并不快:SQL Server 2012 SP2 开发版,64 位,6 GB RAM,双 2.5 Ghz Core i5,和 5400 RPM SATA 驱动器。我还在运行 SSMS 2014、SQL Server Express 2014、Chrome 和其他一些东西。
根据我系统的响应时间,我将重申我们需要更多信息(即关于表和数据的细节、建议测试的结果等),以帮助缩小 20 - 25 秒响应时间的原因你看到的。
SET ANSI_NULLS, NOCOUNT ON;
GO
IF (OBJECT_ID(N'dbo.XmlReadTest') IS NOT NULL)
BEGIN
PRINT N'Dropping table...';
DROP TABLE dbo.XmlReadTest;
END;
PRINT N'Creating table...';
CREATE TABLE dbo.XmlReadTest
(
ID INT NOT NULL IDENTITY(1, 1),
Col2 BIGINT,
Col3 UNIQUEIDENTIFIER,
Col4 DATETIME,
Col5 XML,
CONSTRAINT [PK_XmlReadTest] PRIMARY KEY CLUSTERED ([ID])
);
GO
DECLARE @MaxSets INT = 1000,
@CurrentSet INT = 1;
WHILE (@CurrentSet <= @MaxSets)
BEGIN
RAISERROR(N'Populating data (1000 sets of 1000 rows); Set # %d ...',
10, 1, @CurrentSet) WITH NOWAIT;
INSERT INTO dbo.XmlReadTest (Col2, Col3, Col4, Col5)
SELECT TOP 1000
CONVERT(BIGINT, CRYPT_GEN_RANDOM(8)),
NEWID(),
GETDATE(),
N'<test>'
+ REPLICATE(CONVERT(NVARCHAR(MAX), CRYPT_GEN_RANDOM(1), 2), 3750)
+ N'</test>'
FROM [master].[sys].all_columns sac1;
IF ((@CurrentSet % 100) = 0)
BEGIN
RAISERROR(N'Executing CHECKPOINT ...', 10, 1) WITH NOWAIT;
CHECKPOINT;
END;
SET @CurrentSet += 1;
END;
--
SELECT COUNT(*) FROM dbo.XmlReadTest; -- Verify that we have 1 million rows
-- O.P. states that the "clustered index fragmentation is close to 0%"
ALTER INDEX [PK_XmlReadTest] ON dbo.XmlReadTest REBUILD WITH (FILLFACTOR = 90);
CHECKPOINT;
--
DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS;
SET STATISTICS IO, TIME ON;
SELECT TOP 1000 * FROM dbo.XmlReadTest;
SET STATISTICS IO, TIME OFF;
/*
Scan count 1, logical reads 21, physical reads 1, read-ahead reads 4436,
lob logical reads 5676, lob physical reads 1, lob read-ahead reads 3967.
SQL Server Execution Times:
CPU time = 171 ms, elapsed time = 8329 ms.
*/
Run Code Online (Sandbox Code Playgroud)
而且,因为我们想要计算读取非 LOB 页面所花费的时间,所以我运行以下查询来选择除 XML 列之外的所有内容(我在上面建议的测试之一)。这相当一致地在 1.5 秒内返回。
DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS;
SET STATISTICS IO, TIME ON;
SELECT TOP 1000 ID, Col2, Col3, Col4 FROM dbo.XmlReadTest;
SET STATISTICS IO, TIME OFF;
/*
Scan count 1, logical reads 21, physical reads 1, read-ahead reads 4436,
lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 1666 ms.
*/
Run Code Online (Sandbox Code Playgroud)
结论(目前)
根据我重新创建您的场景的尝试,我认为我们不能将 SATA 驱动器或非顺序 I/O 视为 20 - 25 秒的主要原因,特别是因为我们仍然不知道不包含 XML 列时查询返回的速度有多快。而且我无法重现您显示的大量逻辑读取(非 LOB),但我有一种感觉,鉴于此以及以下语句,我需要向每一行添加更多数据:
~90% 的表页是 LOB_DATA
我的表有 100 万行,每行有超过 15k 的 XML 数据,并sys.dm_db_index_physical_stats
显示有 200 万个 LOB_DATA 页。剩下的 10% 将是 222k IN_ROW 数据页,但我只有其中的 11,630 个。因此,我们再次需要有关实际表架构和实际数据的更多信息。
Mik*_*son 10
我认为 LOB_DATA 页不仅因为它们的大小,而且因为 SQL Server 无法有效地扫描聚集索引会导致扫描缓慢,这是否正确?
是的,读取未存储在行中的 LOB 数据会导致随机 IO 而不是顺序 IO。此处用来理解为什么快或慢的磁盘性能指标是随机读取 IOPS。
LOB 数据存储在树状结构中,其中聚集索引中的数据页指向一个 LOB 数据页,而 LOB 根结构又指向实际的 LOB 数据。在遍历聚集索引中的根节点时,SQL Server 只能通过顺序读取来获取行内数据。要获取 LOB 数据,SQL Server 必须转到磁盘上的其他位置。
我想如果你改用 SSD 磁盘,你不会因此受到太多影响,因为 SSD 的随机 IOPS 比旋转磁盘高得多。
拥有这样的表结构/数据模式是否合理?
是的,可能是。取决于这张桌子为你做什么。
通常,当您想要使用 T-SQL 查询 XML 时,SQL Server 中的 XML 的性能问题会发生,当您想要在 where 子句或连接的谓词中使用来自 XML 的值时更是如此。如果是这种情况,您可以查看属性提升或选择性 XML 索引或重新设计表结构,将 XML 分解为表。
我试过压缩
10 多年前,我在一个产品中做过一次,从那以后就后悔了。我真的很想念无法使用 T-SQL 处理数据,所以如果可以避免的话,我不会向任何人推荐。