Les tentatives de récupération d'espace inutilisé augmentent considérablement l'espace utilisé dans SQL Server

15

J'ai une table dans une base de données de production d'une taille de 525 Go, dont 383 Go inutilisés:

Espace inutilisé

Je voudrais récupérer une partie de cet espace, mais, avant de jouer avec la base de données de production, je teste certaines stratégies sur une table identique dans une base de données de test avec moins de données. Ce tableau a un problème similaire:

Espace inutilisé

Quelques informations sur la table:

  • Le facteur de remplissage est défini sur 0
  • Il y a environ 30 colonnes
  • L'une des colonnes est un LOB de type image, et il stocke des fichiers dont la taille varie de quelques Ko à plusieurs centaines de Mo
  • La table n'a aucun index hypothétique qui lui est associé

Le serveur exécute SQL Server 2017 (RTM-GDR) (KB4505224) - 14.0.2027.2 (X64). La base de données utilise le SIMPLEmodèle de récupération.

Certaines choses que j'ai essayées:

  • La reconstruction des index: ALTER INDEX ALL ON dbo.MyTable REBUILD. Cela a eu un impact négligeable.
  • Réorganisant les indices: ALTER INDEX ALL ON dbo.MyTable REORGANIZE WITH(LOB_COMPACTION = ON). Cela a eu un impact négligeable.
  • Copié la colonne LOB dans une autre table, supprimé la colonne, recréé la colonne et recopié les données (comme indiqué dans cet article: Libération de l'espace SQL Server Table SQL Server ). Cela a diminué l'espace inutilisé, mais il semblait simplement le convertir en espace utilisé:

    Espace inutilisé

  • Utilisé l'utilitaire bcp pour exporter la table, la tronquer et la recharger (comme indiqué dans cet article: Comment libérer l'espace inutilisé pour une table ). Cela a également réduit l'espace inutilisé et augmenté l'espace utilisé dans une mesure similaire à l'image ci-dessus.

  • Même si ce n'est pas recommandé, j'ai essayé les commandes DBCC SHRINKFILE et DBCC SHRINKDATABASE, mais elles n'ont eu aucun impact sur l'espace inutilisé.
  • La course DBCC CLEANTABLE('myDB', 'dbo.myTable')n'a pas fait de différence
  • J'ai essayé tout ce qui précède à la fois tout en conservant les types de données image et texte et après avoir changé les types de données en varbinary (max) et varchar (max).
  • J'ai essayé d'importer les données dans une nouvelle table dans une nouvelle base de données, et cela n'a également converti que l'espace inutilisé en espace utilisé. J'ai décrit les détails de cette tentative dans cet article .

Je ne veux pas faire ces tentatives sur la base de données de production si ce sont les résultats auxquels je peux m'attendre, donc:

  1. Pourquoi l'espace inutilisé est-il simplement converti en espace utilisé après certaines de ces tentatives? J'ai l'impression de ne pas bien comprendre ce qui se passe sous le capot.
  2. Puis-je faire autre chose pour réduire l'espace inutilisé sans augmenter l'espace utilisé?

EDIT: voici le rapport d'utilisation du disque et le script de la table:

Utilisation du disque

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

Voici les résultats de l'exécution des commandes dans la réponse de Max Vernon:

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

MISE À JOUR:

J'ai exécuté ce qui suit comme suggéré par Max Vernon:

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

Et voici la sortie:

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.

Cela a mis à jour l'utilisation du disque pour la table:

entrez la description de l'image ici

Et l'utilisation globale du disque:

entrez la description de l'image ici

Ainsi, il semble que le problème était que l'utilisation du disque, telle que suivie par SQL Server, soit complètement désynchronisée avec l'utilisation réelle du disque. Je considérerai ce problème résolu, mais je serais intéressé de savoir pourquoi cela se serait produit en premier lieu!

Ken
la source

Réponses:

10

Je courrais DBCC UPDATEUSAGE sur la table comme une première étape, puisque les symptômes montrent l' utilisation de l' espace incohérent.

