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バイトです。
- スロット配列:これは1行あたり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
)。
- 詳細については、次の質問と回答を参照してください:スロット配列と合計ページサイズ
- データページがどのようにレイアウトされるかについてのいくつかの興味深い詳細があるポールランドールからの次のブログ投稿を参照してください。
行に格納されていないデータのLOBポインター。したがって、DATALENGTH
+ pointer_size が考慮されます。しかし、これらは標準サイズではありません。この複雑なトピックの詳細については、次のブログ投稿を参照してください:Varchar、Varbinaryなどの(MAX)タイプのLOBポインターのサイズは?。そのリンクされた投稿と私が行った追加のテストの間で、(デフォルト)ルールは次のようになります。
- SQL Server 2005
TEXT
以降NTEXT
、誰も使用してはならないレガシー/非推奨のLOBタイプ(、、およびIMAGE
):
- デフォルトでは、常にデータをLOBページに格納し、常にLOBストレージへの16バイトのポインターを使用します。
- IF 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)バイトのデータの場合、スペースが許せば、LOBページを直接指す1〜5個のポインター(24〜72バイト)のIN ROWがあります。最初の8k LOBページ用に24バイト、さらに4つまでの8kページ用に追加の8kページごとに12バイト。
- TEXT_TREE — 42,000バイトを超えるデータの場合、または1〜5個のポインターが行内に収まらない場合、LOBページへのポインターのリストの開始ページへの24バイトポインターのみ(つまり、「text_tree」 "ページ)。
- IF sp_tableoptionを設定するために使用された
large value types out of row
オプションを、そして常にLOBストレージへの16バイトのポインタを使用します。
- 私がしたので、私は「デフォルト」のルールを言っていない、そのようなデータ圧縮などの特定の機能への影響に対するテストで行の値、列レベルの暗号化などを透過的データ暗号化の、常に暗号化され、
LOBオーバーフローページ:値が10kの場合、1つの完全な8kページのオーバーフローが必要で、次に2ページ目の一部が必要になります。他のデータが残りのスペースを占有できない場合(または許可されている場合でも、そのルールは不明です)、その2番目のLOBオーバーフローデータページに約6kbの「無駄な」スペースがあります。
未使用スペース:8kデータページは8192バイトです。サイズは変わりません。ただし、その上に配置されたデータとメタデータは、すべての8192バイトにうまく収まるとは限りません。また、行を複数のデータページに分割することはできません。したがって、残りの100バイトはあるが行が収まらない(またはいくつかの要因によってはその場所に収まる行がない)場合、データページはまだ8192バイトを占めており、2番目のクエリはデータページ。この値は2か所にあります(この値の一部がその予約済みスペースの一部であることを覚えておいてください)。
DBCC PAGE( db_name, file_id, page_id ) WITH TABLERESULTS;
以下のためのルックParentObject
と:=「ページヘッダ」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
が1つ以上の列に対応しているテーブルで(主に固定長データ型の)スペースを節約します。SPARSE
オプションはせるNULL
(例えば4つのバイトとして代わりに通常の固定長の量の、0バイトの最大値型を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バイトを追加する計算は、固定長の型でのみ機能するため、少し単純化されていることに注意してください。さらに、行ごとに追加のメタデータがあり(これまでに私が知ることができます)、少なくとも1つの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
どちらDBCC IND
もsys.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バイトしかかかりません。どちらかである整数型NULL
または0
領域を消費しないと、単純であることが示されNULL
(すなわち「空」または0
列のうちの配列マッピングで)。そして、他にもたくさんのルールがあります。有するUnicodeデータ(NCHAR
、NVARCHAR(1 - 4000)
が、ではない NVARCHAR(MAX)
、で行格納されている場合であっても)?Unicode圧縮はSQL Server 2008 R2で追加されましたが、ルールの複雑さを考慮して実際の圧縮を行わずにすべての状況で「圧縮」値の結果を予測する方法はありません。
したがって、実際には、2番目のクエリは、ディスクで使用される物理領域全体の点ではより正確REBUILD
ですが、クラスター化インデックスを実行した場合にのみ正確になります。そしてその後も、FILLFACTOR
100未満の設定はすべて考慮する必要があります。それでも、常にページヘッダーがあり、小さすぎてこの行に収まらないため単に埋めることができない「無駄な」スペースが十分にあることがよくあります。テーブル、または少なくとも論理的にそのスロットに入れる必要がある行。
「データ使用量」を決定する際の2番目のクエリの精度については、データ使用量ではないため、ページヘッダーバイトをバックアウトするのが最も公平であると考えられます。これらはビジネスコストのオーバーヘッドです。データページに1つの行があり、その行が単なるである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」列に対するすべての計算。
さらに、DATALENGTH
各フィールドごとに使用すると行ごとのメタデータを返すことができないことを考慮して、テーブルごとのクエリに追加して、DATALENGTH
各フィールドごとに取得し、各「部門」でフィルタリングする必要があります。
- レコードタイプとNULLビットマップへのオフセット:4バイト
- 列数:2バイト
- スロット配列:2バイト(「レコードサイズ」には含まれませんが、それでも考慮する必要があります)
- NULLビットマップ:8列ごとに1バイト(すべての列)
- 行のバージョン管理:14バイト(データベースに
ALLOW_SNAPSHOT_ISOLATION
またはにREAD_COMMITTED_SNAPSHOT
設定されている場合ON
)
- 可変長列オフセット配列:すべての列が固定長の場合は0バイト。可変長の列がある場合は、2バイトに加えて、可変長列のみのそれぞれに2バイト。
- LOBポインター:値が
NULL
である場合はポインターが存在しないため、この部分は非常に不正確です。値が行に収まる場合は、ポインターよりもはるかに小さいまたは非常に大きく、値がオフに格納されている場合は、行の場合、ポインタのサイズは、そこにあるデータの量によって異なる場合があります。ただし、見積もり(つまり、「盗品」)が必要なだけなので、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
、2番目のクエリを確認するのにあまり時間をかけなかったものをどのように説明できるかを考えることに集中していました。そのクエリを単一のテーブルに対して実行してから、それらの値をレポート対象の値と比較しましたがsys.dm_db_database_page_allocations
、ページ数の値は同じではありませんでした。直感的に、集約関数andを削除しGROUP BY
、SELECT
リストをに置き換えましたa.*, '---' AS [---], p.*
。そして、それが明らかになりました。人々はこれらのあいまいなインターウェブのどこで情報やスクリプトを;-)から取得するかに注意する必要があります。質問に投稿された2番目のクエリは、特にこの特定の質問については、正確ではありません。
マイナーな問題:それがあまり意味をしていない外部のGROUP BY rows
間の結合(および集約関数でそのカラムを持っていない)sys.allocation_units
とsys.partitions
技術的に正しくありません。アロケーションユニットには3つのタイプがあり、そのうちの1つは別のフィールドにJOINする必要があります。かなり頻繁にpartition_id
とhobt_id
同じなので、問題ありませんかもしれませんが、時にはこれらの2つのフィールドは異なる値を持っています。
主な問題:クエリはused_pages
フィールドを使用します。このフィールドは、データ、インデックス、IAMなどのすべてのタイプのページをカバーします。実際のデータのみに関連する場合に使用する、より適切なフィールドがもう1つありますdata_pages
。
上記の項目を念頭に置き、ページヘッダーをバックアウトするデータページサイズを使用して、質問の2番目のクエリを調整しました。:私は、2つのことが不要だったJOINを削除sys.schemas
(への呼び出しに置き換えSCHEMA_NAME()
)、およびsys.indexes
(クラスタ化インデックスが常にありindex_id = 1
、私たちが持っているindex_id
中でsys.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;