DATALENGTH的总和与sys.allocation_units中的表大小不匹配


11

我的印象是,如果我将DATALENGTH()一个表中所有记录的所有字段的总和求和,我将得出该表的总大小。我错了吗?

SELECT 
SUM(DATALENGTH(Field1)) + 
SUM(DATALENGTH(Field2)) + 
SUM(DATALENGTH(Field3)) TotalSizeInBytes
FROM SomeTable
WHERE X, Y, and Z are true

我在下面使用此查询(我从网上获得此查询来获取表大小,仅聚集索引,因此它不包含NC索引)来获取数据库中特定表的大小。出于计费目的(我们按部门使用的空间量向部门收取费用),我需要找出此表中每个部门使用的空间。我有一个查询,用于标识表中的每个组。我只需要弄清楚每个小组要占用多少空间。

由于VARCHAR(MAX)表中的字段,每行空间可能会波动很大,因此我不能只取平均大小*部门的行数比率。当我使用上述DATALENGTH()方法时,我只能获得下面查询中使用的总空间的85%。有什么想法吗?

SELECT 
s.Name AS SchemaName,
t.NAME AS TableName,
p.rows AS RowCounts,
(SUM(a.total_pages) * 8)/1024 AS TotalSpaceMB, 
(SUM(a.used_pages) * 8)/1024 AS UsedSpaceMB, 
((SUM(a.total_pages) - SUM(a.used_pages)) * 8)/1024 AS UnusedSpaceMB
FROM 
    sys.tables t with (nolock)
INNER JOIN 
    sys.schemas s with (nolock) ON s.schema_id = t.schema_id
INNER JOIN      
    sys.indexes i with (nolock) ON t.OBJECT_ID = i.object_id
INNER JOIN 
    sys.partitions p with (nolock) ON i.object_id = p.OBJECT_ID AND i.index_id = p.index_id
INNER JOIN 
    sys.allocation_units a with (nolock) ON p.partition_id = a.container_id
WHERE 
    t.is_ms_shipped = 0
    AND i.OBJECT_ID > 255 
    AND i.type_desc = 'Clustered'
GROUP BY 
    t.Name, s.Name, p.Rows
ORDER BY 
    TotalSpaceMB desc

建议我为每个部门创建一个过滤索引,或者对表进行分区,这样我就可以直接查询每个索引使用的空间。可以以编程方式创建过滤后的索引(然后在维护时段或需要执行定期账单时将其删除),而不是一直使用空间(在这方面分区会更好)。

我喜欢这个建议,通常会这样做。但是说实话,我以“每个部门”为例来解释为什么我需要这样做,但是说实话,那并不是真正的原因。由于机密性原因,我无法解释我为什么需要此数据的确切原因,但这类似于不同部门。

关于此表上的非聚集索引:如果我可以获取NC索引的大小,那就太好了。但是,NC索引占聚集索引大小的<1%,因此我们可以不包括这些索引。但是,无论如何我们将如何包含NC索引?我什至无法获得簇索引的准确大小:)


因此,从本质上讲,您有两个问题:(1)为什么行长度的总和与元数据对整个表大小的考虑不匹配?下面的答案至少部分解决了该问题(并且可能随版本和功能而波动,例如压缩,列存储等)。更重要的是:(2)如何准确确定每个部门使用的实际空间?我不知道您能准确地做到这一点-因为对于答案中所占的一些数据,无法分辨出它属于哪个部门。
亚伦·伯特兰

我认为问题不在于您没有准确的聚簇索引大小-元数据绝对可以准确地告诉您索引占用了多少空间。至少在给定当前设计/结构的情况下,元数据的设计目的不是告诉您-每个部门与多少数据相关联。
亚伦·伯特兰

Answers:


19

                          Please note that the following info is not intended to be a comprehensive
description of how data pages are laid out, such that one can calculate
the number of bytes used per any set of rows, as that is very complicated.

