Schnelle Änderung der Spalte NVARCHAR (4000) zu NVARCHAR (260)

12

Ich habe ein Leistungsproblem mit sehr großen Speicherzuweisungen, die diese Tabelle mit ein paar NVARCHAR(4000)Spalten behandeln. Diese Spalten sind niemals größer als NVARCHAR(260).

Verwenden

ALTER TABLE [table] ALTER COLUMN [col] NVARCHAR(260) NULL

Dies führt dazu, dass SQL Server die gesamte Tabelle neu schreibt (und 2x Tabellengröße im Protokollbereich verwendet). Dies sind Milliarden von Zeilen, nur um nichts zu ändern. Dies ist keine Option. Das Erhöhen der Spaltenbreite hat dieses Problem nicht, das Verringern jedoch.

Ich habe versucht, eine Einschränkung zu erstellen, CHECK (DATALENGTH([col]) <= 520)oder CHECK (LEN([col]) <= 260)SQL Server beschließt weiterhin, die gesamte Tabelle neu zu schreiben.

Gibt es eine Möglichkeit, den Spaltendatentyp als reine Metadatenoperation zu ändern? Ohne den Aufwand, die gesamte Tabelle neu zu schreiben? Ich verwende SQL Server 2017 (14.0.2027.2 und 14.0.3192.2).

Hier ist ein Beispiel für eine DDL-Tabelle, die zur Reproduktion verwendet werden soll:

CREATE TABLE [table](
    id INT IDENTITY(1,1) NOT NULL,
    [col] NVARCHAR(4000) NULL,
    CONSTRAINT [PK_test] PRIMARY KEY CLUSTERED (id ASC)
);

Und dann laufen die ALTER.

Nick Whaley
quelle

Antworten:

15

Gibt es eine Möglichkeit, den Spaltendatentyp als reine Metadatenoperation zu ändern?

Ich glaube nicht, so funktioniert das Produkt jetzt. Es gibt einige wirklich gute Umgehungsmöglichkeiten für diese in Joes Antwort vorgeschlagene Einschränkung .

... führt dazu, dass SQL Server die gesamte Tabelle neu schreibt (und 2x Tabellengröße im Protokollbereich verwendet)

Ich werde auf die beiden Teile dieser Aussage getrennt antworten.

Umschreiben der Tabelle

Wie ich bereits erwähnt habe, gibt es keinen Weg, dies zu vermeiden. Das scheint die Realität zu sein, auch wenn es aus unserer Sicht als Kunden keinen Sinn ergibt.

Ein Blick DBCC PAGEvor und nach dem Ändern der Spalte von 4000 auf 260 zeigt, dass alle Daten auf der Datenseite dupliziert wurden (meine Testtabelle hatte 'A'260-mal in der Reihe):

Screenshot des Datenteils der DBCC-Seite davor und danach

Zu diesem Zeitpunkt befinden sich zwei Kopien der exakt gleichen Daten auf der Seite. Die "alte" Spalte wird im Wesentlichen gelöscht (die ID wird von "id = 2" in "id = 67108865" geändert), und die "neue" Version der Spalte wird aktualisiert, um auf den neuen Versatz der Daten auf der Seite zu verweisen:

Screenshot der Spalten-Metadaten-Teile der dbcc-Seite davor und danach

Verwenden von 2x Tabellengröße im Protokollbereich

Durch WITH (ONLINE = ON)das Hinzufügen am Ende der ALTERAnweisung wird die Protokollierungsaktivität um etwa die Hälfte reduziert. Dies ist also eine Verbesserung, die Sie vornehmen können, um die Anzahl der Schreibvorgänge auf die Festplatte bzw. den erforderlichen Speicherplatz zu verringern.

Ich habe dieses Testgeschirr benutzt, um es auszuprobieren:

