Tentativas de recuperar espaço não utilizado fazem com que o espaço usado aumente significativamente no SQL Server

15

Eu tenho uma tabela em um banco de dados de produção com um tamanho de 525 GB, dos quais 383 GB não são utilizados:

Espaço não utilizado

Gostaria de recuperar parte desse espaço, mas, antes de mexer com o banco de dados de produção, estou testando algumas estratégias em uma tabela idêntica em um banco de dados de teste com menos dados. Esta tabela tem um problema semelhante:

Espaço não utilizado

Algumas informações sobre a tabela:

  • O fator de preenchimento está definido como 0
  • Existem cerca de 30 colunas
  • Uma das colunas é um LOB de tipo de imagem e está armazenando arquivos com tamanho variando de algumas KB a várias centenas de MB
  • A tabela não possui nenhum índice hipotético associado a ela

O servidor está executando o SQL Server 2017 (RTM-GDR) (KB4505224) - 14.0.2027.2 (X64). O banco de dados está usando o SIMPLEmodelo de recuperação.

Algumas coisas que eu tentei:

  • Reconstrução dos índices: ALTER INDEX ALL ON dbo.MyTable REBUILD. Isso teve um impacto insignificante.
  • A reorganização dos índices: ALTER INDEX ALL ON dbo.MyTable REORGANIZE WITH(LOB_COMPACTION = ON). Isso teve um impacto insignificante.
  • Copiou a coluna LOB para outra tabela, soltou a coluna, recriou a coluna e copiou os dados novamente (conforme descrito nesta postagem: Liberando Espaço Não Utilizado na Tabela do SQL Server ). Isso diminuiu o espaço não utilizado, mas parecia convertê-lo em espaço usado:

    Espaço não utilizado

  • Utilizou o utilitário bcp para exportar, truncá-la e recarregá-la (conforme descrito nesta postagem: Como liberar o espaço não utilizado para uma tabela ). Isso também reduziu o espaço não utilizado e aumentou o espaço usado em uma extensão semelhante à da imagem acima.

  • Embora não seja recomendado, tentei os comandos DBCC SHRINKFILE e DBCC SHRINKDATABASE, mas eles não tiveram nenhum impacto no espaço não utilizado.
  • Correr DBCC CLEANTABLE('myDB', 'dbo.myTable')não fez diferença
  • Eu tentei todas as opções acima, mantendo os tipos de dados de imagem e texto e depois de alterá-los para varbinary (max) e varchar (max).
  • Tentei importar os dados para uma nova tabela em um banco de dados novo, e isso também converteu apenas o espaço não utilizado em espaço usado. Descrevi os detalhes dessa tentativa neste post .

Eu não quero fazer essas tentativas no banco de dados de produção se estes são os resultados que posso esperar, então:

  1. Por que o espaço não utilizado está sendo convertido em espaço usado após algumas dessas tentativas? Sinto que não tenho uma boa compreensão do que está acontecendo sob o capô.
  2. Há mais alguma coisa que eu possa fazer para diminuir o espaço não utilizado sem aumentar o espaço usado?

EDIT: Aqui está o relatório de uso de disco e script para a tabela:

Uso de disco

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

Aqui estão os resultados da execução dos comandos na resposta de Max Vernon:

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

ATUALIZAR:

Executei o seguinte, conforme sugerido por Max Vernon:

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

E aqui estava a saída:

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.

Isso atualizou o uso do disco para a tabela:

insira a descrição da imagem aqui

E o uso geral do disco:

insira a descrição da imagem aqui

Portanto, parece que o problema foi que o uso do disco, conforme rastreado pelo SQL Server, ficou descontrolado com o uso real do disco. Considerarei esse problema resolvido, mas eu estaria interessado em saber por que isso teria acontecido em primeiro lugar!