数据不是8k数据页上唯一占用空间的东西:

  • 有预留空间。您只允许使用8192字节中的8060(即从头开始从未使用过的132字节):

    • 页头:恰好是96个字节。
    • 插槽数组:每行2个字节,表示每行从页面开始的偏移量。该数组的大小不限于剩余的36个字节(132-96 = 36),否则您将被有效地限制为仅在数据页上最多放置18行。这意味着每一行比您认为的大2个字节。如所报告的DBCC PAGE,此值未包含在“记录大小”中,这就是为什么在此处将其保持单独而不是在以下每行信息中包含该值的原因。
    • 每行元数据(包括但不限于):
      • 大小取决于表定义(例如,列数,可变长度或固定长度等)。从@PaulWhite和@Aaron的评论中获取的信息可以在与该答案和测试有关讨论中找到。
      • 行标题:4个字节,其中2个字节表示记录类型,另外2个字节为NULL位图的偏移量
      • 列数:2个字节
      • NULL位图:当前是哪些列NULL。每8列每组1个字节。对于所有列,甚至是NOT NULL那些列。因此,最少1个字节。
      • 可变长度列偏移数组:最少4个字节。2个字节保存可变长度列的数量,然后每个可变长度列2个字节保存偏移量到它的起始位置。
      • 版本控制信息:14个字节(如果您的数据库设置为ALLOW_SNAPSHOT_ISOLATION ON或,则将存在READ_COMMITTED_SNAPSHOT ON)。
    • 有关更多详细信息,请参见以下问答:插槽阵列和总页面大小
    • 请参阅Paul Randall的以下博客文章,其中包含有关数据页面布局的一些有趣的详细信息:深入探讨DBCC PAGE(第1部分)
  • LOB指针用于未存储在行中的数据。这样就占了DATALENGTH+ pointer_size。但是这些不是标准尺寸。有关此复杂主题的详细信息,请参见以下博客文章:(MAX)类型(如Varchar,Varbinary,Etc)的LOB指针的大小是多少?。在该链接的帖子和我已经完成的一些其他测试之间,(默认)规则应如下所示:

    • 从SQL Server 2005开始TEXT,不再应使用的旧版/不推荐使用的LOB类型(NTEXT,和IMAGE):
      • 默认情况下,始终将其数据存储在LOB页上,并始终使用16字节的指针指向LOB存储。
      • 如果使用 sp_tableoption设置text in row选项,则:
        • 如果页面上有空间存储该值,并且该值不大于最大行内大小(可配置范围为24-7000字节,默认值为256),则它将存储在行内,
        • 否则它将是一个16字节的指针。
    • 在SQL Server 2005中引入的较新的LOB类型(VARCHAR(MAX)NVARCHAR(MAX),和VARBINARY(MAX)):
      • 默认情况下:
        • 如果该值不大于8000个字节,并且页面上有空间,则它将存储在行中。
        • 内联根(Inline Root)-对于空间介于8001到40,000(实际为42,000)字节之间的数据,在空间允许的情况下,IN ROW中将有1到5个指针(24-72字节)直接指向LOB页面。初始8k LOB页为24个字节,每增加8k页每增加12个字节,最多可再添加四个8k页。
        • TEXT_TREE —对于超过42,000字节的数据,或者如果1到5个指针不能容纳在行中,则指向LOB页面的指针列表的起始页面将只有24个字节的指针(即“ text_tree” “页面)。
      • 如果使用 sp_tableoption设置large value types out of row选项,则始终使用16字节的指针指向LOB存储。
    • 我说:“默认”规则,因为我并没有反对的某些功能,如数据压缩,列级加密,透明数据加密,始终处于加密等冲击试验中,行值
  • LOB溢出页:如果值是10k,则将需要1个完整的8k页溢出,然后是第二页的一部分。如果没有其他数据可以占用剩余空间(我不确定该规则是否允许),那么在第二个LOB溢出数据页上大约有6kb的“浪费”空间。

  • 未使用的空间:8k数据页就是:8192字节。它的大小没有变化。但是,放置在其上的数据和元数据并不总是很好地适合所有8192字节。行不能拆分为多个数据页。因此,如果您剩余100字节,但没有行(或者没有行适合该位置,具体取决于多个因素),那么数据页仍会占用8192字节,并且您的第二个查询仅计算数据页。您可以在两个位置找到此值(请记住,此值的某些部分是该保留空间的一部分):

    • DBCC PAGE( db_name, file_id, page_id ) WITH TABLERESULTS;查找ParentObject=“ PAGE HEADER:”和Field=“ m_freeCnt”。该Value字段是未使用的字节数。
    • SELECT buff.free_space_in_bytes FROM sys.dm_os_buffer_descriptors buff WHERE buff.[database_id] = DB_ID(N'db_name') AND buff.[page_id] = page_id;该值与“ m_freeCnt”报告的值相同。这比DBCC容易,因为它可以获得很多页面,而且还需要首先将页面读入缓冲池。
  • 保留空间FILLFACTOR小于100。新创建的页面不遵守该FILLFACTOR设置,但是执行REBUILD将在每个数据页面上保留该空间。保留空间背后的想法是,由于可变长度的列将使用稍微更多的数据进行更新(但不足以导致出现错误的情况),因此非连续插入和/或更新将使用它们来扩展页面上行的大小。页面拆分)。但是,您可以轻松地在数据页面上保留空间,这些数据页面自然不会获得新行,也不会更新现有行,或者至少不会以增加行大小的方式进行更新。

  • 分页(碎片):需要在没有空间容纳行的位置添加行将导致分页。在这种情况下,大约50%的现有数据将移至新页面,并将新行添加到2个页面之一。但是您现在有更多的可用空间,这些空间无法通过DATALENGTH计算来解决。

  • 标记为删除的行。当您删除行时,它们并不总是立即从数据页中删除。如果不能立即将其删除,则会将它们“标记为已死亡”(Steven Segal参考),稍后将通过幽灵清理过程将其物理删除(我相信这是名称)。但是,这些可能与该特定问题无关。

  • 鬼页?不知道这是否是正确的术语,但是有时直到完成“聚集索引”的重建后,数据页才会被删除。那也将占到页面DATALENGTH总数之和。通常这应该不会发生,但是几年前我已经遇到过一次。

  • SPARSE列:稀疏列可节省表中的空间(主要用于固定长度的数据类型),在该表中,很大百分比的行NULL用于一列或多列。该SPARSE选项使NULL值类型最多占用0个字节(而不是正常的固定长度数量,例如,为4个字节INT),但是,非NULL值每个都为固定长度类型占用额外的4个字节,而为可变长度类型。这里的问题是,DATALENGTH在SPARSE列中不包含用于非NULL值的额外4个字节,因此需要重新添加这4个字节。您可以通过以下方法检查是否有任何SPARSE列:

    SELECT OBJECT_SCHEMA_NAME(sc.[object_id]) AS [SchemaName],
           OBJECT_NAME(sc.[object_id]) AS [TableName],
           sc.name AS [ColumnName]
    FROM   sys.columns sc
    WHERE  sc.is_sparse = 1;
    

    然后针对每一SPARSE列,更新原始查询以使用:

    SUM(DATALENGTH(FieldN) + 4)

    请注意,以上添加标准4字节的计算有点简单,因为它仅适用于定长类型。而且,每行有额外的元数据(据我所知,这已经到了),只需具有至少一个SPARSE列即可减少可用于数据的空间。有关更多详细信息,请参见MSDN页面上的Use Sparse Columns

  • 索引页面和其他页面(例如IAM,PFS,GAM,SGAM等):就用户数据而言,这些不是“数据”页面。这些将增加表格的总大小。如果使用SQL Server 2012或更高版本,则可以使用sys.dm_db_database_page_allocations动态管理功能(DMF)来查看页面类型(SQL Server的早期版本可以使用DBCC IND(0, N'dbo.table_name', 0);):

    SELECT *
    FROM   sys.dm_db_database_page_allocations(
                   DB_ID(),
                   OBJECT_ID(N'dbo.table_name'),
                   1,
                   NULL,
                   N'DETAILED'
                  )
    WHERE  page_type = 1; -- DATA_PAGE
    

    无论是DBCC INDsys.dm_db_database_page_allocations(与WHERE子句)将报告任何索引页,只有DBCC IND将报告至少一个IAM页。

  • DATA_COMPRESSION:如果在“聚集索引”或“堆”上启用了“压缩” ROW或“ PAGE压缩”,那么您可以忘记到目前为止提到的大多数内容。96字节的页页眉,每行2字节的插槽数组和每行14字节的版本控制信息仍然存在,但是数据的物理表示变得非常复杂(比“压缩”时已经提到的要复杂得多)未使用)。例如,对于行压缩,SQL Server尝试使用最小的容器来容纳每一行的每一列。因此,如果您有一BIGINT栏否则SPARSE将占用8个字节(假设也未启用),如果该值在-128到127之间(即带符号的8位整数),则它将仅使用1个字节,并且如果价值可能适合SMALLINT,它将仅占用2个字节。NULL0不占用空间的整数类型,在映射出列的数组中简单地表示为“是” NULL或“空”(即0)。还有很多其他规则。使用Unicode数据(NCHARNVARCHAR(1 - 4000),但没有 NVARCHAR(MAX),即使储存在行)?在SQL Server 2008 R2中添加了Unicode压缩,但是鉴于规则的复杂性,如果不进行实际压缩,就无法在所有情况下预测“压缩”值的结果。

