nvarchar (max) Konvertierung in varchar und Tabellenoptimierung


7

Ich arbeite mit einer Tabelle, für die alle Zeichentypen festgelegt nvarcharsind nvarchar(max). Wir konvertieren all dies in varcharund geben eine Zeichenbreite an, die auf der tatsächlichen Verwendung in der Produktion basiert. Die Produktionsdaten verwenden einen Bereich von 2 Zeichen bis zu 900 Zeichen der tatsächlich verwendeten Breite für eine bestimmte Spalte. Wir werden gegebenenfalls eine Polsterung von 10% hinzufügen.

-- Insert statements for procedure here
UPDATE Listings WITH (ROWLOCK) 
SET [SubType] = 'S' 
WHERE @idSettings = idSettings AND
    (@idRetsClass = 0 OR idRetsClass = @idRetsClass)
    AND (@idRetsSetting = 0 OR idRetsSetting = @idRetsSetting)
    AND IsNew = 1 AND ([SubType] LIKE '%Single Family Home%' OR [SubType] LIKE '%Modular%' OR [SubType] LIKE '%Mobile Home%' 
    OR [SubType] LIKE '% Story%' OR [SubType] = '' OR [SubType] = 'residential - S' OR [SubType] = '1 House on Lot' OR [SubType] = '2 Houses on Lot' 
    OR [SubType] = 'Detached' OR [SubType] LIKE '%single family%' OR [SubType] = 'ranch' OR [SubType] = 'Semi-Detached' OR [SubType] = 'single' OR [SubType] = 'one family' OR [SubType] = 'Residential' 
    OR [SubType] = 'Ranch Type' OR [SubType] = '2 or More Stories' OR [SubType] = 'Cape Cod' OR [SubType] = 'Split Level' OR [SubType] = 'Bi-Level' OR [SubType] = 'Detached Single' 
    OR [SubType] = 'Single-Family Homes' OR [SubType] = 'house' OR [SubType] = 'detached housing'  OR [SubType] = 'det')

Eine umfassende Überarbeitung dieser Tabelle, die buchstäblich aus 140 ( nvarchar) Spalten besteht, wobei 11 MAX ist. Ich lösche 30 Indizes und erstelle sie anschließend neu.

Meine Frage ist, in welchen Situationen varchar(max)bevorzugt wird?

Nur wenn Sie 4k oder mehr Zeichen erwarten?

Was soll ich dabei lernen und vorbereiten?

Verbessert dies die Leistung, wenn bei einer Aktualisierung des Clustered-Index, die sich auf den Clustered Key auswirkt, alle nicht Clustered-Indizes aktualisiert werden müssen?

Es gibt ein Zeitlimit für Aktualisierungsverfahren, bei dem 75% bis 95% des Abfrageausführungsplans und des angezeigten Plans für eine Cluster-Indexaktualisierung verwendet werden.

Link zum eigentlichen Ausführungsplan

Antworten:


9

{Dies ist vielleicht etwas langwierig, aber Ihre tatsächlichen Probleme können nicht durch Betrachten von Ausführungsplänen gelöst werden. Es gibt zwei Hauptprobleme, und beide sind architektonisch. }}

Ablenkungen

Beginnen wir mit den Elementen, die nicht Ihre Hauptproblembereiche sind. Dies sind Dinge, die berücksichtigt werden sollten, da dies definitiv dazu beiträgt, die Leistung zu verbessern, wenn Sie die Datentypen verwenden, die Sie benötigen, und nicht nur einen allgemeinen Datentyp, der den meisten entspricht. Es gibt einen sehr guten Grund, warum die verschiedenen Datentypen existieren. Wenn das Speichern von 100 Zeichen NVARCHAR(MAX)keine negativen Auswirkungen auf Abfragen (oder andere Aspekte des Systems) hätte, würde alles als gespeichert NVARCHAR(MAX). Das Aufräumen dieser Bereiche führt jedoch nicht zu einer echten Skalierbarkeit.

