LOB_DATA,慢速表扫描和一些I / O问题


19

我有一个相当大的表,其中一列是XML数据,XML条目的平均大小约为15 KB。所有其他列都是常规int,bigints,GUID等。要获得一些具体数字,我们假设该表有100万行,大小约为15 GB。

我注意到的是,如果我要选择所有列,则从此表选择数据的速度确实很慢。当我做

SELECT TOP 1000 * FROM TABLE

从磁盘读取数据大约需要20-25秒-即使我没有对结果施加任何顺序。我使用冷缓存(即之后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#应用程序中)将XML从20KB压缩到〜2.5KB并将其存储在VARBINARY列中,从而避免使用LOB数据页。在我的测试中,SELECT的速度提高了20倍。


亚历克斯:不知道您是否看到与我的答案有关的讨论(链接在我的答案下方的评论中),但是我能够重现您的情况。我填充了一个与您的描述相匹配的表(与我的信息一样多),并且得到了非常相似的I / O统计信息。除此之外,“ LOB物理读取”从未接近。因此,我想知道您是否更新了XML(但未更新其他列)和/或数据文件有很多物理碎片。我仍然不介意获取表的DDL和每个数据文件的自动增长设置,并且您会收缩数据文件吗?
所罗门·鲁茨基

首先-非常感谢您的详细回答,由于时间有限,我当时无法参加讨论。既然您已经提到了这一点(当问到问题时我就没有想到)-XML字段在创建后会多次更新,并且创建得很小。因此,我怀疑最初是将其存储在行中,并且在进行一些更新之后将其移到LOB页面结构中,然后再进行其他更新。
Alexander Shelemin '16

(续)在询问问题之前,我检查了文件的物理碎片,并且内置的Windows工具认为还可以,因此不再赘述。自动增长是默认设置,我相信是1 MB,并且数据文件还没有缩小。
Alexander Shelemin '16

在我的特定情况下,选择前1000 *很重要。我当然知道这是一种不好的做法,但是经过很长一段时间,某些应用程序设计决策确实很难更改。Select *基本上用作我们应用程序中不同组件之间的跨数据库复制策略。它有优点,例如,我们可以动态地对数据/架构进行很多任意操作,这对于内置复制技术来说很难,但是它带来了问题。
Alexander Shelemin '16

Alex,SELECT *如果您需要XML数据,则不是问题。如果您不希望使用XML数据,这只是一个问题,在这种情况下,为什么要降低查询速度以取回您不使用的数据?我询问有关XML的更新,想知道是否没有正确报告LOB页上的碎片。这就是为什么我在回答中问您如何准确确定聚集索引没有碎片化?您可以提供您运行的命令吗?您是否对集群索引进行了完整的重建?(续)
所罗门·鲁兹基

Answers:


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,Etc等(MAX)类型的LOB指针的大小是多少?),行外LOB分配有两种类型:

  1. 内联根(Inline Root)-对于空间在8001到40,000(实际为42,000)字节之间的数据,空间允许,IN ROW中将有1到5个指针(24-72字节)直接指向LOB页面。
  2. 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个字节,并且具有FILLFACTOR100 个字节,则:

  • 无压缩: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 *。这不太可能是应用程序甚至会发出的查询,并且不应将其用作可能不必要的优化的唯一基础。

有关更多详细信息和更多测试,请参阅以下部分。


这个问题不能给出明确的答案,但是我们至少可以取得一些进展,并建议其他研究来帮助我们进一步找出确切的问题(理想情况下基于证据)。

我们所知道的:

  1. 表格约有100万行
  2. 表格大小约为15 GB
  3. 表包含一个XML列和类型的其他几个栏目:INTBIGINTUNIQUEIDENTIFIER,“等”
  4. XML列“大小” 平均约为15k
  5. 运行之后DBCC DROPCLEANBUFFERS,需要20到25秒的时间才能完成以下查询:SELECT TOP 1000 * FROM TABLE
  6. 正在扫描聚簇索引
  7. 聚集索引上的碎片接近0%