USE [master];
GO
DROP DATABASE IF EXISTS [248749];
GO
CREATE DATABASE [248749] 
ON PRIMARY 
(
    NAME = N'248749', 
    FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL14.SQL2017\MSSQL\DATA\248749.mdf', 
    SIZE = 2048000KB, 
    FILEGROWTH = 65536KB
)
LOG ON 
(
    NAME = N'248749_log', 
    FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL14.SQL2017\MSSQL\DATA\248749_log.ldf', 
    SIZE = 2048000KB, 
    FILEGROWTH = 65536KB
);
GO
USE [248749];
GO

CREATE TABLE dbo.[table]
(
    id int IDENTITY(1,1) NOT NULL,
    [col] nvarchar (4000) NULL,

    CONSTRAINT [PK_test] PRIMARY KEY CLUSTERED (id ASC)
);

INSERT INTO dbo.[table]
SELECT TOP (1000000)
    REPLICATE(N'A', 260)
FROM master.dbo.spt_values v1
    CROSS JOIN master.dbo.spt_values v2
    CROSS JOIN master.dbo.spt_values v3;
GO

Ich habe sys.dm_io_virtual_file_stats(DB_ID(N'248749'), DEFAULT)vor und nach dem Ausführen der ALTERAnweisung überprüft , und hier sind die Unterschiede:

Standard (Offline) ALTER

  • Datendateischreibvorgänge / geschriebene Bytes: 34.809 / 2.193.801.216
  • Schreibvorgänge / geschriebene Bytes in der Protokolldatei: 40.953 / 1.484.910.080

Online ALTER

  • Schreibvorgänge in Datendateien / geschriebene Bytes: 36.874 / 1.693.745.152 (22,8% Rückgang)
  • Schreibvorgänge in Protokolldateien / geschriebene Bytes: 24.680 / 866.166.272 (41% Rückgang)

Wie Sie sehen, sind die Schreibvorgänge in der Datendatei geringfügig und in der Protokolldatei erheblich zurückgegangen.

Josh Darnell
quelle
15

Ich kenne keinen Weg, um direkt das zu erreichen, wonach Sie hier suchen. Beachten Sie, dass das Abfrageoptimierungsprogramm zu diesem Zeitpunkt nicht intelligent genug ist, um Einschränkungen für Speicherzuweisungsberechnungen zu berücksichtigen, sodass die Einschränkung sowieso nicht geholfen hätte. Einige Methoden, die ein erneutes Schreiben der Tabellendaten vermeiden:

  1. CAST die Spalte als NVARCHAR (260) in allen Codes, die es verwenden. Das Abfrageoptimierungsprogramm berechnet die Speicherzuweisung unter Verwendung des umgesetzten Datentyps anstelle des unformatierten.
  2. Benennen Sie die Tabelle um, und erstellen Sie eine Ansicht, die stattdessen die Umwandlung ausführt. Dies entspricht Option 1, kann jedoch die zu aktualisierende Codemenge begrenzen.
  3. Erstellen Sie eine nicht persistierte berechnete Spalte mit dem richtigen Datentyp und lassen Sie alle Ihre Abfragen aus dieser Spalte anstelle der ursprünglichen auswählen.
  4. Benennen Sie die vorhandene Spalte um und fügen Sie die berechnete Spalte mit dem ursprünglichen Namen hinzu. Passen Sie dann alle Ihre Abfragen an, indem Sie Aktualisierungen oder Einfügungen an der ursprünglichen Spalte vornehmen, um stattdessen den neuen Spaltennamen zu verwenden.
Joe Obbish
quelle
2

Ich war oft in einer ähnlichen Situation.

Schritte :

Fügen Sie eine neue Spalte der gewünschten Breite hinzu

Verwenden Sie einen Cursor mit einigen tausend Iterationen (möglicherweise zehn oder zwanzigtausend) pro Commit, um Daten von der alten Spalte in die neue Spalte zu kopieren

Alte Spalte löschen

Neue Spalte umbenennen in Name der alten Spalte