因此,实际上,您的第二个查询虽然在磁盘上占用的总物理空间方面更为准确,但仅在执行REBUILD“聚集索引” 时才真正准确。之后,您仍然需要考虑FILLFACTOR低于100的任何设置。即使如此,总会有页面标题,并且经常有足够的“浪费”空间,这些空间由于太小而无法填充其中的任何行表,或至少在逻辑上应放入该插槽的行。

关于第二个查询在确定“数据使用量”方面的准确性,回退页面头字节似乎是最公平的,因为它们不是数据使用量:它们是业务成本的开销。如果数据页上有1行,而该行只是a TINYINT,则该1字节仍需要数据页存在,因此需要标头的96个字节。该1个部门应该为整个数据页面付费吗?如果该数据页随后被部门2填满,他们是否会平均分配“开销”成本或按比例支付?似乎最容易将其退出。在这种情况下,使用值8乘以number of pages太高。怎么样:

-- 8192 byte data page - 96 byte header = 8096 (approx) usable bytes.
SELECT 8060.0 / 1024 -- 7.906250

因此,使用类似:

(SUM(a.total_pages) * 7.91) / 1024 AS [TotalSpaceMB]

针对“ number_of_pages”列的所有计算。

AND,考虑到使用DATALENGTH每个字段不能返回每行元数据,应该将其添加到每表查询中,以获取DATALENGTH每个字段,并在每个“部门”进行过滤:

  • 记录类型和偏移量为NULL位图:4个字节
  • 列数:2个字节
  • 插槽阵列:2个字节(“记录大小”中不包括,但仍需考虑)
  • NULL位图:每8列1个字节(对于所有列)
  • 行版本控制:14个字节(如果数据库具有ALLOW_SNAPSHOT_ISOLATIONREAD_COMMITTED_SNAPSHOT设置为ON
  • 可变长度列偏移数组:如果所有列都是固定长度,则为0个字节。如果有任何列为可变长度,则为2个字节,每个可变长度列加2个字节。
  • LOB指针:这部分非常不精确,因为如果值为NULL,则不会有指针;如果该值适合行,则它可以比指针小得多或大得多,并且该值存储在以下位置:行,那么指针的大小可能取决于有多少数据。但是,由于我们只想要一个估计值(即“ swag”),因此似乎可以使用24个字节的值(很好,和其他任何;-一样)。这是每个MAX字段。

因此,使用类似:

  • 通常(行标题+列数+插槽数组+ NULL位图):

    ([RowCount] * (( 4 + 2 + 2 + (1 + (({NumColumns} - 1) / 8) ))
  • 通常(自动检测是否存在“版本信息”):

    + (SELECT CASE WHEN snapshot_isolation_state = 1 OR is_read_committed_snapshot_on = 1
                     THEN 14 ELSE 0 END FROM sys.databases WHERE [database_id] = DB_ID())
    
  • 如果有任何变长列,则添加:

    + 2 + (2 * {NumVariableLengthColumns})
  • 如果有MAX/ LOB列,则添加:

    + (24 * {NumLobColumns})
  • 一般来说:

    )) AS [MetaDataBytes]

这是不正确的,如果您在堆索引或聚簇索引上启用了行或页面压缩,则再次无法使用,但绝对可以使您更接近。


关于差异15%之谜的更新

我们(包括我本人)非常专注于思考数据页面的布局方式以及如何DATALENGTH解释这些问题,因此我们没有花很多时间来审查第二个查询。我针对单个表运行了该查询,然后将这些值与报告的值进行了比较sys.dm_db_database_page_allocations,它们的页数值不同。凭直觉,我删除了聚合函数和GROUP BY,并将SELECT列表替换为a.*, '---' AS [---], p.*。并且然后它变得清晰:其中在这些阴暗的interwebs他们得到他们的信息和脚本的人一定要小心;-)。问题中发布的第二个查询并不完全正确,尤其是对于该特定问题。

  • 次要问题:它不造成太大的意义之外GROUP BY rows(而不是在一个聚合函数列)之间的JOIN sys.allocation_unitssys.partitions不技术上是正确的。分配单元有3种类型,其中一种应加入另一个字段。经常partition_id并且hobt_id相同,因此可能永远不会有问题,但是有时这两个字段的值确实不同。

  • 主要问题:查询使用used_pages字段。该字段涵盖所有类型的页面:数据,索引,IAM等,tc。仅关注实际数据时,还有一个更合适的字段可以使用:data_pages