我们认为我们知道的:

  1. 这些查询之外没有其他磁盘活动。你确定吗?即使没有其他用户查询,是否还会进行后台操作?在同一台计算机上运行的SQL Server外部是否存在可能占用某些IO的进程?可能没有,但仅根据提供的信息尚不清楚。
  2. 返回15 MB的XML数据。这个数字是基于什么的?是从1000行乘以每行平均15k XML数据的平均值得出的估算值吗?还是以编程方式汇总了该查询的内容?如果只是一种估计,我就不会依赖它,因为XML数据的分布可能不像简单的平均值所暗示的那样。
  3. XML压缩可能会有所帮助。您将如何在.NET中进行压缩?通过GZipStreamDeflateStream类?这不是零成本的选择。它肯定会很大程度地压缩某些数据,但由于每次都需要一个额外的过程来压缩/解压缩数据,因此它还需要更多的CPU。该计划还将完全消除您的能力:

    • 查询经由XML数据.nodes.value.query,和.modifyXML功能。
    • 索引XML数据。

      请记住(因为您提到XML是“高度冗余的”),XML数据类型已经过优化,因为它在字典中存储元素和属性名称,为每个项目分配一个整数索引ID,然后使用该整数ID整个文档中(因此,它不会在每次使用时都重复全名,也不会作为元素的结束标记再次重复此全名)。实际数据还删除了多余的空白。这就是为什么提取的XML文档不保留其原始结构的原因,以及为什么将空元素提取为原样<element />也是如此的原因<element></element>。因此,只能通过压缩元素和/或属性值来发现通过GZip(或其他方法)进行压缩所获得的任何收益,这是一个可以比大多数人预期的要小得多的表面积,并且很可能不值得失去上面直接指出的功能。

      还请记住,压缩XML数据并存储VARBINARY(MAX)结果不会消除LOB访问,只会减少LOB访问。根据行中其余数据的大小,压缩后的值可能适合行内,或者仍然需要LOB页。

这些信息虽然有用,但还远远不够。有许多因素会影响查询性能,因此我们需要更详细的了解情况。

我们不知道,但需要:

  1. 为什么要表现SELECT *物质?这是您在代码中使用的模式吗?如果是这样,为什么?
  2. 仅选择XML列的性能如何?如果执行以下操作,统计信息和时间安排是什么SELECT TOP 1000 XmlColumn FROM TABLE;
  3. 返回这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的不被访问一贯之间SELECTSELECT INTOSELECT @XmlVariable = XmlColumnSELECT @XmlVariable = XmlColumn.query(N'/'),和SELECT @NVarCharVariable = CONVERT(NVARCHAR(MAX), XmlColumn)。因此,我们的选择在这里受到更多限制,但这是可以做的:

    1. 通过在运行SQL Server的服务器(SSMS或SQLCMD.EXE)中执行查询来排除网络问题。
    2. 通过转到“查询选项”->“结果”->“网格”并选中“执行后丢弃结果”选项,可以排除SSMS中的客户端问题。请注意,此选项将阻止所有输出(包括消息),但是对于排除SSMS为每一行分配内存并在网格中绘制所需的时间仍然有用。
      或者,您可以通过SQLCMD.EXE执行查询,并通过以下命令将输出定向到无处-o NUL:
  4. 是否有与此查询关联的等待类型?如果是,那等待类型是什么?
  5. 返回的列的实际数据大小多少?如果“ TOP 1000”行包含总数据的不成比例的很大部分,则整个表中该列的平均大小并不重要。如果您想了解前1000行,请查看这些行。请运行以下命令:XMLXML

    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;
  6. 精确的表模式。请提供完整的 CREATE TABLE声明,包括所有索引。
  7. 查询计划?那是你可以张贴的东西吗?该信息可能不会改变任何东西,但是最好知道它不会比猜测它不会并且是错误的要好;-)
  8. 数据文件上是否存在物理/外部碎片?尽管这可能不是一个很大的因素,但是由于您使用的是“消费级SATA”,而不是SSD甚至是超级昂贵的SATA,因此,次优排序扇区的影响将更加明显,尤其是随着这些扇区数量的增加需要读取的内容增加了。
  9. 以下查询的确切结果是什么:

    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个。因此,我们再次需要有关实际表架构和实际数据的更多信息。



10

我是否认为LOB_DATA页可能导致扫描缓慢不仅是因为其大小,还因为SQL Server无法有效扫描聚集索引而引起的

是的,读取不在行中存储的LOB数据会导致随机IO而不是顺序IO。此处使用的磁盘性能度量标准是“随机读取IOPS”,以了解其速度快还是慢。

LOB数据以树结构存储,其中聚集索引中的数据页指向具有LOB根结构的LOB数据页,而LOB根结构又指向实际的LOB数据。当遍历聚集索引中的根节点时,SQL Server只能通过顺序读取来获取行内数据。为了获得LOB数据,SQL Server必须移至磁盘上的其他位置。

我猜想,如果您更改为SSD磁盘,那么您将不会遭受太大的损失,因为SSD的随机IOPS远高于旋转磁盘。

具有这样的表结构/数据模式是否合理?

是的,可能是。取决于此表为您执行的操作。

通常,当您想使用T-SQL查询XML时,SQL Server中的XML就会出现性能问题,甚至在您想在where子句或联接的谓词中使用XML中的值时,也会发生这种问题。如果是这种情况,您可以查看属性提升选择性的XML索引,或者重新设计将XML分解为表的表结构。

我尝试过压缩

十多年前,我曾在一款产品中做到过这一点,从那以后一直感到后悔。我真的很想念无法使用T-SQL处理数据,因此如果可以避免的话,我不建议任何人使用。


非常感谢您的回答。关于压缩:我不确定这种严格的反建议是否合理,因为从T-SQL实际查询该数据的需求显然取决于所存储数据的性质。就我而言,我决定暂时进行压缩。
Alexander Shelemin
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.