it-swarm.com.de

Versuche, nicht verwendeten Speicherplatz zurückzugewinnen, führen dazu, dass der verwendete Speicherplatz in SQL Server erheblich zunimmt

Ich habe eine Tabelle in einer Produktionsdatenbank mit einer Größe von 525 GB, von denen 383 GB nicht verwendet werden:

(Unused Space

Ich möchte einen Teil dieses Speicherplatzes zurückfordern, aber bevor ich mich mit der Produktionsdatenbank beschäftige, teste ich einige Strategien an einer identischen Tabelle in einer Testdatenbank mit weniger Daten. Diese Tabelle hat ein ähnliches Problem:

(Unused Space

Einige Informationen zur Tabelle:

  • Der Füllfaktor wird auf 0 gesetzt
  • Es gibt ungefähr 30 Spalten
  • Eine der Spalten ist ein LOB vom Typ Bild und speichert Dateien mit einer Größe von einigen KB bis zu mehreren hundert MB
  • Der Tabelle sind keine hypothetischen Indizes zugeordnet

Auf dem Server wird SQL Server 2017 (RTM-DDR) (KB4505224) - 14.0.2027.2 (X64) ausgeführt. Die Datenbank verwendet das Wiederherstellungsmodell SIMPLE.

Einige Dinge, die ich versucht habe:

  • Neuerstellung der Indizes: ALTER INDEX ALL ON dbo.MyTable REBUILD. Dies hatte einen vernachlässigbaren Einfluss.
  • Reorganisation der Indizes: ALTER INDEX ALL ON dbo.MyTable REORGANIZE WITH(LOB_COMPACTION = ON). Dies hatte einen vernachlässigbaren Einfluss.
  • Kopierte die LOB-Spalte in eine andere Tabelle, löschte die Spalte, erstellte die Spalte neu und kopierte die Daten zurück (wie in diesem Beitrag beschrieben: Freigeben von nicht verwendetem Speicherplatz SQL Server-Tabelle ). Dies verringerte den nicht genutzten Raum, schien ihn jedoch nur in genutzten Raum umzuwandeln:

    (Unused Space

  • Verwendete das Dienstprogramm bcp, um die Tabelle zu exportieren, abzuschneiden und neu zu laden (wie in diesem Beitrag beschrieben: So geben Sie den nicht verwendeten Speicherplatz für eine Tabelle frei ). Dies reduzierte auch den nicht genutzten Raum und vergrößerte den genutzten Raum in einem ähnlichen Ausmaß wie das obige Bild.

  • Obwohl dies nicht empfohlen wird, habe ich die Befehle DBCC SHRINKFILE und DBCC SHRINKDATABASE ausprobiert, aber sie hatten keinen Einfluss auf den nicht verwendeten Speicherplatz.
  • Das Ausführen von DBCC CLEANTABLE('myDB', 'dbo.myTable') machte keinen Unterschied
  • Ich habe all das versucht, sowohl unter Beibehaltung der Bild- und Textdatentypen als auch nach dem Ändern der Datentypen in varbinary (max) und varchar (max).
  • Ich habe versucht, die Daten in eine neue Tabelle in einer neuen Datenbank zu importieren, und dies hat auch nur den nicht verwendeten Speicherplatz in den verwendeten Speicherplatz konvertiert. Ich habe die Details dieses Versuchs in diesem Beitrag umrissen.

Ich möchte diese Versuche nicht in der Produktionsdatenbank durchführen, wenn dies die Ergebnisse sind, die ich erwarten kann.

  1. Warum wird der nicht genutzte Speicherplatz nach einigen dieser Versuche nur in genutzten Speicherplatz umgewandelt? Ich habe das Gefühl, dass ich nicht gut verstehe, was unter der Haube passiert.
  2. Kann ich noch etwas tun, um den nicht genutzten Speicherplatz zu verringern, ohne den verwendeten Speicherplatz zu vergrößern?

BEARBEITEN: Hier ist der Datenträgerverwendungsbericht und das Skript für die Tabelle:

(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

Hier sind die Ergebnisse der Ausführung der Befehle in Max Vernons Antwort:

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

UPDATE:

Ich habe Folgendes ausgeführt, wie von Max Vernon vorgeschlagen:

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

Und hier war die Ausgabe:

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.

Dadurch wurde die Festplattennutzung für die Tabelle aktualisiert:

(enter image description here

Und die gesamte Festplattennutzung:

(enter image description here

Es sieht also so aus, als ob das Problem darin bestand, dass die von SQL Server verfolgte Festplattennutzung nicht mehr mit der tatsächlichen Festplattennutzung synchron war. Ich werde dieses Problem als gelöst betrachten, aber ich würde gerne wissen, warum dies überhaupt passiert wäre!

15
Ken

Ich würde DBCC UPDATEUSAGE als ersten Schritt für die Tabelle ausführen, da die Symptome eine inkonsistente Speicherplatznutzung anzeigen.

DBCC UPDATEUSAGE korrigiert die Zeilen, verwendeten Seiten, reservierten Seiten, Blattseiten und Datenseitenzahlen für jede Partition in einer Tabelle oder einem Index. Wenn die Systemtabellen keine Ungenauigkeiten aufweisen, gibt DBCC UPDATEUSAGE keine Daten zurück. Wenn Ungenauigkeiten gefunden und korrigiert werden und WITH NO_INFOMSGS nicht verwendet wird, gibt DBCC UPDATEUSAGE die Zeilen und Spalten zurück, die in den Systemtabellen aktualisiert werden.

Die Syntax lautet:

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

Nachdem Sie das ausgeführt haben, würde ich EXEC sys.sp_spaceused Für die Tabelle ausführen:

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

Der obige Befehl bietet die Möglichkeit, die Verwendung zu aktualisieren. Da Sie jedoch DBCC UPDATEUSAGE Zuerst manuell ausgeführt haben, belassen Sie diesen Wert einfach auf false. Wenn Sie DBCC UPDATEUSAGE Manuell ausführen, können Sie sehen, ob etwas korrigiert wurde.

Die folgende Abfrage sollte den Prozentsatz der freien Bytes in der Tabelle und den Prozentsatz der freien Seiten in der Tabelle anzeigen. Da die Abfrage eine undokumentierte Funktion verwendet, ist es unklug, auf die Ergebnisse zu zählen, aber sie scheint im Vergleich zur Ausgabe von sys.sp_spaceused Auf hoher Ebene korrekt zu sein.

Wenn der Prozentsatz der freien Bytes deutlich höher ist als der Prozentsatz der freien Seiten, haben Sie viele teilweise leere Seiten.

Teilweise leere Seiten können verschiedene Ursachen haben, darunter:

  1. Seitenteilung, bei der die Seite geteilt werden muss, um neue Einfügungen in den Clustered-Index aufzunehmen

  2. Eine Unfähigkeit, die Seite aufgrund der Spaltengröße mit Spalten zu füllen.

Die Abfrage verwendet die undokumentierte dynamische Verwaltungsfunktion 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

Die Ausgabe sieht aus wie:

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

Ich habe einen Blog-Beitrag geschrieben, der die Funktion hier beschreibt.

In Ihrem Szenario sollten Sie, da Sie ALTER TABLE ... REBUILD Ausgeführt haben, eine sehr niedrige Zahl für TotalEmptyPages sehen, aber ich schätze, Sie haben immer noch ungefähr 72% in BytesFreePercent.

Ich habe Ihr CREATE TABLE - Skript verwendet, um zu versuchen, Ihr Szenario neu zu erstellen.

Dies ist das MCVE Ich benutze:

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

Die folgende Abfrage zeigt eine einzelne Zeile für jede der Tabelle zugewiesene Seite und verwendet dieselbe undokumentierte 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

Die Ausgabe zeigt eine Menge Zeilen an, wenn Sie sie für Ihre reale Tabelle in Ihrer Testumgebung ausführen. Möglicherweise können Sie jedoch erkennen, wo das Problem liegt.

Können Sie das folgende Skript ausführen und die Ergebnisse in Ihrer Frage veröffentlichen? Ich versuche nur sicherzustellen, dass wir auf derselben Seite sind.

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

Eine der Spalten ist ein LOB vom Typ Bild und speichert Dateien mit einer Größe von einigen KB bis zu mehreren hundert MB

Möglicherweise tritt eine interne Fragmentierung auf.
Was ist das Seitenfragmentierung für diese Tabelle?
Und unterscheidet sich die Fragmentierung für die In-Row-Seiten von den Off-Row-Seiten?

Sie sagen, Sie haben Dateien mit einigen KB.
SQL Server speichert alles auf 8060-Byte-Seiten. Das heißt, wenn Sie eine Zeile (oder Daten außerhalb der Zeile) mit 4040 Byte haben und die nächste ähnlich ist, können nicht beide auf dieselbe Seite passen, und Sie verschwenden die Hälfte Ihres Speicherplatzes. Versuchen Sie, Ihre Zeilengröße zu ändern, indem Sie Spalten variabler Länge (z. B. mit dem Bild beginnen) in einer anderen Tabelle speichern.

0
DrTrunks Bell