Das Vorhandensein eines XML-Felds bewirkt, dass sich die meisten Tabellendaten auf LOB_DATA-Seiten befinden (tatsächlich sind ~ 90% der Tabellenseiten LOB_DATA).
Nur die XML-Spalte in der Tabelle zu haben, hat diesen Effekt nicht. Aufgrund des Vorhandenseins von XML- Daten wird unter bestimmten Umständen ein Teil der Daten einer Zeile außerhalb der Zeile auf LOB_DATA-Seiten gespeichert. Und während einer (oder vielleicht mehrere ;-) argumentieren, dass duh, XML
impliziert die Spalte, dass es tatsächlich XML-Daten geben wird, kann nicht garantiert werden, dass die XML-Daten außerhalb der Zeile gespeichert werden müssen: es sei denn, die Zeile ist so gut wie bereits gefüllt Abgesehen davon, dass es sich um XML-Daten handelt, passen kleine Dokumente (bis zu 8000 Byte) möglicherweise in eine Zeile und werden niemals zu einer LOB_DATA-Seite weitergeleitet.
Stimmt es, dass LOB_DATA-Seiten nicht nur aufgrund ihrer Größe langsame Scans verursachen können, sondern auch, weil SQL Server den Clustered-Index nicht effektiv scannen kann, wenn viele LOB_DATA-Seiten in der Tabelle enthalten sind?
Das Scannen bezieht sich auf das Betrachten aller Zeilen. Wenn eine Datenseite gelesen wird, werden natürlich alle In-Row- Daten gelesen, auch wenn Sie eine Teilmenge der Spalten ausgewählt haben. Der Unterschied zu LOB-Daten besteht darin, dass die Daten außerhalb der Zeile nicht gelesen werden, wenn Sie diese Spalte nicht auswählen. Daher ist es nicht wirklich fair, eine Schlussfolgerung darüber zu ziehen, wie effizient SQL Server diesen Clustered Index scannen kann, da Sie dies nicht genau getestet haben (oder die Hälfte davon getestet haben). Sie haben alle Spalten ausgewählt, einschließlich der XML-Spalte. Wie Sie bereits erwähnt haben, befinden sich dort die meisten Daten.
Wir wissen also bereits, dass der SELECT TOP 1000 *
Test nicht nur eine Reihe von 8.000 Datenseiten in einer Reihe las, sondern stattdessen zu anderen Stellen pro Reihe sprang . Die genaue Struktur dieser LOB-Daten kann je nach Größe variieren. Basierend auf den hier gezeigten Untersuchungen ( Wie groß ist der LOB-Zeiger für (MAX) -Typen wie Varchar, Varbinary usw.? ) Gibt es zwei Arten von Off-Row-LOB-Zuweisungen:
- Inline Root - für Daten zwischen 8001 und 40.000 (wirklich 42.000) Bytes, sofern der Speicherplatz dies zulässt, gibt es 1 bis 5 Zeiger (24 - 72 Bytes) IN ROW, die direkt auf die LOB-Seite (n) verweisen.
- TEXT_TREE - für Daten über 42.000 Bytes oder wenn die 1 bis 5 Zeiger nicht in die Zeile passen, gibt es nur einen 24-Byte-Zeiger auf die Startseite einer Liste von Zeigern auf die LOB-Seiten (dh die " text_tree "Seite).
Eine dieser beiden Situationen tritt jedes Mal auf, wenn Sie LOB-Daten abrufen, die größer als 8000 Bytes sind oder einfach nicht in die Zeile passen. Ich habe ein Testskript auf PasteBin.com veröffentlicht ( T-SQL-Skript zum Testen von LOB-Zuweisungen und -Lesevorgängen ), in dem die drei Arten von LOB-Zuweisungen (basierend auf der Größe der Daten) sowie deren Auswirkungen auf logische und angezeigt werden physische liest. Wenn in Ihrem Fall die XML-Daten tatsächlich weniger als 42.000 Byte pro Zeile umfassen, sollte sich keine (oder nur sehr wenige) davon in der am wenigsten effizienten TEXT_TREE-Struktur befinden.
Wenn Sie testen wollen , wie schnell SQL Server scannen , dass Clustered Index, tun das SELECT TOP 1000
aber geben Sie eine oder mehrere Spalten nicht mit dieser XML - Spalte. Wie wirkt sich das auf Ihre Ergebnisse aus? Es sollte ziemlich viel schneller sein.
Wird es als sinnvoll erachtet, eine solche Tabellenstruktur / ein solches Datenmuster zu haben?
Da wir eine unvollständige Beschreibung der tatsächlichen Tabellenstruktur und des Datenmusters haben, ist eine Antwort möglicherweise nicht optimal, abhängig von den fehlenden Details. In diesem Sinne würde ich sagen, dass Ihre Tabellenstruktur oder Ihr Datenmuster offensichtlich nichts Unangemessenes ist.
Ich kann (in ac # app) XML von 20 KB auf ~ 2,5 KB komprimieren und in der Spalte VARBINARY speichern, wodurch die Verwendung von LOB-Datenseiten verhindert wird. Dies beschleunigt SELECTs in meinen Tests um das 20-fache.
Das hat das Auswählen aller Spalten oder sogar nur der XML-Daten (jetzt in VARBINARY
) beschleunigt, aber es schadet tatsächlich Abfragen, bei denen die "XML" -Daten nicht ausgewählt werden. Angenommen, Sie haben ungefähr 50 Bytes in den anderen Spalten und haben einen FILLFACTOR
Wert von 100, dann:
Keine Komprimierung: Für XML
15 KB Daten sollten 2 LOB_DATA-Seiten erforderlich sein, für die dann 2 Zeiger für den Inline-Stamm erforderlich sind. Der erste Zeiger ist 24 Bytes und der zweite 12 Bytes für insgesamt 36 Bytes, die in Reihe für die XML-Daten gespeichert sind. Die Gesamtzeilengröße beträgt 86 Byte, und Sie können ungefähr 93 dieser Zeilen auf eine 8060-Byte-Datenseite einfügen. Daher erfordert 1 Million Zeilen 10.753 Datenseiten.
Benutzerdefinierte Komprimierung: 2,5 KB VARBINARY
Daten passen in eine Zeile. Die Gesamtzeilengröße beträgt 2610 (2,5 * 1024 = 2560) Byte, und Sie können nur 3 dieser Zeilen auf eine 8060-Byte-Datenseite einfügen. Daher erfordert 1 Million Zeilen 333.334 Datenseiten.
Ergo führt die Implementierung einer benutzerdefinierten Komprimierung zu einer 30-fachen Erhöhung der Datenseiten für den Clustered Index. Das bedeutet, dass bei allen Abfragen, die einen Clustered-Index-Scan verwenden, jetzt etwa 322.500 weitere Datenseiten zu lesen sind. Weitere Informationen zu dieser Art der Komprimierung finden Sie im folgenden Abschnitt.
Ich würde davor warnen, Refactoring auf der Grundlage der Leistung von durchzuführen SELECT TOP 1000 *
. Dies ist wahrscheinlich keine Abfrage, die von der Anwendung selbst ausgegeben wird, und sollte nicht als einzige Grundlage für möglicherweise unnötige Optimierungen verwendet werden.
Weitere Informationen und Tests finden Sie im folgenden Abschnitt.
Diese Frage kann nicht definitiv beantwortet werden, aber wir können zumindest einige Fortschritte erzielen und zusätzliche Untersuchungen vorschlagen, um uns dem genauen Problem zu nähern (im Idealfall basierend auf Beweisen).
Was wir wissen:
- Die Tabelle enthält ungefähr 1 Million Zeilen
- Die Tabellengröße beträgt ca. 15 GB
- Tabelle enthält eine
XML
Spalte und mehrere andere Spalten der Typen: INT
, BIGINT
, UNIQUEIDENTIFIER
, „etc.“
XML
Die "Spaltengröße" beträgt im Durchschnitt ca. 15 KB
- Nach der Ausführung
DBCC DROPCLEANBUFFERS
dauert es 20 bis 25 Sekunden, bis die folgende Abfrage abgeschlossen ist:SELECT TOP 1000 * FROM TABLE
- Der Clustered Index wird gescannt
- Fragmentierung auf dem Clustered Index liegt nahe bei 0%
Was wir zu wissen glauben:
- Keine andere Festplattenaktivität außerhalb dieser Abfragen. Bist du sicher? Gibt es Hintergrundoperationen, auch wenn keine anderen Benutzerabfragen vorliegen? Gibt es Prozesse außerhalb von SQL Server, die auf demselben Computer ausgeführt werden, auf dem möglicherweise ein Teil der E / A ausgeführt wird? Es kann sein, dass dies nicht der Fall ist, aber es ist nicht klar, allein basierend auf den bereitgestellten Informationen.
- Es werden 15 MB XML-Daten zurückgegeben. Worauf basiert diese Zahl? Eine Schätzung, die aus den 1000 Zeilen mal dem Durchschnitt von 15.000 XML-Daten pro Zeile abgeleitet wurde? Oder eine programmatische Zusammenfassung dessen, was für diese Abfrage empfangen wurde? Wenn es sich nur um eine Schätzung handelt, würde ich mich nicht darauf verlassen, da die Verteilung der XML-Daten möglicherweise nicht so ist, wie es ein einfacher Durchschnitt impliziert.
XML-Komprimierung könnte helfen. Wie genau würden Sie die Komprimierung in .NET durchführen? Über die Klassen GZipStream oder DeflateStream ? Dies ist keine kostenfreie Option. Es wird sicherlich einige der Daten um einen großen Prozentsatz komprimieren, aber es wird auch mehr CPU erfordern, da Sie jedes Mal einen zusätzlichen Prozess zum Komprimieren / Dekomprimieren der Daten benötigen. Mit diesem Plan können Sie auch Folgendes vollständig entfernen:
- Abfrage der XML - Daten über die
.nodes
, .value
, .query
und .modify
XML - Funktionen.
Indizieren Sie die XML-Daten.
Beachten Sie (da Sie erwähnt haben, dass XML "hochredundant" ist), dass der XML
Datentyp bereits dahingehend optimiert ist, dass er die Element- und Attributnamen in einem Wörterbuch speichert, jedem Element eine ganzzahlige Index-ID zuweist und dann diese ganzzahlige ID verwendet im gesamten Dokument (daher wiederholt es nicht den vollständigen Namen für jede Verwendung, noch wiederholt es ihn erneut als abschließendes Tag für Elemente). Bei den eigentlichen Daten wurden auch externe Leerzeichen entfernt. Dies ist der Grund, warum extrahierte XML-Dokumente ihre ursprüngliche Struktur nicht beibehalten und warum leere Elemente so extrahiert werden, als <element />
ob sie als eingefügt worden wären<element></element>
. Alle Vorteile der Komprimierung über GZip (oder etwas anderes) werden also nur durch die Komprimierung der Element- und / oder Attributwerte erzielt. Dies ist eine viel kleinere Oberfläche, die verbessert werden könnte, als die meisten dies erwarten würden und deren Verlust sich wahrscheinlich nicht lohnt Fähigkeiten wie direkt oben angegeben.
Beachten Sie bitte auch, dass durch das Komprimieren der XML-Daten und das Speichern des VARBINARY(MAX)
Ergebnisses der LOB-Zugriff nicht aufgehoben, sondern nur reduziert wird. Abhängig von der Größe der restlichen Daten in der Zeile passt der komprimierte Wert möglicherweise in die Zeile oder erfordert weiterhin LOB-Seiten.
Diese Informationen sind zwar hilfreich, aber bei weitem nicht genug. Es gibt eine Reihe von Faktoren, die die Abfrageleistung beeinflussen. Daher benötigen wir ein detaillierteres Bild der aktuellen Ereignisse.
Was wir nicht wissen, aber müssen:
- Warum ist die Leistung von
SELECT *
Materie? Ist dies ein Muster, das Sie im Code verwenden. Wenn ja warum?
- Was ist die Leistung beim Auswählen nur der XML-Spalte? Was sind die Statistiken und das Timing, wenn Sie nur Folgendes tun
SELECT TOP 1000 XmlColumn FROM TABLE;
:?
Wie viel von den 20 bis 25 Sekunden, die für die Rückgabe dieser 1000 Zeilen benötigt werden, hängt mit den Netzwerkfaktoren (Abrufen der Daten über die Leitung) zusammen und wie viel mit den Client-Faktoren (Rendern von ca. 15 MB plus dem Rest der nicht zur Verfügung stehenden Daten). XML-Daten in das Grid in SSMS (oder möglicherweise auf Festplatte speichern)?
Das Ausklammern dieser beiden Aspekte des Vorgangs kann manchmal erfolgen, indem die Daten einfach nicht zurückgegeben werden. Man könnte nun überlegen, eine temporäre Tabelle oder eine Tabellenvariable auszuwählen, aber dies würde nur ein paar neue Variablen einführen (z. B. Datenträger-E / A für tempdb
Transaktionsprotokoll-Schreibvorgänge, mögliches automatisches Wachstum von Tempdb-Daten und / oder Protokolldateien) Platz im Buffer Pool, etc). All diese neuen Faktoren können die Abfragezeit tatsächlich verlängern. Stattdessen speichere ich die Spalten normalerweise in Variablen (des entsprechenden Datentyps; nicht SQL_VARIANT
), die mit jeder neuen Zeile (dh SELECT @Column1 = tab.Column1,...
) überschrieben werden .
JEDOCH , wie von @PaulWhite in diesem DBA.StackExchange Q & A wies darauf hin, Logik liest anders , wenn die gleichen LOB - Daten zugreifen , mit zusätzlicher Forschung meiner eigenen geschrieben auf Pastebin ( T-SQL - Skript verschiedene Szenarien zu testen , für LOB liest ) , LOB konsistent zwischen nicht zugegriffen SELECT
, SELECT INTO
, SELECT @XmlVariable = XmlColumn
, SELECT @XmlVariable = XmlColumn.query(N'/')
, und SELECT @NVarCharVariable = CONVERT(NVARCHAR(MAX), XmlColumn)
. Daher sind unsere Optionen hier etwas eingeschränkt, aber hier ist, was getan werden kann:
- Schließen Sie Netzwerkprobleme aus, indem Sie die Abfrage auf dem Server ausführen, auf dem SQL Server ausgeführt wird, entweder in SSMS oder in SQLCMD.EXE.
- Schließen Sie Client-Probleme in SSMS aus, indem Sie unter Abfrageoptionen -> Ergebnisse -> Raster die Option "Ergebnisse nach Ausführung verwerfen" aktivieren. Bitte beachten Sie, dass diese Option ALLE Ausgaben, einschließlich Nachrichten, verhindert, aber dennoch nützlich sein kann, um die Zeit auszuschließen, die SSMS benötigt, um den Speicher für jede Zeile zuzuweisen und ihn dann in das Raster zu zeichnen.
Alternativ können Sie die Abfrage über sqlcmd.exe ausführen und die Ausgabe zu nirgendwo gehen direkt über: -o NUL:
.
- Ist dieser Abfrage ein Wartetyp zugeordnet? Wenn ja, welcher Wartetyp ist das?
Was ist die tatsächliche Datengröße für die XML
Spalten zurückgegeben werden ? Die durchschnittliche Größe dieser Spalte in der gesamten Tabelle spielt keine Rolle, wenn die "TOP 1000" -Zeilen einen unverhältnismäßig großen Teil der Gesamtdaten XML
enthalten. Wenn Sie mehr über die TOP 1000-Zeilen erfahren möchten, schauen Sie sich diese Zeilen an. Bitte führen Sie Folgendes aus:
SELECT TOP 1000 tab.*,
SUM(DATALENGTH(tab.XmlColumn)) / 1024.0 AS [TotalXmlKBytes],
AVG(DATALENGTH(tab.XmlColumn)) / 1024.0 AS [AverageXmlKBytes]
STDEV(DATALENGTH(tab.XmlColumn)) / 1024.0 AS [StandardDeviationForXmlKBytes]
FROM SchemaName.TableName tab;
- Das genaue Tabellenschema. Bitte geben Sie die vollständige
CREATE TABLE
Erklärung einschließlich aller Indizes an.
- Abfrageplan? Kannst du das posten? Diese Information wird wahrscheinlich nichts ändern, aber es ist besser zu wissen, dass dies nicht der Fall ist, als zu erraten, dass dies nicht der Fall ist und falsch ist ;-)
- Befindet sich in der Datendatei eine physische / externe Fragmentierung? Obwohl dies hier möglicherweise kein großer Faktor ist, da Sie "Consumer-Grade-SATA" und nicht SSD oder sogar Super-Expensive-SATA verwenden, wird der Effekt von nicht optimal geordneten Sektoren stärker spürbar sein, insbesondere hinsichtlich der Anzahl dieser Sektoren das muss gelesen werden erhöht.
Was sind die genauen Ergebnisse der folgenden Abfrage:
SELECT * FROM sys.dm_db_index_physical_stats(DB_ID(),
OBJECT_ID(N'dbo.SchemaName.TableName'), 1, 0, N'LIMITED');
AKTUALISIEREN
Mir fiel ein, dass ich versuchen sollte, dieses Szenario zu reproduzieren, um festzustellen, ob ich ein ähnliches Verhalten habe. Also habe ich eine Tabelle mit mehreren Spalten erstellt (ähnlich der vagen Beschreibung in der Frage) und sie dann mit 1 Million Zeilen aufgefüllt. Die XML-Spalte enthält ungefähr 15 KB Daten pro Zeile (siehe Code unten).
Was ich gefunden habe, ist, dass SELECT TOP 1000 * FROM TABLE
das erste Mal in 8 Sekunden und danach jedes Mal 2 - 4 Sekunden abgeschlossen wurden (ja, wird DBCC DROPCLEANBUFFERS
vor jedem Durchlauf der SELECT *
Abfrage ausgeführt). Und mein mehrere Jahre alter Laptop ist nicht schnell: SQL Server 2012 SP2 Developer Edition, 64-Bit, 6 GB RAM, dualer 2,5-GHz-Core i5 und ein SATA-Laufwerk mit 5400 U / min. Ich verwende auch SSMS 2014, SQL Server Express 2014, Chrome und einige andere Dinge.
Aufgrund der Reaktionszeit meines Systems möchte ich wiederholen, dass wir weitere Informationen benötigen (z. B. Angaben zu Tabelle und Daten, Ergebnisse der vorgeschlagenen Tests usw.), um die Ursache für die Reaktionszeit von 20 bis 25 Sekunden einzugrenzen dass du siehst.
SET ANSI_NULLS, NOCOUNT ON;
GO
IF (OBJECT_ID(N'dbo.XmlReadTest') IS NOT NULL)
BEGIN
PRINT N'Dropping table...';
DROP TABLE dbo.XmlReadTest;
END;
PRINT N'Creating table...';
CREATE TABLE dbo.XmlReadTest
(
ID INT NOT NULL IDENTITY(1, 1),
Col2 BIGINT,
Col3 UNIQUEIDENTIFIER,
Col4 DATETIME,
Col5 XML,
CONSTRAINT [PK_XmlReadTest] PRIMARY KEY CLUSTERED ([ID])
);
GO
DECLARE @MaxSets INT = 1000,
@CurrentSet INT = 1;
WHILE (@CurrentSet <= @MaxSets)
BEGIN
RAISERROR(N'Populating data (1000 sets of 1000 rows); Set # %d ...',
10, 1, @CurrentSet) WITH NOWAIT;
INSERT INTO dbo.XmlReadTest (Col2, Col3, Col4, Col5)
SELECT TOP 1000
CONVERT(BIGINT, CRYPT_GEN_RANDOM(8)),
NEWID(),
GETDATE(),
N'<test>'
+ REPLICATE(CONVERT(NVARCHAR(MAX), CRYPT_GEN_RANDOM(1), 2), 3750)
+ N'</test>'
FROM [master].[sys].all_columns sac1;
IF ((@CurrentSet % 100) = 0)
BEGIN
RAISERROR(N'Executing CHECKPOINT ...', 10, 1) WITH NOWAIT;
CHECKPOINT;
END;
SET @CurrentSet += 1;
END;
--
SELECT COUNT(*) FROM dbo.XmlReadTest; -- Verify that we have 1 million rows
-- O.P. states that the "clustered index fragmentation is close to 0%"
ALTER INDEX [PK_XmlReadTest] ON dbo.XmlReadTest REBUILD WITH (FILLFACTOR = 90);
CHECKPOINT;
--
DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS;
SET STATISTICS IO, TIME ON;
SELECT TOP 1000 * FROM dbo.XmlReadTest;
SET STATISTICS IO, TIME OFF;
/*
Scan count 1, logical reads 21, physical reads 1, read-ahead reads 4436,
lob logical reads 5676, lob physical reads 1, lob read-ahead reads 3967.
SQL Server Execution Times:
CPU time = 171 ms, elapsed time = 8329 ms.
*/
Und weil wir die Zeit herausrechnen möchten, die zum Lesen der Nicht-LOB-Seiten benötigt wird, habe ich die folgende Abfrage ausgeführt, um alle außer der XML-Spalte auszuwählen (einer der oben vorgeschlagenen Tests). Dies kehrt ziemlich konsequent in 1,5 Sekunden zurück.
DBCC DROPCLEANBUFFERS WITH NO_INFOMSGS;
SET STATISTICS IO, TIME ON;
SELECT TOP 1000 ID, Col2, Col3, Col4 FROM dbo.XmlReadTest;
SET STATISTICS IO, TIME OFF;
/*
Scan count 1, logical reads 21, physical reads 1, read-ahead reads 4436,
lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 1666 ms.
*/
Fazit (für den Moment)
Aufgrund meines Versuchs, Ihr Szenario neu zu erstellen, können wir weder auf das SATA-Laufwerk noch auf nicht-sequentielle E / A als Hauptursache für die 20 bis 25 Sekunden verweisen, vor allem, weil wir dies immer noch tun Ich weiß nicht, wie schnell die Abfrage zurückkehrt, wenn die XML-Spalte nicht enthalten ist. Und ich war nicht in der Lage, die große Anzahl von Logical Reads (Nicht-LOB) zu reproduzieren, die Sie anzeigen, aber ich habe das Gefühl, dass ich in Anbetracht dessen und der folgenden Aussage mehr Daten zu jeder Zeile hinzufügen muss :
~ 90% der Tabellenseiten sind LOB_DATA
Meine Tabelle enthält 1 Million Zeilen mit jeweils etwas mehr als 15.000 XML-Daten und sys.dm_db_index_physical_stats
zeigt, dass es 2 Millionen LOB_DATA-Seiten gibt. Die restlichen 10% wären dann 222.000 IN_ROW-Datenseiten, von denen ich jedoch nur 11.630 habe. Wir brauchen also noch einmal mehr Informationen über das aktuelle Tabellenschema und die aktuellen Daten.