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

Chr*_*ods 11 sql-server data-pages clustered-index metadata database-internals

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

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

我在下面使用了这个查询(我从网上得到的表大小,聚集索引,所以它不包括 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
Run Code Online (Sandbox Code Playgroud)

有人建议我为每个部门创建一个过滤索引或对表进行分区,这样我就可以直接查询每个索引使用的空间。可以以编程方式创建过滤索引(并在维护窗口期间或需要执行定期计费时再次删除),而不是一直使用空间(在这方面分区会更好)。

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

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

Sol*_*zky 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 个表示记录类型,另外两个是 NULL Bitmap 的偏移量
      • 列数:2字节
      • NULL 位图:当前是哪些列NULL。每组 8 列 1 个字节。对于所有列,甚至是NOT NULL那些列。因此,最小 1 个字节。
      • 可变长度列偏移数组:最少 4 个字节。2 个字节用于保存可变长度列的数量,然后每个可变长度列 2 个字节用于保存其开始位置的偏移量。
      • 版本信息:14 字节(如果您的数据库设置为ALLOW_SNAPSHOT_ISOLATION ON或 ,则会出现此信息READ_COMMITTED_SNAPSHOT ON)。
    • 有关更多详细信息,请参阅以下问答:Slot Array and Total Page Size
    • 请参阅 Paul Randall 的以下博客文章,其中包含有关数据页面布局方式的几个有趣细节:Poking about DBCC PAGE(第 1 部分?)
  • 未存储在行中的数据的 LOB 指针。所以这将解释DATALENGTH+pointer_size。但这些都不是标准尺寸。有关此复杂主题的详细信息,请参阅以下博客文章:Varchar、Varbinary 等 (MAX) 类型的 LOB 指针的大小是多少?. 在该链接帖子和我所做的一些其他测试之间,(默认)规则应如下所示:

    • 旧/过时LOB类型,没有人应该被使用了,因为SQL Server 2005中的(TEXTNTEXT,和IMAGE):
      • 默认情况下,始终将其数据存储在 LOB 页上,并始终使用 16 字节的 LOB 存储指针。
      • 如果 sp_tableoption用于设置text in row选项,则:
        • 如果页面上有空间存储值,并且值不大于最大行内大小(可配置范围为 24 - 7000 字节,默认为 256),则将其存储在行内,
        • 否则它将是一个 16 字节的指针。
    • 在SQL Server 2005中引入的较新的LOB类型(VARCHAR(MAX)NVARCHAR(MAX),和VARBINARY(MAX)):
      • 默认情况下:
        • 如果该值不大于 8000 字节,并且页面上有空间,则将其存储在行中。
        • 内联根 - 对于 8001 到 40,000(实际上是 42,000)字节之间的数据,如果空间允许,将有 1 到 5 个指针(24 - 72 字节)IN ROW 直接指向 LOB 页。初始 8k LOB 页为 24 个字节,每个额外的 8k 页为 12 个字节,最多四个 8k 页。
        • TEXT_TREE — 对于超过 42,000 字节的数据,或者如果 1 到 5 个指针不能放在行中,那么将只有一个 24 字节的指针指向 LOB 页的指针列表的起始页(即“text_tree “ 页)。
      • 如果使用 sp_tableoption设置large value types out of row选项,则始终使用指向 LOB 存储的 16 字节指针。
    • 我说:“默认”规则,因为我并没有反对的某些功能,如数据压缩,列级加密,透明数据加密,始终处于加密等冲击试验中,行值
  • 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 将在每个数据页面上保留该空间。保留空间背后的想法是它将被非顺序插入和/或更新使用,这些插入和/或更新已经扩展了页面上的行大小,因为可变长度列被更新为稍微更多的数据(但不足以导致分页)。但是您可以轻松地在数据页上保留空间,这些数据页自然不会获得新行,也不会更新现有行,或者至少不会以增加行大小的方式进行更新。

  • Page-Splits(分页):需要在没有空间容纳该行的位置添加一行会导致分页。在这种情况下,大约 50% 的现有数据将移动到新页面,并将新行添加到 2 个页面之一。但是您现在有更多的可用空间,这些空间未被DATALENGTH计算考虑在内。

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

  • 鬼页?不确定这是否是正确的术语,但有时在完成聚集索引的重建之前不会删除数据页。这也将导致比DATALENGTH加起来更多的页面。这通常不应该发生,但几年前我遇到过一次。

  • 稀疏列:稀疏列在表中节省空间(主要用于固定长度的数据类型),其中大部分行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;
    
    Run Code Online (Sandbox Code Playgroud)

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

    SUM(DATALENGTH(FieldN) + 4)
    
    Run Code Online (Sandbox Code Playgroud)

    请注意,上面添加标准 4 字节的计算有点简单,因为它仅适用于固定长度类型。而且,每行都有额外的元数据(据我目前所知)减少了可用于数据的空间,只需至少有一个 SPARSE 列即可。有关更多详细信息,请参阅使用稀疏列的 MSDN 页面。

  • 索引和其他(例如 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
    
    Run Code Online (Sandbox Code Playgroud)

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

  • DATA_COMPRESSION:如果你有ROWPAGE聚集索引或堆启用压缩,那么你可以对什么最到目前为止已经提到的忘记。96 字节的页头、每行 2 字节的槽阵列和每行 14 字节的版本信息仍然存在,但数据的物理表示变得非常复杂(比压缩时已经提到的要复杂得多没有被使用)。例如,对于行压缩,SQL Server 尝试使用尽可能小的容器来适应每一列、每一行。因此,如果您有一个BIGINT列,否则(假设SPARSE也未启用)总是占用 8 个字节,如果值在 -128 和 127 之间(即有符号的 8 位整数),那么它将只使用 1 个字节,如果价值可以适合SMALLINT,它只会占用 2 个字节。在映射出列的数组中不占用NULL0不占用空间并且简单地表示为NULL或“空”(即0)的整数类型。还有很多很多其他的规则。有 Unicode 数据(NCHAR, NVARCHAR(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
Run Code Online (Sandbox Code Playgroud)

因此,使用类似的东西:

(SUM(a.total_pages) * 7.91) / 1024 AS [TotalSpaceMB]
Run Code Online (Sandbox Code Playgroud)

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

AND,考虑到使用DATALENGTHper each field 不能返回 per-row 元数据,应该将其添加到您的 per-table 查询中,您可以获得DATALENGTH每个字段,过滤每个“部门”:

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

因此,使用类似的东西:

这并不准确,如果您在堆或聚集索引上启用了行或页压缩,则同样不起作用,但肯定会让您更接近。


关于 15% 差异之谜的更新

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

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

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

考虑到上述项目,我调整了问题中的第二个查询,并使用支持页眉的数据页大小。我还删除了两个不必要的 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;
Run Code Online (Sandbox Code Playgroud)


pap*_*zzo 6

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

所以DATALENGTH只占总数的86%。它仍然是非常有代表性的分裂。srutzky 的优秀答案中的开销应该相当均匀。

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

而且您必须考虑更接近的答案会增加成本,因此即使是在拆分中失败的部门也可能会支付更多费用。