DBCC UPDATEUSAGE corrige les lignes, les pages utilisées, les pages réservées, les pages feuilles et le nombre de pages de données pour chaque partition d'une table ou d'un index. S'il n'y a aucune inexactitude dans les tables système, DBCC UPDATEUSAGE ne renvoie aucune donnée. Si des inexactitudes sont trouvées et corrigées et WITH NO_INFOMSGS n'est pas utilisé, DBCC UPDATEUSAGE renvoie les lignes et les colonnes mises à jour dans les tables système.

La syntaxe est:

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

Après avoir exécuté cela, je courrais EXEC sys.sp_spaceusedcontre la table:

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

La commande ci-dessus a la possibilité de mettre à jour l'utilisation, mais étant donné que vous avez d'abord exécuté DBCC UPDATEUSAGEmanuellement, laissez simplement ce paramètre à false. L'exécution DBCC UPDATEUSAGEmanuelle vous permet de voir si quelque chose a été corrigé.

La requête suivante doit afficher le pourcentage d'octets libres dans le tableau et le pourcentage de pages libres dans le tableau. Étant donné que la requête utilise une fonctionnalité non documentée, il n'est pas judicieux de compter sur les résultats, mais elle semble précise par rapport à la sortie de sys.sp_spaceused, à un niveau élevé.

Si le pourcentage d'octets libres est nettement supérieur au pourcentage de pages libres, alors vous avez beaucoup de pages partiellement vides.

Les pages partiellement vides peuvent provenir d'un certain nombre de causes, notamment:

  1. Fractions de page, où la page doit être divisée pour accueillir de nouvelles insertions dans l'index clusterisé

  2. Une incapacité à remplir la page de colonnes en raison de la taille des colonnes.

La requête utilise la sys.dm_db_database_page_allocationsfonction de gestion dynamique non documentée :

;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

La sortie ressemble à:

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

J'ai écrit un article de blog décrivant la fonction ici .

Dans votre scénario, puisque vous avez exécuté ALTER TABLE ... REBUILD, vous devriez voir un nombre très faible pour TotalEmptyPages, mais je suppose que vous aurez toujours environ 72% BytesFreePercent.

J'ai utilisé votre CREATE TABLEscript pour tenter de recréer votre scénario.

Voici le MCVE que j'utilise:

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

La requête suivante affiche une seule ligne pour chaque page allouée à la table et utilise ce même DMV non documenté:

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

La sortie affichera un grand nombre de lignes si vous l'exécutez sur votre vraie table dans votre environnement de test, mais cela peut vous permettre de voir où est le problème.

Pouvez-vous exécuter le script suivant et publier les résultats dans votre question? J'essaie juste de m'assurer que nous sommes sur la même longueur d'onde.

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
la source
2
L'exécution a DBCC UPDATEUSAGEmis à jour l'espace et le nombre de pages inutilisés. Il semble que l'utilisation du disque et les informations de page signalées par SQL Server soient extrêmement désynchronisées - j'ai mis à jour mon message avec les détails. Je suis curieux de savoir comment cela aurait pu se produire en premier lieu, mais au moins le problème a été trouvé. Merci pour toute votre aide, je l'apprécie vraiment!
Ken
0

L'une des colonnes est un LOB de type image, et il stocke des fichiers dont la taille varie de quelques Ko à plusieurs centaines de Mo

Vous pourriez rencontrer une fragmentation interne.
Quelle est la fragmentation des pages pour ce tableau?
Et la fragmentation des lignes en ligne est-elle différente des pages hors ligne?

