web-dev-qa-db-ja.com

DATALENGTHの合計がsys.allocation_unitsのテーブルサイズと一致しません

テーブル内のすべてのレコードのすべてのフィールドのDATALENGTH()を合計すると、テーブルの合計サイズが得られるという印象を受けました。私は間違っていますか?

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

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

テーブルのVARCHAR(MAX)フィールドが原因で、1行あたりのスペースが大きく変動する可能性があるため、平均サイズ*部門の行の比率だけをとることはできません。上記の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インデックスをどのように含めますか?クラスタ化インデックスの正確なサイズを取得することもできません:)

11
Chris Woods

 以下の情報は包括的なものではないことに注意してください
計算できるように、データページがどのように配置されるかの説明
非常に複雑であるため、行のセットごとに使用されるバイト数。

8kデータページでスペースを占めるのはデータだけではありません。

  • 予約スペースがあります。 8192バイトのうち8060だけを使用することが許可されています(これは、最初は自分のものではなかった132バイトです)。

    • ページヘッダー:これは正確に96バイトです。
    • スロット配列:これは1行あたり2バイトで、各行がページのどこから始まるかのオフセットを示します。この配列のサイズは、残りの36バイト(132-96 = 36)に制限されません。それ以外の場合は、データページに最大18行を配置するだけに制限されます。これは、各行が思ったよりも2バイト大きいことを意味します。 この値はDBCC PAGEによって報告される「レコードサイズ」には含まれません。そのため、以下の行ごとの情報に含まれるのではなく、ここで個別に保持されます。
    • 行ごとのメタデータ(これに限定されない):
      • サイズはテーブル定義(つまり、列の数、可変長または固定長など)によって異なります。 @PaulWhiteのコメントと@Aaronのコメントから取得した情報で、この回答に関連する discussion およびテストにあります。
      • 行ヘッダー: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が考慮されます。しかし、これらは標準サイズではありません。この複雑なトピックの詳細については、次のブログ投稿を参照してください: Varchar、Varbinaryなどの(MAX)タイプのLOBポインターのサイズは? 。そのリンクされた投稿と 私が行った追加のテスト の間で、(デフォルト)ルールは次のようになります。

    • SQL Server 2005以降、誰も使用してはならないレガシー/非推奨のLOBタイプ(TEXTNTEXT、および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 = "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列:スパース列は、行の大部分が1つ以上の列のNULLであるテーブルのスペース(主に固定長データ型の場合)を節約します。 SPARSEオプションを指定すると、NULLの値の型が0バイト上になります(INTの4バイトなどの通常の固定長の代わりに)、しかし、非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ページの 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のみが少なくとも1つのIAMページを報告します。

  • DATA_COMPRESSION:クラスタ化インデックスまたはヒープでROWまたはPAGE圧縮を有効にしている場合は、これまでに述べたことのほとんどを忘れることができます。 96バイトのページヘッダー、行あたり2バイトのスロット配列、および行あたり14バイトのバージョン管理情報は引き続き存在しますが、データの物理表現は非常に複雑になります(圧縮時にすでに述べられているものよりもはるかに複雑になります)。は使用されていません)。たとえば、行圧縮を使用すると、SQL Serverは、各行ごとに各列に収まるように最小のコンテナーを使用しようとします。したがって、そうでなければ(BIGINTも有効になっていないと仮定して)SPARSE列がある場合、常に8バイトを使用します。値が-128から127の間(つまり、符号付き8ビット整数)の場合、1バイトだけを使用します。値がSMALLINTに収まる場合、2バイトしか使用しません。 NULLまたは0の整数型はスペースをとらず、列をマッピングする配列でNULLまたは "空"(つまり0)として単に示されます。そして、他にもたくさんのルールがあります。 Unicodeデータを持っていますか(NCHARNVARCHAR(1 - 4000)notNVARCHAR(MAX)、たとえ行に格納されていても)? Unicode圧縮はSQL Server 2008 R2で追加されましたが、 rules の複雑さを考えると、実際の圧縮を行わずにすべての状況で「圧縮」値の結果を予測する方法はありません。

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

「データ使用量」を決定する際の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」列に対するすべての計算。

[〜#〜] and [〜#〜]、フィールドごとにDATALENGTHを使用しても行ごとのメタデータを返すことができないことを考慮して、DATALENGTHを取得するテーブルごとのクエリに追加する必要があります各フィールドごとに、各「部門」でフィルタリングします。

  • レコードタイプとNULLビットマップへのオフセット:4バイト
  • 列数:2バイト
  • スロット配列:2バイト(「レコードサイズ」には含まれませんが、それでも考慮する必要があります)
  • NULLビットマップ:8列ごとに1バイト(all列の場合)
  • 行のバージョン管理:14バイト(データベースでALLOW_SNAPSHOT_ISOLATIONまたはREAD_COMMITTED_SNAPSHOTONに設定されている場合)
  • 可変長列オフセット配列:すべての列が固定長の場合は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が2番目のクエリの確認に多くの時間を費やさなかったものをどのように説明できるかを考えることに集中しました。そのクエリを単一のテーブルに対して実行し、それらの値をsys.dm_db_database_page_allocationsによって報告されているものと比較しましたが、ページ数の値は同じではありませんでした。直感的に、集約関数とGROUP BYを削除し、SELECTリストをa.*, '---' AS [---], p.*に置き換えました。そしてthenが明らかになりました:人々はこれらのあいまいなインターウェブのどこで情報とスクリプトを;-)から取得するかに注意する必要があります。質問に投稿された2番目のクエリは、特にこの特定の質問については、正確ではありません。

  • 軽微な問題:それ以外に、GROUP BY rowsにはあまり意味がなく(その列が集計関数にない)、sys.allocation_unitssys.partitionsの間のJOINは技術的に正しくありません。アロケーションユニットには3つのタイプがあり、そのうちの1つは別のフィールドにJOINする必要があります。多くの場合、partition_idhobt_idは同じであるため、問題が発生することはありませんが、これら2つのフィールドの値が異なる場合があります。

  • 主な問題:クエリはused_pagesフィールドを使用します。そのフィールドは、allページのタイプ(データ、インデックス、IAMなど)をカバーします。実際のデータのみを扱う場合に使用する別のより適切なフィールドがあります:data_pages

上記の項目を念頭に置き、ページヘッダーを取り消すデータページサイズを使用して、質問の2番目のクエリを調整しました。また、不要な2つのJOINを削除しました。sys.schemasSCHEMA_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;
19
Solomon Rutzky

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

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

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

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

6
paparazzo