Ken
fonte
Os encolhimentos foram tentados depois de alterar os tipos de dados?
#
1
Você tem alguma chance de compartilhar a reprodução que fez conosco ou está apenas usando um backup do banco de dados de produção como uma cópia para testar?
John Eisbrener 01/08/19
@LowlyDBA Sim, tentei diminuir depois de alterar os tipos de dados.
Ken
Eu executei o script e confirmei que não há índices hipotéticos.
Ken
1
Há quanto tempo você mantém esse banco de dados em produção? Com que frequência você corre DBCC CHECKDB? Você já pensou em se afastando dos tipos de dados obsoletos , texte image? Eles podem estar contribuindo para as estatísticas impróprias.
Max Vernon

Respostas:

9

Eu executaria o DBCC UPDATEUSAGE na tabela como uma primeira etapa, pois os sintomas mostram o uso inconsistente de espaço.

DBCC UPDATEUSAGE corrige as linhas, páginas usadas, páginas reservadas, páginas folha e contagem de páginas de dados para cada partição em uma tabela ou índice. Se não houver imprecisões nas tabelas do sistema, o DBCC UPDATEUSAGE não retornará dados. Se imprecisões forem encontradas e corrigidas e WITH NO_INFOMSGS não for usado, DBCC UPDATEUSAGE retornará as linhas e colunas que estão sendo atualizadas nas tabelas do sistema.

A sintaxe é:

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

Depois de executar isso, eu correria EXEC sys.sp_spaceusedcontra a mesa:

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

O comando acima tem a opção de atualizar o uso, mas desde que você executou DBCC UPDATEUSAGEmanualmente primeiro, deixe esse conjunto como false. A execução DBCC UPDATEUSAGEmanual permite ver se alguma coisa foi corrigida.

A consulta a seguir deve mostrar a porcentagem de bytes livres na tabela e a porcentagem de páginas livres na tabela. Como a consulta usa um recurso não documentado, não é aconselhável contar com os resultados, mas parece preciso quando comparado com a saída de sys.sp_spaceusedalto nível.

Se a porcentagem de bytes livres for significativamente maior que a porcentagem de páginas gratuitas, você terá muitas páginas parcialmente vazias.

As páginas parcialmente vazias podem resultar de várias causas, incluindo:

  1. Divisões de página, em que a página deve ser dividida para acomodar novas inserções no índice em cluster

  2. Incapacidade de preencher a página com colunas devido ao tamanho da coluna.

A consulta usa a sys.dm_db_database_page_allocationsfunção de gerenciamento dinâmico não documentada :

;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

A saída se parece com:

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

Eu escrevi uma postagem no blog descrevendo a função aqui .

No seu cenário, desde que você executou ALTER TABLE ... REBUILD, você deve ver um número muito baixo para TotalEmptyPages, mas acho que você ainda terá cerca de 72% em BytesFreePercent.

Eu usei seu CREATE TABLEscript para tentar recriar seu cenário.

Este é o MCVE que estou usando:

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

A consulta a seguir mostra uma única linha para cada página alocada para a tabela e usa a mesma DMV não documentada:

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

A saída mostrará muitas linhas se você a executar na tabela real em seu ambiente de teste, mas poderá permitir que você veja onde está o problema.

Você pode executar o script a seguir e postar os resultados em sua pergunta? Só estou tentando garantir que estamos na mesma página.

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;
Max Vernon
fonte
2
A execução DBCC UPDATEUSAGEatualizou o espaço não utilizado e a contagem de páginas não utilizadas. Parece que o uso do disco e as informações da página relatadas pelo SQL Server estavam extremamente fora de sincronia - atualizei minha postagem com os detalhes. Estou curioso sobre como isso teria acontecido em primeiro lugar, mas pelo menos o problema foi encontrado. Obrigado por toda a sua ajuda, eu realmente aprecio isso!
Ken
0

Uma das colunas é um LOB do tipo imagem e está armazenando arquivos com tamanho variando de algumas KB a várias centenas de MB

Você pode estar enfrentando uma fragmentação interna.
Qual é a fragmentação da página para esta tabela?
E a fragmentação para a linha é diferente das páginas fora da linha?

