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

Ken*_*Ken 15 index sql-server storage sql-server-2017

我在生产数据库中有一个表,其大小为 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
Run Code Online (Sandbox Code Playgroud)

以下是执行 Max Vernon 回答中的命令的结果:

?????????????????????????????????????????????????????????????????????????????????????????????????????
? TotalBytes ? FreeBytes ? TotalPages ? TotalEmptyPages ? PageBytesFreePercent ? UnusedPagesPercent ?
?????????????????????????????????????????????????????????????????????????????????????????????????????
?  9014280192? 8653594624?     1100376?          997178 ?            95.998700 ?          90.621500 ?
?????????????????????????????????????????????????????????????????????????????????????????????????????
Run Code Online (Sandbox Code Playgroud)
????????????????????????????????????????????????????????
? ObjectName  ? ReservedPageCount ?      UsedPageCount ?
????????????????????????????????????????????????????????
? dbo.MyTable ?           5109090 ?            2850245 ?
????????????????????????????????????????????????????????
Run Code Online (Sandbox Code Playgroud)

更新:

我按照 Max Vernon 的建议运行了以下命令:

DBCC UPDATEUSAGE (N'<database_name>', N'<table_name>');
Run Code Online (Sandbox Code Playgroud)

这是输出:

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.
Run Code Online (Sandbox Code Playgroud)

这更新了表的磁盘使用情况:

在此处输入图片说明

以及整体磁盘使用情况:

在此处输入图片说明

因此,问题似乎在于 SQL Server 跟踪的磁盘使用情况与实际磁盘使用情况严重不同步。我会认为这个问题已经解决,但我很想知道为什么会发生这种情况!

Han*_*non 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;
Run Code Online (Sandbox Code Playgroud)

上面的命令可以选择更新使用情况,但由于您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
Run Code Online (Sandbox Code Playgroud)

输出看起来像:

?????????????????????????????????????????????????????? ?????????????????????????????????????????????
? 总KB ? 自由知识库?总页数?总空页数?BytesFreePercent ?未使用的页面百分比?
?????????????????????????????????????????????????????? ?????????????????????????????????????????????
? 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

Run Code Online (Sandbox Code Playgroud)

以下查询为分配给表的每个页面显示一行,并使用相同的未记录 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
Run Code Online (Sandbox Code Playgroud)

如果您在测试环境中针对真实表运行它,输出将显示很多行,但它可能会让您看到问题出在哪里。

您可以运行以下脚本并将结果发布在您的问题中吗?我只是想确保我们在同一页面上。

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;
Run Code Online (Sandbox Code Playgroud)