J'ai une table dans une base de données de production d'une taille de 525 Go, dont 383 Go inutilisés:
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:
Quelques informations sur la table:
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:
ALTER INDEX ALL ON dbo.MyTable REBUILD
. Cela a eu un impact négligeable.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é:
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.
DBCC CLEANTABLE('myDB', 'dbo.myTable')
n'a pas fait de différenceJe 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:
EDIT: voici le rapport d'utilisation du disque et le script de la table:
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:
Et l'utilisation globale du disque:
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!
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:
Fractions de page, où la page doit être divisée pour accueillir de nouvelles insertions dans l'index clusterisé
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;
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.