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,Etc等(MAX)类型的LOB指针的大小是多少?),行外LOB分配有两种类型:
- 内联根(Inline Root)-对于空间在8001到40,000(实际为42,000)字节之间的数据,空间允许,IN ROW中将有1到5个指针(24-72字节)直接指向LOB页面。
- TEXT_TREE-对于超过42,000字节的数据,或者如果1到5个指针不能容纳在行中,则指向LOB页面的指针列表的起始页面将只有24个字节的指针(即“ text_tree”页面)。
每当您检索超过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倍。
这样可以VARBINARY
更快地选择所有列,甚至选择XML数据(现在是),但实际上会伤害那些不选择“ XML”数据的查询。假设您在其他列中大约有50个字节,并且具有FILLFACTOR
100 个字节,则:
无压缩:15k XML
数据应需要2个LOB_DATA页,然后需要2个指针用于内联根。第一个指针为24个字节,第二个指针为12个,用于XML数据的行中总共存储了36个字节。总行大小为86字节,您可以在8060字节数据页上容纳大约93行。因此,一百万行需要10,753个数据页。
自定义压缩:2.5k VARBINARY
数据将适合行内。总行大小为2610(2.5 * 1024 = 2560)字节,并且您只能在8060字节数据页上容纳其中的3行。因此,一百万行需要333,334个数据页。
如此,实施自定义压缩会使 “聚簇索引”的数据页增加 30倍。这意味着,所有使用聚簇索引扫描的查询现在都可以读取约322,500 个数据页。请参阅下面的详细部分,以了解执行此类型压缩的其他影响。
我会告诫不要基于的性能进行任何重构SELECT TOP 1000 *
。这不太可能是应用程序甚至会发出的查询,并且不应将其用作可能不必要的优化的唯一基础。
有关更多详细信息和更多测试,请参阅以下部分。
这个问题不能给出明确的答案,但是我们至少可以取得一些进展,并建议其他研究来帮助我们进一步找出确切的问题(理想情况下基于证据)。
我们所知道的:
- 表格约有100万行
- 表格大小约为15 GB
- 表包含一个
XML
列和类型的其他几个栏目:INT
,BIGINT
,UNIQUEIDENTIFIER
,“等”
XML
列“大小” 平均约为15k
- 运行之后
DBCC DROPCLEANBUFFERS
,需要20到25秒的时间才能完成以下查询:SELECT TOP 1000 * FROM TABLE
- 正在扫描聚簇索引
- 聚集索引上的碎片接近0%
我们认为我们知道的:
- 这些查询之外没有其他磁盘活动。你确定吗?即使没有其他用户查询,是否还会进行后台操作?在同一台计算机上运行的SQL Server外部是否存在可能占用某些IO的进程?可能没有,但仅根据提供的信息尚不清楚。
- 返回15 MB的XML数据。这个数字是基于什么的?是从1000行乘以每行平均15k XML数据的平均值得出的估算值吗?还是以编程方式汇总了该查询的内容?如果只是一种估计,我就不会依赖它,因为XML数据的分布可能不像简单的平均值所暗示的那样。
XML压缩可能会有所帮助。您将如何在.NET中进行压缩?通过GZipStream或DeflateStream类?这不是零成本的选择。它肯定会很大程度地压缩某些数据,但由于每次都需要一个额外的过程来压缩/解压缩数据,因此它还需要更多的CPU。该计划还将完全消除您的能力:
- 查询经由XML数据
.nodes
,.value
,.query
,和.modify
XML功能。
索引XML数据。
请记住(因为您提到XML是“高度冗余的”),XML
数据类型已经过优化,因为它在字典中存储元素和属性名称,为每个项目分配一个整数索引ID,然后使用该整数ID整个文档中(因此,它不会在每次使用时都重复全名,也不会作为元素的结束标记再次重复此全名)。实际数据还删除了多余的空白。这就是为什么提取的XML文档不保留其原始结构的原因,以及为什么将空元素提取为原样<element />
也是如此的原因<element></element>
。因此,只能通过压缩元素和/或属性值来发现通过GZip(或其他方法)进行压缩所获得的任何收益,这是一个可以比大多数人预期的要小得多的表面积,并且很可能不值得失去上面直接指出的功能。
还请记住,压缩XML数据并存储VARBINARY(MAX)
结果不会消除LOB访问,只会减少LOB访问。根据行中其余数据的大小,压缩后的值可能适合行内,或者仍然需要LOB页。
这些信息虽然有用,但还远远不够。有许多因素会影响查询性能,因此我们需要更详细的了解情况。
我们不知道,但需要:
- 为什么要表现
SELECT *
物质?这是您在代码中使用的模式吗?如果是这样,为什么?
- 仅选择XML列的性能如何?如果执行以下操作,统计信息和时间安排是什么
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问答中所指出的那样,逻辑访问相同的LOB数据时读取的内容有所不同,我自己在PasteBin上发表的其他研究报告(T-SQL脚本可以测试LOB读取的各种情况) ,LOB的不被访问一贯之间SELECT
,SELECT INTO
,SELECT @XmlVariable = XmlColumn
,SELECT @XmlVariable = XmlColumn.query(N'/')
,和SELECT @NVarCharVariable = CONVERT(NVARCHAR(MAX), XmlColumn)
。因此,我们的选择在这里受到更多限制,但这是可以做的:
- 通过在运行SQL Server的服务器(SSMS或SQLCMD.EXE)中执行查询来排除网络问题。
- 通过转到“查询选项”->“结果”->“网格”并选中“执行后丢弃结果”选项,可以排除SSMS中的客户端问题。请注意,此选项将阻止所有输出(包括消息),但是对于排除SSMS为每一行分配内存并在网格中绘制所需的时间仍然有用。
或者,您可以通过SQLCMD.EXE执行查询,并通过以下命令将输出定向到无处-o NUL:
。
- 是否有与此查询关联的等待类型?如果是,那等待类型是什么?
返回的列的实际数据大小是多少?如果“ TOP 1000”行包含总数据的不成比例的很大部分,则整个表中该列的平均大小并不重要。如果您想了解前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;
- 在精确的表模式。请提供完整的
CREATE TABLE
声明,包括所有索引。
- 查询计划?那是你可以张贴的东西吗?该信息可能不会改变任何东西,但是最好知道它不会比猜测它不会并且是错误的要好;-)
- 数据文件上是否存在物理/外部碎片?尽管这可能不是一个很大的因素,但是由于您使用的是“消费级SATA”,而不是SSD甚至是超级昂贵的SATA,因此,次优排序扇区的影响将更加明显,尤其是随着这些扇区数量的增加需要读取的内容增加了。
以下查询的确切结果是什么:
SELECT * FROM sys.dm_db_index_physical_stats(DB_ID(),
OBJECT_ID(N'dbo.SchemaName.TableName'), 1, 0, N'LIMITED');
更新
我想到应该尝试重现这种情况,看看我是否遇到类似的行为。因此,我创建了一个包含几列的表(类似于“问题”中含糊的描述),然后在其中填充了一百万行,而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.
*/
并且,由于我们要考虑读取非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.
*/
结论(目前)
基于我尝试重新创建场景的想法,我认为我们不能将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个。因此,我们再次需要有关实际表架构和实际数据的更多信息。