web-dev-qa-db-fra.com

Les tentatives de récupération d'espace inutilisé entraînent une augmentation significative de l'espace utilisé dans SQL Server

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

Unused Space

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:

Unused Space

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 modèle de récupération SIMPLE.

Certaines choses que j'ai essayées:

  • Reconstruction des index: ALTER INDEX ALL ON dbo.MyTable REBUILD. Cela a eu un impact négligeable.
  • Réorganisation des index: 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 la table SQL Server de l'espace inutilisé ). Cela a diminué l'espace inutilisé, mais il semblait simplement le convertir en espace utilisé:

    Unused Space

  • 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é.
  • L'exécution de 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 ce message .

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:

Disk Usage

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:

enter image description here

Et l'utilisation globale du disque:

enter image description here

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!

15
Ken

Je lancerais DBCC UPDATEUSAGE sur la table dans un premier temps, car les symptômes montrent une utilisation d'espace incohérente.

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 lancerais EXEC sys.sp_spaceused Sur 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 comme vous avez d'abord exécuté DBCC UPDATEUSAGE Manuellement, laissez simplement ce paramètre à false. L'exécution manuelle de DBCC UPDATEUSAGE 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 fonction de gestion dynamique sys.dm_db_database_page_allocations 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 encore environ 72% dans BytesFreePercent.

J'ai utilisé votre script CREATE TABLE Pour tenter de recréer votre scénario.

Ceci est le MCVE 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 beaucoup 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;
10
Max Vernon

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 de page 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 dans la même page et vous gaspillerez la moitié de votre espace. Essayez de modifier la taille de votre ligne en stockant des colonnes de longueur variable (commencez par l'image par exemple) dans un autre tableau.

0
DrTrunks Bell