Vous dites que vous avez des fichiers de quelques Ko.
SQL Server stocke tout dans des pages de 8060 octets. Cela signifie que si vous avez une ligne (ou des données hors ligne) de 4040 octets et que la suivante est similaire, elle ne peut pas tenir toutes les deux sur la même page et vous gaspillerez la moitié de votre espace. Essayez de changer la taille de votre ligne en stockant des colonnes de longueur variable (commencez par l'image par exemple) dans un autre tableau.

DrTrunks Bell
la source
Je ne pense pas que la fragmentation soit le problème. Après avoir reconstruit les index, la fragmentation de l'index cluster est de 0,45% et le nombre de pages est de 98,93%.
Ken
La reconstruction d'une table ou d'un index ne vous aidera pas si vous souffrez de très grandes lignes ou de données LOB qui ne tiennent pas bien dans les pages de 8 Ko. C'est ce que Max Vernon a expliqué plus en détail: "vous avez beaucoup de pages partiellement vides". aussi appelé fragmentation interne
DrTrunks Bell
-3

La base de données est-elle en mode de récupération complète? Si c'est le cas, lorsque vous effectuez une réduction, il enregistre toutes les modifications et ne la réduira pas comme vous l'attendez. En fonction de vos heures d'ouverture, vous pouvez effectuer une sauvegarde, passer en mode de récupération de livraison groupée, puis exécuter la réduction sur le fichier de données. Après cela, vous souhaitez exécuter des scripts d'index pour réparer / reconstruire et revenir à une récupération complète. C'est ce que j'essaierais de toute façon, mais encore une fois, cela dépend de vos heures d'ouverture pour tout cela.

John-Henry Lochbaum
la source
4
Faire apparaître le modèle de récupération est intéressant. Je pense que ce serait plus applicable si l'OP avait des problèmes avec la taille de leur fichier journal. En l'état, ils ont des problèmes avec la taille du fichier de données, donc je serais surpris si le modèle de récupération était à l'origine du problème décrit.
Josh Darnell
C'est vrai, mais les seules fois où j'ai exécuté un rétrécissement et cela n'a pas vraiment eu d'impact sur l'espace était à cause du modèle de récupération, donc j'ai pensé qu'il valait la peine d'être mentionné au cas où il s'agirait d'un diagnostic erroné.
John-Henry Lochbaum
-3

La seule fois où je n'ai pas pu réduire une base de données et récupérer de l'espace, c'est parce que vous ne pouvez pas réduire une base de données au-delà de la taille initiale de la base de données lors de sa création. Ainsi, par exemple, si votre base de données est une copie de la base de données de production et que vous avez d'abord créé la base de données à 525 Go, le serveur SQL ne vous permettra pas de réduire la taille en dessous de 525 Go, peu importe la quantité de données que vous supprimez de la base de données. Mais si la base de données a été créée en dessous de 383 Go et est ensuite passée à 525 Go, vous ne devriez pas avoir de problème à récupérer l'espace. J'ai longtemps pensé que c'était une restriction stupide et arbitraire de Microsoft.

Rétrécir la base de données uniquement jusqu'à sa taille initiale définie après la création de la base de données

ZyxwvuTJ
la source
La question n'est pas de réduire une base de données (et si c'était le cas, la capacité de la réduire dépend de l'espace utilisé après la région de taille initiale)
vérifie
Tant qu'il y a de l'espace inutilisé, il est possible de réduire la base de données à quelques Mo, quelle que soit la taille d'origine. Ce n'est pas nécessairement une bonne idée, mais j'ai eu de nombreuses occasions de réduire les bases de données et de ne jamais rencontrer une telle limite.
Ray
-3

J'ai rencontré ce problème auparavant sur les boîtes de production, ce que vous devez faire est de reconstruire les tables et les index pour chaque table (dans cet ordre).

Voici la requête que j'utilise pour contrôler les tables. Il vous aidera à déterminer quelles tables doivent être reconstruites et à créer les requêtes SQL que vous devez exécuter. Cette requête est limitée à ceux qui ont plus de 1 Mo d'espace inutilisé et un ratio inutilisé de 5%, de sorte que vous ne reconstruisez que ce sur quoi vous devez vraiment vous concentrer:

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
la source
reconstruire la table, comme l'OP le dit, éliminerait l'essentiel de la fragmentation. Je doute que faire une autre reconstruction vous aidera davantage.
Max Vernon