考虑到以上各项,我改编了Question中的第二个查询,并使用了回退页眉的数据页大小。我也删除2 JOIN的这是不必要的:sys.schemas(与调用替换SCHEMA_NAME()),和sys.indexes(聚集索引总是index_id = 1与我们index_idsys.partitions)。

SELECT  SCHEMA_NAME(st.[schema_id]) AS [SchemaName],
        st.[name] AS [TableName],
        SUM(sp.[rows]) AS [RowCount],
        (SUM(sau.[total_pages]) * 8.0) / 1024 AS [TotalSpaceMB],
        (SUM(CASE sau.[type]
           WHEN 1 THEN sau.[data_pages]
           ELSE (sau.[used_pages] - 1) -- back out the IAM page
         END) * 7.91) / 1024 AS [TotalActualDataMB]
FROM        sys.tables st
INNER JOIN  sys.partitions sp
        ON  sp.[object_id] = st.[object_id]
INNER JOIN  sys.allocation_units sau
        ON  (   sau.[type] = 1
            AND sau.[container_id] = sp.[partition_id]) -- IN_ROW_DATA
        OR  (   sau.[type] = 2
            AND sau.[container_id] = sp.[hobt_id]) -- LOB_DATA
        OR  (   sau.[type] = 3
            AND sau.[container_id] = sp.[partition_id]) -- ROW_OVERFLOW_DATA
