web-dev-qa-db-ja.com

未使用のスペースを再利用しようとすると、SQL Serverで使用済みスペースが大幅に増加する

525 GBのサイズの本番データベースにテーブルがあり、そのうち383 GBは未使用です。

Unused Space

この領域の一部を再利用したいのですが、本番DBをいじる前に、データが少ないテストDBの同じテーブルでいくつかの戦略をテストしています。このテーブルにも同様の問題があります。

Unused Space

テーブルに関するいくつかの情報:

  • 曲線因子は0に設定されます
  • 約30列あります
  • 列の1つはイメージタイプのLOBで、数KBから数百MBのサイズのファイルを格納します。
  • テーブルには仮想インデックスが関連付けられていません

サーバーはSQL Server 2017(RTM-GDR)(KB4505224)-14.0.2027.2(X64)を実行しています。データベースはSIMPLE復旧モデルを使用しています。

私が試したいくつかのこと:

  • インデックスの再構築:_ALTER INDEX ALL ON dbo.MyTable REBUILD_。これによる影響はごくわずかです。
  • インデックスの再編成:ALTER INDEX ALL ON dbo.MyTable REORGANIZE WITH(LOB_COMPACTION = ON)。これによる影響はごくわずかです。
  • LOB列を別のテーブルにコピーし、列を削除し、列を再作成し、データをコピーして戻しました(この投稿で概説されているように、 空き領域SQL Serverテーブルの解放 )。これは未使用のスペースを減らしましたが、それを使用済みスペースに変換するだけのようでした:

    Unused Space

  • Bcpユーティリティを使用して、テーブルをエクスポートし、切り捨てて、再読み込みしました(この投稿で概説しています テーブルの未使用領域を解放する方法 )。これにより、未使用のスペースが減少し、使用済みスペースが上の画像と同じ程度に増加しました。

  • お勧めしませんが、DBCC SHRINKFILEコマンドとDBCC SHRINKDATABASEコマンドを試してみましたが、未使用の領域に影響はありませんでした。
  • DBCC CLEANTABLE('myDB', 'dbo.myTable')を実行しても違いはありませんでした
  • 画像とテキストのデータ型を維持しながら、データ型をvarbinary(max)とvarchar(max)に変更した後、上記のすべてを試しました。
  • 新しいデータベースの新しいテーブルにデータをインポートしようとしましたが、これも未使用のスペースを使用済みスペースに変換するだけでした。この試みの詳細を この投稿 で概説しました。

これらが期待できる結果である場合、本番DBでこれらの試みを行いたくないので、次のようにします。

  1. これらの試みのいくつかの後に、未使用のスペースが使用済みスペースに変換されるのはなぜですか?フードの下で何が起こっているのかをよく理解していないような気がします。
  2. 使用済みスペースを増やすことなく未使用スペースを減らすために他にできることはありますか?

編集:テーブルのディスク使用量レポートとスクリプトは次のとおりです:

Disk Usage

_SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[MyTable](
    [Column1]  [int] NOT NULL,
    [Column2]  [int] NOT NULL,
    [Column3]  [int] NOT NULL,
    [Column4]  [bit] NOT NULL,
    [Column5]  [tinyint] NOT NULL,
    [Column6]  [datetime] NULL,
    [Column7]  [int] NOT NULL,
    [Column8]  [varchar](100) NULL,
    [Column9]  [varchar](256) NULL,
    [Column10] [int] NULL,
    [Column11] [image] NULL,
    [Column12] [text] NULL,
    [Column13] [varchar](100) NULL,
    [Column14] [varchar](6) NULL,
    [Column15] [int] NOT NULL,
    [Column16] [bit] NOT NULL,
    [Column17] [datetime] NULL,
    [Column18] [varchar](50) NULL,
    [Column19] [varchar](50) NULL,
    [Column20] [varchar](60) NULL,
    [Column21] [varchar](20) NULL,
    [Column22] [varchar](120) NULL,
    [Column23] [varchar](4) NULL,
    [Column24] [varchar](75) NULL,
    [Column25] [char](1) NULL,
    [Column26] [varchar](50) NULL,
    [Column27] [varchar](128) NULL,
    [Column28] [varchar](50) NULL,
    [Column29] [int] NULL,
    [Column30] [text] NULL,
 CONSTRAINT [PK] PRIMARY KEY CLUSTERED 
(
    [Column1] ASC,
    [Column2] ASC,
    [Column3] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
ALTER TABLE [dbo].[MyTable] ADD  CONSTRAINT [DF_Column4]  DEFAULT (0) FOR [Column4]
GO
ALTER TABLE [dbo].[MyTable] ADD  CONSTRAINT [DF_Column5]  DEFAULT (0) FOR [Column5]
GO
ALTER TABLE [dbo].[MyTable] ADD  CONSTRAINT [DF_Column15]  DEFAULT (0) FOR [Column15]
GO
ALTER TABLE [dbo].[MyTable] ADD  CONSTRAINT [DF_Column16]  DEFAULT (0) FOR [Column16]
GO
_

Max Vernonの回答のコマンドを実行した結果は次のとおりです。

_╔════════════╦═══════════╦════════════╦═════════════════╦══════════════════════╦════════════════════╗
║ TotalBytes ║ FreeBytes ║ TotalPages ║ TotalEmptyPages ║ PageBytesFreePercent ║ UnusedPagesPercent ║
╠════════════╬═══════════╬════════════╬═════════════════╬══════════════════════╬════════════════════╣
║  9014280192║ 8653594624║     1100376║          997178 ║            95.998700 ║          90.621500 ║
╚════════════╩═══════════╩════════════╩═════════════════╩══════════════════════╩════════════════════╝
_
_╔═════════════╦═══════════════════╦════════════════════╗
║ ObjectName  ║ ReservedPageCount ║      UsedPageCount ║
╠═════════════╬═══════════════════╬════════════════════╣
║ dbo.MyTable ║           5109090 ║            2850245 ║
╚═════════════╩═══════════════════╩════════════════════╝
_

UPDATE:

Max Vernonの提案に従って、以下を実行しました。

_DBCC UPDATEUSAGE (N'<database_name>', N'<table_name>');
_

そしてここに出力がありました:

_DBCC UPDATEUSAGE: Usage counts updated for table 'MyTable' (index 'PK_MyTable', partition 1):
        USED pages (LOB Data): changed from (568025) to (1019641) pages.
        RSVD pages (LOB Data): changed from (1019761) to (1019763) pages.
_

これにより、テーブルのディスク使用量が更新されました。

enter image description here

そして全体的なディスク使用量:

enter image description here

そのため、SQL Serverによって追跡されたディスク使用量が実際のディスク使用量と大幅に同期しなくなったことが問題であると思われます。この問題は解決したと考えますが、そもそもなぜこれが起こったのか知りたいです。

15
Ken

最初のステップとして、テーブルに対して DBCC UPDATEUSAGE を実行します。これは、症状が一貫性のないスペース使用を示しているためです。

DBCC UPDATEUSAGEは、テーブルまたはインデックスの各パーティションの行、使用済みページ、予約済みページ、リーフページ、およびデータページカウントを修正します。システムテーブルに誤りがない場合、DBCC UPDATEUSAGEはデータを返しません。不正確な部分が見つかり修正され、WITH NO_INFOMSGSが使用されない場合、DBCC UPDATEUSAGEはシステムテーブルで更新されている行と列を返します。

構文は次のとおりです。

DBCC UPDATEUSAGE (N'<database_name>', N'<table_name>');

それを実行した後、テーブルに対してEXEC sys.sp_spaceusedを実行します。

EXEC sys.sp_spaceused @objname = N'dbo.MyTable'
    , @updateusage = 'false' --true or false
    , @mode = 'ALL' --ALL, LOCAL_ONLY, REMOTE_ONLY
    , @oneresultset = 1;

上記のコマンドには使用状況を更新するオプションがありますが、最初にDBCC UPDATEUSAGEを手動で実行したので、それをfalseに設定したままにします。 DBCC UPDATEUSAGEを手動で実行すると、何か修正されたかどうかを確認できます。

次のクエリは、テーブルの空きバイトの割合とテーブルの空きページの割合を表示する必要があります。クエリはドキュメント化されていない機能を使用しているため、結果を当てにするのは賢明ではありませんが、高レベルでのsys.sp_spaceusedからの出力と比較すると正確なようです。

空きバイトの割合が空きページの割合よりも大幅に高い場合、部分的に空のページが多数あります。

部分的に空のページは、次のようないくつかの原因から発生する可能性があります。

  1. ページ分割。クラスター化インデックスへの新しい挿入に対応するためにページを分割する必要があります。

  2. 列のサイズが原因で、ページを列で埋めることができない。

クエリは、ドキュメント化されていないsys.dm_db_database_page_allocations動的管理関数を使用します。

;WITH dpa AS 
(
    SELECT dpa.*
        , page_free_space_percent_corrected = 
          CASE COALESCE(dpa.page_type_desc, N'')
            WHEN N'TEXT_MIX_PAGE' THEN 100 - COALESCE(dpa.page_free_space_percent, 100)
            WHEN N'TEXT_TREE_PAGE' THEN 100 - COALESCE(dpa.page_free_space_percent, 100)
            ELSE COALESCE(dpa.page_free_space_percent, 100)
          END
    FROM sys.dm_db_database_page_allocations(DB_ID(), OBJECT_ID('dbo.MyTable'), NULL, NULL, 'DETAILED') dpa
)
, src AS
(
SELECT TotalKB = COUNT_BIG(1) * 8192 / 1024
    , FreeKB = SUM((dpa.page_free_space_percent_corrected / 100) * CONVERT(bigint, 8192)) / 1024
    , TotalPages = COUNT_BIG(1)
    , TotalEmptyPages = SUM(CASE WHEN dpa.page_free_space_percent_corrected = 100 THEN 1 ELSE 0 END) --completely empty pages
FROM dpa
)
SELECT *
    , BytesFreePercent = (CONVERT(decimal(38,2), src.FreeKB) / src.TotalKB) * 100
    , UnusedPagesPercent = (CONVERT(decimal(38,2), src.TotalEmptyPages) / src.TotalPages) * 100
FROM src

出力は次のようになります。

╔═════════╦════════╦════════════╦═════════════════ ╦══════════════════╦════════════════════╗
║TotalKB║FreeKB ║TotalPages║TotalEmptyPages║BytesFreePercent║UnusedPagesPercent║
╠═════════╬════════╬════════════╬═══ ══════════════╬══════════════════╬════════════════ ════╣
║208║96║26║12║46.153800║46.153800║
╚═════════╩════════╩══ ══════════╩═════════════════╩══════════════════╩ ═══════════════════╝

私は、関数 here について説明するブログ投稿を書きました。

あなたのシナリオでは、ALTER TABLE ... REBUILDを実行したので、TotalEmptyPagesの数値は非常に低いはずですが、BytesFreePercentには約72%が残っていると思います。 。

CREATE TABLEスクリプトを使用して、シナリオの再現を試みました。

これは [〜#〜] mcve [〜#〜] 使用しています:

DROP TABLE IF EXISTS dbo.MyTable;

CREATE TABLE [dbo].[MyTable](
    [Column1]  [int]            NOT NULL IDENTITY(1,1),
    [Column2]  [int]            NOT NULL,
    [Column3]  [int]            NOT NULL,
    [Column4]  [bit]            NOT NULL,
    [Column5]  [tinyint]        NOT NULL,
    [Column6]  [datetime]       NULL,
    [Column7]  [int]            NOT NULL,
    [Column8]  [varchar](100)   NULL,
    [Column9]  [varchar](256)   NULL,
    [Column10] [int]            NULL,
    [Column11] [image]          NULL,
    [Column12] [text]           NULL,
    [Column13] [varchar](100)   NULL,
    [Column14] [varchar](6)     NULL,
    [Column15] [int]            NOT NULL,
    [Column16] [bit]            NOT NULL,
    [Column17] [datetime]       NULL,
    [Column18] [varchar](50)    NULL,
    [Column19] [varchar](50)    NULL,
    [Column20] [varchar](60)    NULL,
    [Column21] [varchar](20)    NULL,
    [Column22] [varchar](120)   NULL,
    [Column23] [varchar](4)     NULL,
    [Column24] [varchar](75)    NULL,
    [Column25] [char](1)        NULL,
    [Column26] [varchar](50)    NULL,
    [Column27] [varchar](128)   NULL,
    [Column28] [varchar](50)    NULL,
    [Column29] [int]            NULL,
    [Column30] [text]           NULL,
 CONSTRAINT [PK] PRIMARY KEY CLUSTERED 
(
    [Column1] ASC,
    [Column2] ASC,
    [Column3] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

ALTER TABLE [dbo].[MyTable] ADD  CONSTRAINT [DF_Column4]  DEFAULT (0) FOR [Column4]

ALTER TABLE [dbo].[MyTable] ADD  CONSTRAINT [DF_Column5]  DEFAULT (0) FOR [Column5]

ALTER TABLE [dbo].[MyTable] ADD  CONSTRAINT [DF_Column15]  DEFAULT (0) FOR [Column15]

ALTER TABLE [dbo].[MyTable] ADD  CONSTRAINT [DF_Column16]  DEFAULT (0) FOR [Column16]
GO

INSERT INTO dbo.MyTable (
      Column2
    , Column3
    , Column4
    , Column5
    , Column6
    , Column7
    , Column8
    , Column9
    , Column10
    , Column11
    , Column12
    , Column13
    , Column14
    , Column15
    , Column16
    , Column17
    , Column18
    , Column19
    , Column20
    , Column21
    , Column22
    , Column23
    , Column24
    , Column25
    , Column26
    , Column27
    , Column28
    , Column29
    , Column30
)
VALUES (
          0
        , 0
        , 0
        , 0
        , '2019-07-09 00:00:00'
        , 1
        , REPLICATE('A', 50)    
        , REPLICATE('B', 128)   
        , 0
        , REPLICATE(CONVERT(varchar(max), 'a'), 1)
        , REPLICATE(CONVERT(varchar(max), 'b'), 9000)
        , REPLICATE('C', 50)    
        , REPLICATE('D', 3)     
        , 0
        , 0
        , '2019-07-10 00:00:00'
        , REPLICATE('E', 25)    
        , REPLICATE('F', 25)    
        , REPLICATE('G', 30)    
        , REPLICATE('H', 10)    
        , REPLICATE('I', 120)   
        , REPLICATE('J', 4)     
        , REPLICATE('K', 75)    
        , 'L'       
        , REPLICATE('M', 50)    
        , REPLICATE('N', 128)   
        , REPLICATE('O', 50)    
        , 0
        , REPLICATE(CONVERT(varchar(max), 'c'), 90000)
);
--GO 100

;WITH dpa AS 
(
    SELECT dpa.*
        , page_free_space_percent_corrected = 
          CASE COALESCE(dpa.page_type_desc, N'')
            WHEN N'TEXT_MIX_PAGE' THEN 100 - COALESCE(dpa.page_free_space_percent, 100)
            WHEN N'TEXT_TREE_PAGE' THEN 100 - COALESCE(dpa.page_free_space_percent, 100)
            ELSE COALESCE(dpa.page_free_space_percent, 100)
          END
    FROM sys.dm_db_database_page_allocations(DB_ID(), OBJECT_ID('dbo.MyTable'), NULL, NULL, 'DETAILED') dpa
)
, src AS
(
SELECT TotalKB = COUNT_BIG(1) * 8192 / 1024
    , FreeKB = SUM((dpa.page_free_space_percent_corrected / 100) * CONVERT(bigint, 8192)) / 1024
    , TotalPages = COUNT_BIG(1)
    , TotalEmptyPages = SUM(CASE WHEN dpa.page_free_space_percent_corrected = 100 THEN 1 ELSE 0 END) --completely empty pages
FROM dpa
)
SELECT *
    , BytesFreePercent = (CONVERT(decimal(38,2), src.FreeKB) / src.TotalKB) * 100
    , UnusedPagesPercent = (CONVERT(decimal(38,2), src.TotalEmptyPages) / src.TotalPages) * 100
FROM src

次のクエリは、テーブルに割り当てられたページごとに1行を表示し、同じドキュメント化されていないDMVを使用しています。

SELECT DatabaseName = d.name
    , ObjectName = o.name
    , IndexName = i.name
    , PartitionID = dpa.partition_id
    , dpa.allocation_unit_type_desc
    , dpa.allocated_page_file_id
    , dpa.allocated_page_page_id
    , dpa.is_allocated
    , dpa.page_free_space_percent --this seems unreliable
    , page_free_space_percent_corrected = 
        CASE COALESCE(dpa.page_type_desc, N'')
        WHEN N'TEXT_MIX_PAGE' THEN 100 - COALESCE(dpa.page_free_space_percent, 100)
        WHEN N'TEXT_TREE_PAGE' THEN 100 - COALESCE(dpa.page_free_space_percent, 100)
        ELSE COALESCE(dpa.page_free_space_percent, 100)
        END
    , dpa.page_type_desc
    , dpa.is_page_compressed
    , dpa.has_ghost_records
FROM sys.dm_db_database_page_allocations(DB_ID(), OBJECT_ID('dbo.MyTable'), NULL, NULL, 'DETAILED') dpa
    LEFT JOIN sys.databases d ON dpa.database_id = d.database_id
    LEFT JOIN sys.objects o ON dpa.object_id = o.object_id
    LEFT JOIN sys.indexes i ON dpa.object_id = i.object_id AND dpa.index_id = i.index_id
WHERE dpa.database_id = DB_ID() --sanity check for sys.objects and sys.indexes

テスト環境で実際のテーブルに対して実行すると、出力には行のlotが表示されますが、問題の場所を確認できる場合があります。

次のスクリプトを実行して、結果を質問に投稿できますか?私たちは同じページにいることを確認しようとしています。

SELECT ObjectName = s.name + N'.' + o.name
    , ReservedPageCount = SUM(dps.reserved_page_count)
    , UsePageCount = SUM(dps.used_page_count)
FROM sys.schemas s
    INNER JOIN sys.objects o ON s.schema_id = o.schema_id
    INNER JOIN sys.partitions p ON o.object_id = p.object_id
    INNER JOIN sys.dm_db_partition_stats dps ON p.object_id = dps.object_id
WHERE s.name = N'dbo'
    AND o.name = N'MyTable'
GROUP BY s.name + N'.' + o.name;
10
Max Vernon

列の1つはタイプimageのLOBで、数KBから数百MBのサイズのファイルを格納します

内部の断片化が発生している可能性があります。
このテーブルの ページの断片化 とは何ですか?
そして、行内の断片化は行外ページと異なりますか?

数KBのファイルがあるとします。
SQL Serverはすべてを8060バイトのページに格納します。つまり、4040バイトの行(または行外データ)があり、次の行が類似している場合、同じページに両方を収めることはできず、スペースの半分が無駄になります。可変長の列(たとえば、画像から開始)を別のテーブルに格納して、行サイズを変更してみてください。

0
DrTrunks Bell