zu MAX oder nicht zu MAX

Ich arbeite mit einer Tabelle, für die alle Zeichentypen festgelegt nvarcharsind nvarchar(max).

OK. Dies ist nicht unbedingt eine schlechte Sache, obwohl es meistens mindestens ein Feld gibt, das vom numerischen Typ als ID ist. Für das bisher beschriebene Szenario gibt es jedoch durchaus gültige Fälle. Und MAXFelder sind von Natur aus nicht schlecht , da sie die Daten auf der Datenseite (dh in einer Reihe ) speichern, wenn die Daten dort passen. In dieser Situation sollte es genauso gut funktionieren wie ein Nicht-MAX-Wert desselben Datentyps. Aber ja, eine Reihe von MAXTypfeldern ist ein Zeichen für Schlamperei bei der Datenmodellierung und es ist weitaus wahrscheinlicher, dass die meisten (oder alle) dieser MAXDaten auf separaten Datenseiten (dh außerhalb der Zeile ) gespeichert werden , die eine zusätzliche Suche benötigen, daher weniger effizient.

VARCHAR gegen NVARCHAR

Wir konvertieren all dies in varchar...

Ok, aber warum genau (ja, ich weiß, dass Informationen und Kommentare, die dieser Aussage folgen, Klarheit schaffen, aber ich werde den Konversationsaspekt aus einem bestimmten Grund beibehalten). Jeder Datentyp hat seinen Platz. VARCHARist 1 Byte pro Zeichen und kann (meistens) 256 Zeichen darstellen, wie auf einer einzelnen Codepage definiert . Während die Zeichenwerte 0 - 127 zwischen den Codepages gleich sind, können sich die Zeichenwerte zwischen 128 und 255 ändern:

;WITH chars ([SampleCharacters]) AS
(
  SELECT CHAR(42) + ' '   -- *
       + CHAR(65) + ' '   -- A
       + CHAR(126) + ' '  -- 
   -------------------------------
       + CHAR(128) + ' '  -- €
       + CHAR(149) + ' '  -- •
       + CHAR(165) + ' '  -- ¥, Y, ?
       + CHAR(183) + ' '  -- ·, ?
       + CHAR(229) + ' '  -- å, a, ?
)
SELECT chr.SampleCharacters COLLATE SQL_Latin1_General_CP1_CI_AS AS [SQL_Latin1_General_CP1_CI_AS],
       chr.SampleCharacters COLLATE SQL_Latin1_General_CP1255_CI_AS AS [SQL_Latin1_General_CP1255_CI_AS],
       chr.SampleCharacters COLLATE Thai_CI_AS_KS_WS AS [Thai_CI_AS_KS_WS],
       chr.SampleCharacters COLLATE Yakut_100_CS_AS_KS AS [Yakut_100_CS_AS_KS],
       chr.SampleCharacters COLLATE Albanian_CS_AI AS [Albanian_CS_AI]
FROM   chars chr;

Bitte beachten Sie, dass VARCHARDaten 2 Bytes pro Zeichen aufnehmen und mehr als 256 Zeichen darstellen können. Weitere Informationen zu Doppelbyte-Zeichensätzen finden Sie in der folgenden Antwort: Speichern japanischer Zeichen in einer Tabelle .

NVARCHARwird als UTF-16 (Little Endian) gespeichert und besteht entweder aus 2 oder 4 Bytes pro Zeichen, die das gesamte Unicode-Spektrum darstellen können. Wenn Ihre Daten also jemals mehr Zeichen speichern müssen, als durch eine einzelne Codepage dargestellt werden können, VARCHARhilft Ihnen der Wechsel zu nicht wirklich.

Vor dem Konvertieren in VARCHARmüssen Sie sicherstellen, dass Sie keine Unicode-Zeichen speichern. Versuchen Sie die folgende Abfrage, um festzustellen, ob es Zeilen gibt, in die nicht konvertiert werden kann, VARCHARohne Daten zu verlieren:

SELECT tbl.PKfield, tbl.SubType
FROM   dbo.[Listings] tbl
WHERE  tbl.SubType <> CONVERT(NVARCHAR(MAX), CONVERT(VARCHAR(MAX), tbl.SubType))

Um zu verdeutlichen, wie es NVARCHARfunktioniert: Die maximale Länge eines NVARCHARFeldes ist die Anzahl der 2-Byte- Zeichen. Daher sind NVARCHAR(50)maximal 100 Bytes zulässig. Wie viele Zeichen in diese 100 Bytes passen, hängt davon ab, wie viele 4-Byte-Zeichen vorhanden sind: Mit keinem können Sie alle 50 Zeichen einfügen. Alle 4-Byte-Zeichen passen nur auf 25 Zeichen und viele Kombinationen dazwischen.

Eine weitere Sache, die Sie in Bezug auf den von VARCHARvs belegten Speicherplatz berücksichtigen sollten NVARCHAR: Ab SQL Server 2008 (nur Enterprise- und Developer-Editionen!) Können Sie die Zeilen- oder Seitenkomprimierung für Tabellen, Indizes und indizierte Ansichten aktivieren. In Situationen, in denen ein Großteil der Daten in einem NVARCHARFeld tatsächlich VARCHARohne Datenverlust hineinpassen kann, können durch Komprimierung Zeichen, in die passen VARCHAR, als 1 Byte gespeichert werden. Und nur Zeichen, die entweder 2 oder 4 Bytes benötigen, belegen diesen Platz. Dies sollte einen der größeren Gründe beseitigen, bei denen sich Menschen häufig dafür entscheiden, zu bleiben VARCHAR. Weitere Informationen zur Komprimierung finden Sie auf der MSDN-Seite zum Erstellen komprimierter Tabellen und Indizes . Bitte beachten Sie, dass die Daten inMAX Datentypen, die außerhalb der Zeile gespeichert werden, sind nicht komprimierbar.

Wirkliche Problembereiche

Die folgenden Bereiche sollten angesprochen werden, wenn diese Tabelle wirklich skalierbar sein soll.

Problemo Numero Uno

... und Angabe einer Zeichenbreite basierend auf der tatsächlichen Verwendung in der Produktion. Die Produktionsdaten verwenden einen Bereich von 2 Zeichen bis zu 900 Zeichen der tatsächlich verwendeten Breite für eine bestimmte Spalte. Wir werden gegebenenfalls eine Polsterung von 10% hinzufügen.

UH, was? Haben Sie all diese Werte addiert? Bei der Anzahl der MAXFelder ist es möglich, dass eines oder mehrere dieser Felder 900 Zeichen enthalten. Auch wenn dies 1800 Byte entsprechen sollte, beträgt der auf der Hauptdatenseite gespeicherte Wert nur 24 Byte (nicht immer 24 als Größe) variiert in Bezug auf mehrere Faktoren). Und das könnte der Grund sein, warum es so viele MAXFelder gibt: Sie könnten nicht in ein anderes passen NVARCHAR(100)(bis zu 200 Bytes), aber sie hatten Platz für 24 Bytes.

Wenn das Ziel darin besteht, die Leistung zu verbessern, ist die Konvertierung der vollständigen Zeichenfolgen in Codes auf einigen Ebenen ein Schritt in die richtige Richtung. Sie reduzieren die Größe jeder Zeile drastisch, was für den Pufferpool und die Festplatten-E / A effizienter ist. Und kürzere Saiten brauchen weniger Zeit zum Vergleichen. Das ist gut, aber nicht großartig.