WHERE       st.is_ms_shipped = 0
--AND         sp.[object_id] = OBJECT_ID(N'dbo.table_name')
AND         sp.[index_id] < 2 -- 1 = Clustered Index; 0 = Heap
GROUP BY    SCHEMA_NAME(st.[schema_id]), st.[name]
ORDER BY    [TotalSpaceMB] DESC;

评论不作进一步讨论;此对话已转移至聊天
保罗怀特9

尽管您为第二个查询提供的更新查询距离更远(现在朝另一个方向:)),但是我对这个答案还可以。显然,这是一个很难克服的难题,而且值得,即使有专家帮助我,我仍然无法弄清楚这两种方法不匹配的确切原因,我感到很高兴。我将仅在另一个答案中使用该方法进行推断。我希望我对这两个答案都可以投赞成票,但是@srutzky协助了所有这两个答案都不成立的原因。
克里斯·伍兹

6

也许这是一个糟糕的答案,但这就是我会做的。

因此,DATALENGTH仅占总数的86%。它仍然是非常有代表性的分裂。srutzky给出的出色答案中的开销应该平均分配。

我将使用您的第二个查询(页面)作为总数。并使用第一个(数据长度)分配拆分。许多成本是使用规范化分配的。

而且,您必须考虑一个更严密的答案,这将增加成本,因此,即使因拆分而损失的部门仍可能会支付更高的费用。

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.