尝试回收未使用的空间会导致已用空间在SQL Server中显着增加


15

我在生产数据库中有一个表,该表的大小为525 GB,其中383 GB未使用:

未使用的空间

我想回收一些空间,但是在弄乱生产数据库之前,我正在用较少数据的测试数据库中的同一表上测试一些策略。该表有一个类似的问题:

未使用的空间

有关表的一些信息:

  • 填充因子设置为0
  • 大约有30列
  • 列之一是图像类型的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)之后,我都尝试了上述所有方法。
  • 我尝试将数据导入到新数据库中的新表中,这也仅将未使用的空间转换为已用空间。我在这篇文章中概述了这种尝试的细节。

如果我期望这些结果,我不想在生产数据库上进行这些尝试,因此:

  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跟踪的磁盘使用情况与实际磁盘使用情况完全不同步。我认为此问题已解决,但我很想知道为什么会首先发生这种情况!

Answers:


10

第一步,我将对表运行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

以下查询显示分配给该表的每个页面的一行,并使用相同的未记录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报告的磁盘使用情况和页面信息非常不同步-我用详细信息更新了我的帖子。我很好奇这会如何发生,但至少发现了问题。感谢您的所有帮助,我非常感谢!
肯(Ken)

0

列之一是图像类型的LOB,它存储的文件大小从几KB到几百MB不等

您可能遇到内部碎片。该表
页面碎片是什么?
行内的碎片与行外网页是否不同?

您说您的文件只有几KB。
SQL Server将所有内容存储在8060字节页面中。这意味着,如果一行(或行外数据)为4040字节,而下一行是相似的,则它不能同时容纳在同一页面中,这将浪费一半的空间。尝试通过在其他表中存储可变长度列(例如,从图像开始)来更改行大小。


我认为零碎不是问题。重建索引后,聚集索引的碎片率为0.45%,其页面填充度为98.93%。
肯(Ken)

如果您遇到的行或LOB数据无法很好地容纳8KB页面,则重建表或索引将无济于事。这是马克斯·弗农(Max Vernon)更详细地解释的:“您有很多部分为空的页面。” 也称为内部碎片
DrTrunks Bell

-3

数据库是否处于完全恢复模式?如果是这样,执行收缩时,它会记录所有更改,并且不会按照您期望的方式进行收缩。根据您的工作时间,可以进行备份,切换到批量装运恢复模式,然后对数据文件执行收缩。之后,您需要运行索引脚本来修复/重建并切换回完全恢复。无论如何,这都是我会尝试的方法,但是这再次取决于您的工作时间。


4
提出恢复模型很有趣。我认为,如果OP的日志文件大小有问题,那将更适用 就目前而言,他们在数据文件的大小方面遇到了麻烦因此如果恢复模型引起了上述问题我将感到惊讶。
乔什·达内尔

没错,但是唯一一次缩水并没有真正影响空间是因为恢复模型,所以我认为值得一提的是万一它被误诊了。
John-Henry Lochbaum

-3

我唯一无法缩小数据库和回收空间的原因是,您不能缩小数据库的大小,使其超出创建数据库时的初始大小。因此,例如,如果您的数据库是生产数据库的副本,并且您最初以525GB创建了该数据库,则无论您从数据库中删除了多少数据,sql server都不允许您将大小缩小到525GB以下。但是,如果数据库是在383GB以下创建的,然后又增长到525GB,则回收该空间应该没有问题。我一直认为这是Microsoft的愚蠢而任意的限制。

仅将数据库缩小到创建数据库后设置的初始大小


问题不在于收缩数据库(如果是,收缩数据库的能力取决于初始大小区域后的已使用空间)
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所言,重建表将消除大部分碎片。我怀疑再进行一次重建会进一步帮助您。
Max Vernon
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.