Wenn das Ziel darin besteht, die Leistung dramatisch zu verbessern, ist die Konvertierung in Codes der falsche Schritt in die richtige Richtung. Es basiert immer noch auf stringbasierten Scans (mit 30 Indizes und 140 Spalten sollte es viele Scans geben, es sei denn, die meisten Felder werden nicht zum Filtern verwendet), und ich gehe davon aus, dass dies bei sensitiven Scans der Fall sein wird , die weniger effizient sind, als wenn sie zwischen Groß- und Kleinschreibung unterscheiden oder binär sind (dh wenn eine Kollatierung zwischen Groß- und Kleinschreibung oder Binär verwendet wird).

Darüber hinaus fehlt bei der Konvertierung in Zeichenfolgen-basierte Codes letztendlich der Punkt, wie ein Transaktionssystem richtig optimiert werden kann. Werden diese Codes in ein Suchformular eingegeben? Es ist weitaus weniger sinnvoll, Menschen 'S'für die Verwendung zu [SubType]haben, als weiter zu suchen 'Single Family'.

Es gibt eine Möglichkeit, Ihren vollständigen beschreibenden Text beizubehalten und gleichzeitig den verwendeten Speicherplatz zu reduzieren und Abfragen erheblich zu beschleunigen: Erstellen Sie eine Nachschlagetabelle. Sie sollten eine Tabelle mit dem Namen haben [SubType], in der jeder der beschreibenden Begriffe eindeutig gespeichert ist und [SubTypeID]für die jeweils eine vorhanden ist. Wenn der Datenteil des Systems (dh eine ist enum), dann wird das [SubTypeID]sollte Feld nicht ein sein IDENTITYFeld , da die Daten über ein Release Script bevölkern bekommen sollen. Wenn die Werte von Endbenutzern eingegeben werden, dann wird das [SubTypeID]Feld sollte eine Identität sein. In beiden Situationen:

  • [SubTypeID] ist der Primärschlüssel.
  • Höchstwahrscheinlich verwenden INTfür [SubTypeID].
  • Wenn es sich bei den Daten um interne Daten / Systemdaten handelt und Sie wissen, dass die maximale Anzahl unterschiedlicher Werte immer unter 40 KB liegt, können Sie möglicherweise davonkommen SMALLINT. Wenn Sie mit der Nummerierung bei 1 beginnen (entweder manuell oder über IDENTITY-Startwert), erhalten Sie maximal 32.768. Wenn Sie jedoch mit dem niedrigsten Wert von -32.768 beginnen, erhalten Sie die vollen 65.535 zu verwendenden Werte.
  • Wenn Sie Enterprise Edition verwenden, aktivieren Sie die Zeilen- oder Seitenkomprimierung
  • Beschreibendes Textfeld kann entweder [SubType](wie der Tabellenname) oder vielleicht aufgerufen werden[SubTypeDescription]
  • UNIQUE INDEXauf [SubTypeDescription]. Beachten Sie, dass Indizes eine maximale Größe von 900 Byte haben . Wenn die maximale Länge dieser Daten in Production 900 Zeichen beträgt und Sie dies benötigen NVARCHAR, funktioniert dies möglicherweise mit aktivierter Komprimierung ODER VARCHARnur, wenn Sie definitiv KEINE Unicode-Zeichen speichern müssen. ELSE erzwingt die Eindeutigkeit über einen AFTER INSERT, UPDATETrigger.
  • [Listings]Tabelle hat [SubTypeID]Feld.
  • [SubTypeID]Feld in der [Listings]Tabelle ist Fremdschlüssel, Referenzierung [SubType].[SubTypeID].
  • Abfragen können nun JOINdie [SubType]und [Listings]Tabellen und suchen auf dem vollständigen Wortlaut der [SubTypeDescription](Groß- und Kleinschreibung, auch, gleich wie aktuelle Funktionalität), während dieser ID mit in eine sehr effiziente Suche auf dem indizierten FK Feld auszuführen [Listings].

Dieser Ansatz kann (und sollte) auf andere Felder in dieser Tabelle (und andere Tabellen) angewendet werden, die sich ähnlich verhalten.

Problemo Numero Dos

