未使用領域を再利用しようとすると、SQL Serverで使用領域が大幅に増加します


15

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

未使用スペース

このスペースの一部を回収したいのですが、実稼働DBをいじる前に、より少ないデータでテストDBの同一のテーブルでいくつかの戦略をテストしています。この表には同様の問題があります。

未使用スペース

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

  • フィルファクターは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テーブルの解放)。これにより、未使用のスペースが減少しましたが、使用済みのスペースに変換するだけのようです。

    未使用スペース

  • bcpユーティリティを使用して、テーブルをエクスポート、切り捨て、再読み込みします(この投稿で説明されているように、テーブルの未使用スペースを解放する方法)。これにより、未使用スペースが削減され、使用済みスペースが上のイメージと同程度に増加しました。

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

これらが期待できる結果である場合、実稼働DBでこれらの試行を行いたくないので、

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

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

ディスクの使用状況

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 
╚═════════════╩═══════════════════╩════════════════════╝

更新:

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.

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

ここに画像の説明を入力してください

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

ここに画像の説明を入力してください

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


データ型を変更した後、縮小は試行されましたか?
LowlyDBA

1
作成した再現を共有できる可能性はありますか、それともテスト用のコピーとして運用データベースのバックアップを使用していますか?
ジョンイスブレナー

@LowlyDBAはい、データ型を変更した後に縮小を試みました。
ケン

スクリプトを実行し、仮想的なインデックスがないことを確認しました。
ケン

1
このデータベースは本番環境でどれくらい使用されていますか?どれくらいの頻度で走りDBCC CHECKDBますか?あなたから離れて移動すると考えられてい推奨されないデータ型textおよびimage?彼らは不適切な統計に貢献している可能性があります。
マックスヴァーノン

回答:


9

最初の手順として、テーブルに対して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║
╚=========╩==================================== ╩=====================================╝

ここで機能を説明するブログ投稿を書きました。

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

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

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

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

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;

2
実行するとDBCC UPDATEUSAGE、未使用領域と未使用ページ数が更新されました。SQL Serverによって報告されているディスク使用量とページ情報が非常に同期していないようです。詳細を投稿に更新しました。そもそもこれがどうなっていたのか興味がありますが、少なくとも問題は見つかりました。すべてのあなたの助けをありがとう、本当に感謝します!
ケン

0

列の1つはイメージタイプのLOBであり、数KBから数百MBのサイズのファイルを格納しています

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

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


断片化が問題だとは思わない。インデックスを再構築した後、クラスター化インデックスの断片化は0.45%で、ページの使用率は98.93%です。
ケン

非常に大きな行や、8KBページにうまく収まらないLOBデータに悩まされている場合、テーブルまたはインデックスを再構築しても役に立ちません。Max Vernonがより詳細に説明したのは、「部分的に空のページがたくさんある」ということです。また、内部フラグメンテーションと呼ばれる
DrTrunksベル

-3

データベースは完全復旧モードですか?その場合、縮小を実行すると、すべての変更がログに記録され、期待どおりに縮小されません。稼働時間に応じて、バックアップを取り、バルク出荷リカバリモードに切り替えてから、データファイルの圧縮を実行できます。その後、インデックススクリプトを実行して修復/再構築し、完全復旧に切り替えます。それは私がとにかく試してみることですが、もう一度、これはすべてのあなたの営業時間に依存します。


4
復旧モデルを立ち上げることは興味深いです。OPがログファイルのサイズに問題がある場合は、より適切だと思います。 現状では、データファイルのサイズに問題があります。そのため、復旧モデルが説明されている問題を引き起こしていた場合、私は驚くでしょう。
ジョシュダーネル

確かに、シュリンクを実行し、スペースに実際に影響を与えなかったのはリカバリモデルのためだけだったので、誤診があった場合にそれを取り上げる価値があると思いました。
ジョンヘンリーロッホバウム

-3

DBを縮小してスペースを再利用できなかったのは、作成時にDBの初期サイズを超えてDBを縮小できないためです。たとえば、DBが運用DBのコピーであり、最初に525GBでDBを作成した場合、SQLサーバーでは、DBから削除するデータの量に関係なく、サイズを525GB未満に縮小することはできません。しかし、DBが383GB未満で作成され、その後525GBになった場合、問題なく領域を再利用できます。私はこれがMicrosoftによる愚かでarbitrary意的な制限だと長い間考えていました。

データベースの作成後に設定される初期サイズまでのみデータベースを縮小します


質問は、データベースを縮小する(そしてそれがあった場合、それを縮小する機能は、初期サイズの領域の後に使用されるスペースに依存します)に関するものではありません
eckes

未使用のスペースがある限り、元のサイズに関係なく、データベースを数MBに縮小することができます。必ずしも良いアイデアとは限りませんが、データベースを縮小し、このような制限に達することは決してありません。
レイ

-3

実稼働ボックスでこの問題に遭遇したことがありますが、必要なのは、各テーブルのテーブルとインデックスを(この順序で)再構築することです。

以下は、テーブルをチェックするために使用するクエリです。どのテーブルを再構築する必要があるかを判断し、実行する必要があるSQLクエリを作成するのに役立ちます。このクエリは、1MBを超える未使用領域と5%の未使用率を持つものに限定されているため、本当に注力する必要があるもののみを再構築します。

SELECT  'alter table [' + t.NAME + '] rebuild;' AS SQL1, 'alter index all on [' + t.NAME + '] rebuild;' as SQL2, 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, case when SUM(a.total_pages)=0 then 0 else (SUM(a.total_pages) - SUM(a.used_pages))*100/SUM(a.total_pages) end as Ratio  FROM     sys.tables t (nolock) INNER JOIN       sys.indexes i (nolock)  ON t.OBJECT_ID = i.object_id INNER JOIN  sys.partitions p (nolock) ON i.object_id = p.OBJECT_ID AND i.index_id = p.index_id INNER JOIN  sys.allocation_units a (nolock) ON p.partition_id = a.container_id LEFT OUTER JOIN  sys.schemas s (nolock) ON t.schema_id = s.schema_id WHERE  t.is_ms_shipped = 0 AND i.OBJECT_ID > 255  GROUP BY  t.Name, s.Name, p.Rows  
having  (SUM(a.total_pages) - SUM(a.used_pages)) * 8/1024>1
and (SUM(a.total_pages) - SUM(a.used_pages))*100/SUM(a.total_pages)>5
ORDER BY    5 desc

OPが行ったようにテーブルを再構築すると、断片化のほとんどが解消されます。別の再構築を行うことがさらに役立つとは思わない。
マックスヴァーノン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.