我的印象是,如果我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 索引?我什至无法获得聚集索引的准确大小:)
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 个字节一开始就不是您的):
DBCC PAGE
,这就是为什么它在此处单独保存,而不是包含在下面的每行信息中。NULL
。每组 8 列 1 个字节。对于所有列,甚至是NOT NULL
那些列。因此,最少 1 个字节。ALLOW_SNAPSHOT_ISOLATION ON
版本信息:14 字节(如果您的数据库设置为或,则会出现此信息READ_COMMITTED_SNAPSHOT ON
)。未存储在行中的数据的 LOB 指针。所以这将占
DATALENGTH
+pointer_size。但这些都不是标准尺寸。有关此复杂主题的详细信息,请参阅以下博客文章:(MAX) 类型(如 Varchar、Varbinary 等)的 LOB 指针的大小是多少?. 在链接的帖子和我已经完成的一些额外测试之间,(默认)规则应如下所示:TEXT
、NTEXT
和IMAGE
):text in row
选项,则:VARCHAR(MAX)
、NVARCHAR(MAX)
和VARBINARY(MAX)
):large value types out of row
选项,则始终使用指向 LOB 存储的 16 字节指针。LOB 溢出页:如果值为 10k,则需要 1 个完整的 8k 页溢出,然后是第 2 页的一部分。如果没有其他数据可以占用剩余空间(或者甚至被允许,我不确定该规则),那么您在第二个 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 更容易,因为它可以获取许多页面,但也需要首先将页面读入缓冲池。< 100保留的空间
FILLFACTOR
。新创建的页面不遵守FILLFACTOR
设置,但执行 REBUILD 将在每个数据页面上保留该空间。保留空间背后的想法是,它将被非顺序插入和/或更新使用,这些插入和/或更新已经扩展了页面上的行大小,因为可变长度列被更新的数据略多(但不足以导致分页)。但是您可以轻松地在数据页上保留空间,这些空间自然不会获得新行,也不会更新现有行,或者至少不会以会增加行大小的方式进行更新。Page-Splits(碎片化):需要将行添加到没有空间容纳该行的位置会导致页面拆分。在这种情况下,大约 50% 的现有数据被移动到新页面,新行被添加到 2 个页面之一。但是您现在有更多的可用空间,
DATALENGTH
计算中没有考虑到这些空间。标记为删除的行。当您删除行时,它们并不总是立即从数据页中删除。如果不能立即删除它们,它们将被“标记为死亡”(Steven Segal 参考),稍后将通过幽灵清理过程物理删除(我相信这就是名称)。但是,这些可能与该特定问题无关。
鬼页?不确定这是否是正确的术语,但有时数据页在聚集索引的重建完成之前不会被删除。这也
DATALENGTH
将占比加起来更多的页面。这通常不应该发生,但几年前我遇到过一次。稀疏列:稀疏列在大部分行
NULL
用于一列或多列的表中节省空间(主要用于固定长度数据类型)。该SPARSE
选项使NULL
值类型增加 0 个字节(而不是正常的固定长度量,例如 4 字节INT
),但是,非 NULL 值每个固定长度类型占用额外的 4 个字节,可变数量变长类型。这里的问题是DATALENGTH
不包括 SPARSE 列中非 NULL 值的额外 4 个字节,因此需要重新添加这 4 个字节。您可以SPARSE
通过以下方式检查是否有任何列:然后对于每一
SPARSE
列,更新原始查询以使用:请注意,上面添加标准 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);
):nor (带有那个 WHERE 子句)都
DBCC IND
不会sys.dm_db_database_page_allocations
报告任何索引页面,只有DBCC IND
会报告至少一个 IAM 页面。DATA_COMPRESSION:如果您在聚集索引或堆上启用
ROW
或PAGE
压缩,那么您可以忘记到目前为止提到的大部分内容。96 字节的页头、每行 2 字节的插槽阵列和每行 14 字节的版本信息仍然存在,但数据的物理表示变得非常复杂(比压缩时已经提到的要复杂得多)没有被使用)。例如,使用行压缩,SQL Server 尝试使用尽可能小的容器来适应每一列、每一行。所以如果你有一个BIGINT
列,否则(假设SPARSE
也没有启用)总是占用 8 个字节,如果值在 -128 和 127 之间(即有符号的 8 位整数),那么它将只使用 1 个字节,如果值可能适合SMALLINT
, 它只占用 2 个字节。整数类型要么是要么NULL
不0
占用空间,并且在映射列的数组中简单地表示为是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
太高。怎么样:因此,使用类似的东西:
针对“number_of_pages”列的所有计算。
AND,考虑到使用
DATALENGTH
每个字段不能返回每行元数据,应将其添加到每个表查询中,在其中获取DATALENGTH
每个字段,过滤每个“部门”:ALLOW_SNAPSHOT_ISOLATION
或READ_COMMITTED_SNAPSHOT
设置为ON
)NULL
,如果该值适合该行,则它可以比指针小得多或大得多,并且如果该值存储在外-行,那么指针的大小可能取决于有多少数据。但是,由于我们只需要一个估计值(即“swag”),因此 24 字节似乎是一个很好的使用值(嗯,和其他任何东西一样好;-)。这是每个MAX
字段。因此,使用类似的东西:
通常(行标题 + 列数 + 槽数组 + NULL 位图):
一般来说(自动检测是否存在“版本信息”):
如果有任何可变长度列,则添加:
如果有任何
MAX
/ LOB 列,则添加:一般来说:
这并不准确,如果您在堆或聚集索引上启用了行或页面压缩,这将再次不起作用,但绝对应该让您更接近。
关于 15% 差异之谜的更新
我们(包括我自己)非常专注于思考数据页面的布局以及如何
DATALENGTH
解释我们没有花很多时间查看第二个查询的事情。我针对单个表运行该查询,然后将这些值与报告的值进行比较sys.dm_db_database_page_allocations
,它们与页数的值不同。凭直觉,我删除了聚合函数 和GROUP BY
,并将SELECT
列表替换为a.*, '---' AS [---], p.*
。然后就很清楚了:人们必须小心在这些模糊的互联网上他们从哪里获取信息和脚本;-)。问题中发布的第二个查询并不完全正确,尤其是对于这个特定的问题。小问题:除此之外
GROUP BY rows
(并且在聚合函数中没有该列)没有多大意义,之间的 JOIN 在sys.allocation_units
技术上sys.partitions
并不正确。分配单元有 3 种类型,其中一种应该 JOIN 到不同的字段。很多时候partition_id
都是hobt_id
一样的,所以可能永远不会有问题,但有时这两个字段确实有不同的值。主要问题:查询使用该
used_pages
字段。该字段涵盖所有类型的页面:数据、索引、IAM 等。当只关注实际数据时,还有另一个更合适的字段:data_pages
.我在考虑到上述项目的情况下调整了问题中的第二个查询,并使用了支持页面标题的数据页面大小。我还删除了两个不必要的 JOIN:(
sys.schemas
替换为对 的调用SCHEMA_NAME()
)和sys.indexes
(聚集索引始终是index_id = 1
并且我们index_id
在 中sys.partitions
)。也许这是一个垃圾答案,但这是我会做的。
所以DATALENGTH只占总数的86%。还是很有代表性的分裂。来自 srutzky 的优秀答案的开销应该相当均匀。
我将使用您的第二个查询(页面)作为总数。并使用第一个(数据长度)来分配拆分。许多成本是使用标准化分配的。
而且你必须考虑一个更接近的答案会增加成本,所以即使是在拆分中失败的部门也可能会支付更多。