Eine umfassende Überarbeitung dieser Tabelle, die buchstäblich aus 140 (nvarchar) Spalten besteht, wobei 11 MAX ist. Ich lösche 30 Indizes und erstelle sie anschließend neu.

Wenn dies ein Transaktionssystem und kein Data Warehouse ist, würde ich sagen, dass (wieder allgemein) 140 Spalten zu viel sind, um effizient verarbeitet zu werden. Ich bezweifle sehr, dass alle 140 Felder gleichzeitig verwendet werden und / oder dieselben Anwendungsfälle haben. Die Tatsache, dass 11 sind, MAXist irrelevant, wenn sie mehr als 4000 Zeichen enthalten müssen. ABER 30 Indizes in einer Transaktionstabelle zu haben, ist wieder etwas unhandlich (wie Sie deutlich sehen).

Gibt es einen technischen Grund, warum die Tabelle alle 140 Felder enthalten muss? Können diese Felder in einige kleinere Gruppen aufgeteilt werden? Folgendes berücksichtigen:

  • Suchen Sie die "Kern" -Felder (die wichtigsten / am häufigsten verwendeten) und fügen Sie sie in die "Haupt" -Tabelle mit dem Namen ein [Listing](ich bevorzuge es, einzelne Wörter beizubehalten, damit das ID-Feld leicht gerecht sein kann TableName + "ID").
  • [Listing] Tabelle hat diese PK: [ListingID] INT IDENTITY(1, 1) NOT NULL CONSTRAINT [PK_Listing] PRIMARY KEY
  • "sekundäre" Tabellen werden benannt als [Listing{GroupName}](z. B. [ListingPropertyAttribute]"Attribute" wie in: NumberOfBedrooms, NumberOfBathrooms usw.).
  • [ListingPropertyAttribute] Tabelle hat diese PK: [ListingID] INT NOT NULL CONSTRAINT [PK_ListingPropertyAttribute] PRIMARY KEY, CONSTRAINT [FK_ListingPropertyAttribute_Listing] FOREIGN KEY REFERENCES([Listing].[ListingID])
    • beachte nein IDENTITYhier
    • Beachten Sie, dass der PK-Name zwischen "Core" - und "Secondary" -Tabellen identisch ist
    • Beachten Sie, dass PK und FK zur "Kerntabelle" dasselbe Feld sind
  • Die "Kerntabelle" [Listing]erhält beide [CreatedDate]und [LastModifiedDate]Felder
  • "sekundäre" Tabellen erhalten nur [LastModifiedDate]Feld. Die Annahme ist, dass alle sekundären Tabellen ihre Zeilen gleichzeitig mit der "Kerntabelle" füllen (dh alle Zeilen sollten immer in allen "sekundären" Tabellen dargestellt werden). Daher ist der [CreatedDate]Wert in der "Kern" [Listing]-Tabelle für alle "sekundären" Tabellen pro Zeile immer gleich, sodass er nicht über die "sekundären" Tabellen hinweg dupliziert werden muss. Sie können jedoch jeweils zu unterschiedlichen Zeiten aktualisiert werden.

Diese Struktur erhöht die Anzahl der JOINs, die für viele Abfragen erforderlich sind. Es können jedoch eine oder mehrere Ansichten erstellt werden, um die am häufigsten verwendeten JOINs zu kapseln. Aber auf der positiven Seite:

  • Wenn es um DML-Anweisungen geht, sollte es viel weniger Konflikte geben, da die "Kern" -Tabelle die meisten Aktualisierungen erhalten sollte.
  • Die meisten Updates benötigen weniger Zeit, da sie kleinere Zeilen ändern.
  • Die Indexpflege für jede der neuen Tabellen (sowohl "Kern" - als auch "Sekundär" -Tabellen) sollte zumindest pro Tabelle schneller sein.

Rekapitulieren

Das aktuelle Modell ist so konzipiert, dass es ineffizient ist, und es scheint dieses Entwurfsziel zu erfüllen (dh es ist langsam). Wenn Sie möchten, dass das System schnell ist, muss das Datenmodell effizient und nicht nur weniger ineffizient sein.


