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インデックスは含まない)、データベース内の特定のテーブルのサイズを取得しました。請求のために(部門に使用するスペースの量に応じて部門に請求します)、この表で各部門が使用したスペースの量を把握する必要があります。テーブル内の各グループを識別するクエリがあります。各グループがどれだけのスペースを使用しているかを知る必要があります。

1行あたりのスペース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インデックスをどのように含めますか?クラスタ化インデックスの正確なサイズを取得することもできません:)


つまり、本質的に2つの質問があります。(1)行の長さの合計がメタデータのテーブル全体のサイズの計算と一致しないのはなぜですか。以下の回答は、少なくとも部分的に対処しています(そして、これは、リリースごとに、たとえば圧縮、列ストアなどの機能ごとに変動する可能性があります)。さらに重要なことは、(2)部門ごとに実際に使用されているスペースをどのように正確に判断できるでしょうか。あなたがそれを正確に行うことができるかわかりません-回答で説明されているデータの一部については、それがどの部門に属しているかを知る方法がありません。
アーロンバートランド

問題はクラスタ化インデックスの正確なサイズがないことではないと思います。メタデータは、インデックスが占めるスペースの量を正確に伝えます。メタデータがあなたに伝えるために設計されていないもの-少なくとも現在の設計/構造を考えると-各部門に関連付けられているデータの量。
アーロンバートランド

回答:


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バイトです。
    • スロット配列:これは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 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バイトしかかかりません。どちらかである整数型NULLまたは0領域を消費しないと、単純であることが示されNULL(すなわち「空」または0列のうちの配列マッピングで)。そして、他にもたくさんのルールがあります。有するUnicodeデータ(NCHARNVARCHAR(1 - 4000)が、ではない NVARCHAR(MAX)、で行格納されている場合であっても)?Unicode圧縮はSQL Server 2008 R2で追加されましたが、ルールの複雑さを考慮して実際の圧縮を行わずにすべての状況で「圧縮」値の結果を予測する方法はありません。

したがって、実際には、2番目のクエリは、ディスクで使用される物理領域全体の点ではより正確REBUILDですが、クラスター化インデックスを実行した場合にのみ正確になります。そしてその後も、FILLFACTOR100未満の設定はすべて考慮する必要があります。それでも、常にページヘッダーがあり、小さすぎてこの行に収まらないため単に埋めることができない「無駄な」スペースが十分にあることがよくあります。テーブル、または少なくとも論理的にそのスロットに入れる必要がある行。

「データ使用量」を決定する際の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 BYSELECTリストをに置き換えましたa.*, '---' AS [---], p.*。そしてそれが明らかになりました。人々はこれらのあいまいなインターウェブのどこで情報やスクリプトを;-)から取得するかに注意する必要があります。質問に投稿された2番目のクエリは、特にこの特定の質問については、正確ではありません。

  • マイナーな問題:それがあまり意味をしていない外部のGROUP BY rows間の結合(および集約関数でそのカラムを持っていない)sys.allocation_unitssys.partitions技術的に正しくありません。アロケーションユニットには3つのタイプがあり、そのうちの1つは別のフィールドにJOINする必要があります。かなり頻繁にpartition_idhobt_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;

コメントは詳細な議論のためのものではありません。この会話はチャットに移動しました
ポールホワイト9

2番目のクエリ用に提供した更新されたクエリはさらに(もう一方の方向では):)離れていますが、私はこの答えで大丈夫です。これは明らかにクラックするのが非常に難しいナットです。それだけの価値があるので、2つの方法が一致しない正確な理由を理解できなかったエキスパートでも助けてくれてうれしいです。他の答えの方法論を使用して外挿します。私はこれらの両方の答えに賛成票を投じることができればいいのですが、@ srutzkyは2つが無効になるすべての理由を支援しました。
クリスウッズ

6

多分これは不快な答えですが、これは私が何をするかです。

したがって、DATALENGTHは全体の86%しか占めていません。それはまだ非常に代表的な分割です。srutzkyからの優れた回答のオーバーヘッドはかなり均等に分割されているはずです。

合計には2番目のクエリ(ページ)を使用します。そして、分割を割り当てるために最初の(データ長)を使用します。多くのコストは、正規化を使用して割り当てられます。

そして、より近い答えはコストを上げることになるので、スプリットで負けた部門でさえもっと支払うかもしれないと考える必要があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.