Você diz que possui arquivos com alguns KB.
O SQL Server armazena tudo em páginas de 8060 bytes. Ou seja, se você tiver uma linha (ou dados fora de linha) com 4040 bytes e a próxima for semelhante, ela não poderá caber na mesma página e você perderá metade do seu espaço. Tente alterar o tamanho da linha armazenando colunas de comprimento variável (comece com a imagem, por exemplo) em uma tabela diferente.

DrTrunks Bell
fonte
Não acho que a fragmentação seja o problema. Após a reconstrução dos índices, a fragmentação do índice em cluster é de 0,45% e a plenitude da página é de 98,93%.
Ken
A reconstrução de uma tabela ou índice não ajudará se você sofrer com linhas muito grandes ou dados LOB que não se encaixam bem em páginas de 8 KB. É o que Max Vernon explicou em mais detalhes: "você tem muitas páginas parcialmente vazias". também chamado de fragmentação interna
DrTrunks Sino
-3

O banco de dados está no modo de recuperação completa? Nesse caso, quando você executa uma redução, ela registra todas as alterações e não diminui da maneira que você espera. Dependendo do horário de funcionamento, você pode fazer um backup, alternar para o modo de recuperação de envio em massa e depois executar a redução no arquivo de dados. Depois disso, você deseja executar scripts de índice para reparar / reconstruir e voltar à recuperação total. É isso que eu tentaria de qualquer maneira, mas novamente, isso depende do seu horário de operação para tudo isso.

John-Henry Lochbaum
fonte
4
Trazer o modelo de recuperação é interessante. Eu acho que seria mais aplicável se o OP estivesse tendo problemas com o tamanho do arquivo de log. Tal como está, eles estão tendo problemas com o tamanho do arquivo de dados, por isso ficaria surpreso se o modelo de recuperação estivesse causando o problema descrito.
Josh Darnell
É verdade, mas as únicas vezes em que encolhi e isso realmente não impactou o espaço foi por causa do modelo de recuperação, então pensei que valeria a pena mencioná-lo caso fosse um diagnóstico errado.
John-Henry Lochbaum 23/07/19
-3

A única vez em que não consegui reduzir um banco de dados e recuperar espaço é porque você não pode reduzir um banco de dados além do tamanho inicial do banco de dados quando ele foi criado. Por exemplo, se o seu banco de dados for uma cópia do banco de dados de produção e você o criou pela primeira vez em 525 GB, o servidor sql não permitirá reduzir o tamanho abaixo de 525 GB, independentemente da quantidade de dados excluídos do banco de dados. Mas se o banco de dados foi criado abaixo de 383 GB e depois cresceu para 525 GB, você não deve ter problemas para recuperar o espaço. Há muito tempo penso que essa é uma restrição estúpida e arbitrária da Microsoft.

Reduza o banco de dados apenas até o tamanho inicial definido após a criação do banco de dados

ZyxwvuTJ
fonte
A pergunta não é de cerca de encolhimento um banco de dados (e se fosse, a capacidade de reduzi-lo depende do espaço utilizado após a região tamanho inicial)
Eckes
Enquanto houver espaço não utilizado, é possível reduzir o banco de dados para alguns MB, independentemente do tamanho original. Não é necessariamente uma boa ideia, mas tive muitas ocasiões para reduzir bancos de dados e nunca ter um limite como esse.
Ray
-3

Já encontrei esse problema antes nas caixas de produção, o que você precisa fazer é recriar tabelas e índices para cada tabela (nessa ordem).

Aqui está a consulta que eu uso para manter as tabelas sob controle. Isso ajudará a determinar quais tabelas precisam ser reconstruídas e a criar as consultas SQL que você precisa executar. Essa consulta é limitada àqueles com mais de 1 MB de espaço não utilizado e uma proporção de 5% não utilizada, para que você reconstrua apenas o que realmente precisa focar:

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
Luis Alberto Barandiaran
fonte
a reconstrução da tabela, como afirma o OP, eliminaria a maior parte da fragmentação. Duvido que fazer outra reconstrução ajude ainda mais.
Max Vernon