Tada!

Jonesome setzt Monica wieder ein
quelle
3
Was passiert, wenn einige Datensätze, die Sie bereits kopiert haben, aktualisiert oder gelöscht werden?
George.Palacios
1
Es ist sehr einfach, ein Finale update table set new_col = old_col where new_col <> old_col;vor dem Ablegen zu machen old_col.
Colin 't Hart
1
@ Colin'tHart dieser Ansatz funktioniert nicht mit Millionen von Zeilen ... die Transaktion wird riesig, und es blockiert ....
Jonesome Reinstate Monica
@samsmith Zuerst machst du das, was du oben beschrieben hast. Führen Sie diese Update-Anweisung aus, bevor Sie die ursprüngliche Spalte löschen, wenn in der Zwischenzeit Aktualisierungen an den ursprünglichen Daten vorgenommen wurden. Es sollte nur die wenigen Zeilen betreffen, die geändert wurden. Oder vermisse ich etwas?
Colin 't Hart
Um Zeilen abzudecken, die während des Prozesses aktualisiert wurden where new_col <> old_col, können Sie einen Auslöser hinzufügen, der diese Änderungen im Laufe des Prozesses überträgt und am Ende des Prozesses entfernt, um den vollständigen Scan zu vermeiden, der ohne andere Filterklauseln ausgeführt wird. Immer noch ein potenzieller Leistungstreffer, aber viele kleine Beträge über die Dauer des Prozesses statt eines großen Treffers am Ende, wahrscheinlich (abhängig vom Aktualisierungsmuster Ihrer App für die Tabelle), was insgesamt weitaus weniger ergibt als dieser große Treffer .
David Spillett
1

Nun gibt es eine Alternative je nach verfügbarem Speicherplatz in Ihrer Datenbank.

  1. Erstellen Sie eine genaue Kopie Ihrer Tabelle (z. B. new_table), mit Ausnahme der Spalte, in der Sie von NVARCHAR(4000)bis kürzen NVARCHAR(260):

    CREATE TABLE [new_table](
        id INT IDENTITY(1,1) NOT NULL,
        [col] NVARCHAR(260) NULL,
        CONSTRAINT [PK_test_new] PRIMARY KEY CLUSTERED (id ASC)
    );
  2. In einem Wartungsfenster kopieren Sie die Daten von der "defekten" Tabelle ( table) in die "feste" Tabelle ( new_table) mit einem einfachen Befehl INSERT ... INTO ... SELECT ....:

    SET IDENTITY_INSERT [new_table] ON
    GO
    INSERT id, col INTO [new_table] SELECT id, col from [table]
    GO
    SET IDENTITY_INSERT [new_table] OFF
    GO
  3. Benenne die "kaputte" Tabelle um tablein etwas anderes:

    EXEC sp_rename 'table', 'old_table';  
  4. Benennen Sie die "feste" Tabelle um new_tablein table:

    EXEC sp_rename 'new_table', 'table';  
  5. Wenn alles in Ordnung ist, löschen Sie die "defekte" umbenannte Tabelle:

     DROP TABLE [old_table]
     GO

Es geht los.

Beantwortung Ihrer Fragen

Gibt es eine Möglichkeit, den Spaltendatentyp als reine Metadatenoperation zu ändern?

Nein, derzeit nicht möglich

Ohne den Aufwand, die gesamte Tabelle neu zu schreiben?

Nein.
( Siehe meine Lösung und andere. )

John aka hot2use
quelle
Ihre "Einfügung in Auswahl von" führt bei einer großen Tabelle (Millionen oder Milliarden von Zeilen) zu einer ENORMOUS-Transaktion, die die Datenbank möglicherweise für zehn oder Hunderte von Minuten zum Stillstand bringt. (Sowie die Ldf enorme und möglicherweise brechende Protokoll Versand, wenn in Gebrauch)
Jonesome Reinstate Monica