4

In welchen Situationen wird varchar (max) bevorzugt

Kommentatoren haben diesen Punkt bereits ausführlich angesprochen. Ich würde sagen, dass dies im VARCHAR(MAX)Allgemeinen sinnvoll ist, wenn Sie zu 100% sicher sind, dass die Spalte niemals Nicht-ASCII-Zeichen benötigt und die maximale Länge der Spalte entweder unbekannt ist oder mehr als 8.000 Zeichen beträgt. Sie können /programming/7141402/why-not-use-varcharmax für eine ähnliche Frage lesen .

Das Zeitlimit für Aktualisierungsverfahren ist abgelaufen

Basierend auf dem Ausführungsplan denke ich, dass ein Hauptfaktor, der die Leistung Ihres Updates beeinflusst, darin bestehen könnte, dass Sie einen Volltextindex für die zu aktualisierende Spalte haben und diesen CHANGE_TRACKING = AUTOfür diesen Volltextindex verwenden.

Das Skript am Ende dieser Antwort zeigt eine einfache Aktualisierungsanweisung für eine bescheidene Anzahl von Zeilen, bei denen die Leistung durch Hinzufügen eines solchen Volltextindex von 19 ms auf über 500 ms steigt.

Abhängig von den geschäftlichen Anforderungen für Ihre Volltextsuche können Sie möglicherweise den Volltextindex erstellen CHANGE_TRACKING = OFF(mit dem dieser Aufwand nicht verbunden ist) und regelmäßig ausführen ALTER FULLTEXT INDEX...START FULL POPULATIONoder START INCREMENTAL POPULATIONdie Daten der Tabelle mit dem Volltextsindex synchronisieren . Den BOL-Artikel finden Sie hier: https://msdn.microsoft.com/en-us/library/ms187317.aspx

-- Create test data on a new database
CREATE DATABASE TestFullTextUpdate
GO
USE TestFullTextUpdate
GO
CREATE TABLE dbo.fulltextUpdateTest (
    id INT NOT NULL IDENTITY(1,1)
        CONSTRAINT PK_fulltextUpdateTest PRIMARY KEY,
    object_counter_name NVARCHAR(512) NOT NULL
)
GO

--13660 row(s) affected
INSERT INTO dbo.fulltextUpdateTest (object_counter_name)
SELECT object_name + ': ' + counter_name
FROM sys.dm_os_performance_counters
CROSS JOIN (SELECT TOP 10 * FROM master..spt_values) x10
GO

-- ~19ms per update
DECLARE @startDate DATETIME2 = GETDATE()
SET NOCOUNT ON
UPDATE dbo.fulltextUpdateTest SET object_counter_name = object_counter_name
SET NOCOUNT OFF
DECLARE @endDate DATETIME2 = GETDATE()
PRINT(DATEDIFF(ms, @startDate, @endDate))
GO 10

-- Add a fulltext index with the default change-tracking behavior
CREATE FULLTEXT CATALOG DefaultFulltextCatalog AS DEFAULT AUTHORIZATION dbo
GO
CREATE FULLTEXT INDEX ON dbo.fulltextUpdateTest (object_counter_name)
KEY INDEX PK_fulltextUpdateTest
WITH CHANGE_TRACKING = AUTO
GO

-- ~522ms per update
-- Execution plan, like the plan in your question, shows that we must
-- touch the fulltext_index_docidstatus for each row that is updated
DECLARE @startDate DATETIME2 = GETDATE()
SET NOCOUNT ON
UPDATE dbo.fulltextUpdateTest SET object_counter_name = object_counter_name
SET NOCOUNT OFF
DECLARE @endDate DATETIME2 = GETDATE()
PRINT(DATEDIFF(ms, @startDate, @endDate))
GO 10

-- Cleanup
USE master
GO
DROP DATABASE TestFullTextUpdate
GO
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.