Warum kann das Erstellen einer einfachen CCI-Zeilengruppe bis zu 30 Sekunden dauern?


20

Ich arbeitete an einer Demo mit CCIs, als ich bemerkte, dass einige meiner Beilagen länger als erwartet dauerten. Zu reproduzierende Tabellendefinitionen:

DROP TABLE IF EXISTS dbo.STG_1048576;
CREATE TABLE dbo.STG_1048576 (ID BIGINT NOT NULL);
INSERT INTO dbo.STG_1048576
SELECT TOP (1048576) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

DROP TABLE IF EXISTS dbo.CCI_BIGINT;
CREATE TABLE dbo.CCI_BIGINT (ID BIGINT NOT NULL, INDEX CCI CLUSTERED COLUMNSTORE);

Für die Tests füge ich alle 1048576 Zeilen aus der Staging-Tabelle ein. Das reicht aus, um genau eine komprimierte Zeilengruppe zu füllen, solange sie aus irgendeinem Grund nicht beschnitten wird.

Wenn ich alle Ganzzahlen mod 17000 einfüge, dauert es weniger als eine Sekunde:

TRUNCATE TABLE dbo.CCI_BIGINT;

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT ID % 17000
FROM dbo.STG_1048576
OPTION (MAXDOP 1);

SQL Server-Ausführungszeiten: CPU-Zeit = 359 ms, verstrichene Zeit = 364 ms.

Wenn ich jedoch die gleichen Ganzzahlen mod 16000 einfüge, dauert es manchmal mehr als 30 Sekunden:

TRUNCATE TABLE dbo.CCI_BIGINT;

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT ID % 16000
FROM dbo.STG_1048576
OPTION (MAXDOP 1);

SQL Server-Ausführungszeiten: CPU-Zeit = 32062 ms, verstrichene Zeit = 32511 ms.

Dies ist ein wiederholbarer Test, der auf mehreren Computern durchgeführt wurde. Es scheint ein klares Muster in der verstrichenen Zeit zu geben, wenn sich der Mod-Wert ändert:

MOD_NUM TIME_IN_MS
1000    2036
2000    3857
3000    5463
4000    6930
5000    8414
6000    10270
7000    12350
8000    13936
9000    17470
10000   19946
11000   21373
12000   24950
13000   28677
14000   31030
15000   34040
16000   37000
17000   563
18000   583
19000   576
20000   584

Wenn Sie selbst Tests durchführen möchten, können Sie den Testcode, den ich hier geschrieben habe , ändern .

Ich konnte in sys.dm_os_wait_stats nichts Interessantes für die Mod 16000 einfügen:

╔════════════════════════════════════╦══════════════╗
             wait_type               diff_wait_ms 
╠════════════════════════════════════╬══════════════╣
 XE_DISPATCHER_WAIT                        164406 
 QDS_PERSIST_TASK_MAIN_LOOP_SLEEP          120002 
 LAZYWRITER_SLEEP                           97718 
 LOGMGR_QUEUE                               97298 
 DIRTY_PAGE_POLL                            97254 
 HADR_FILESTREAM_IOMGR_IOCOMPLETION         97111 
 SQLTRACE_INCREMENTAL_FLUSH_SLEEP           96008 
 REQUEST_FOR_DEADLOCK_SEARCH                95001 
 XE_TIMER_EVENT                             94689 
 SLEEP_TASK                                 48308 
 BROKER_TO_FLUSH                            48264 
 CHECKPOINT_QUEUE                           35589 
 SOS_SCHEDULER_YIELD                           13 
╚════════════════════════════════════╩══════════════╝

Warum dauert der Einsatz für ID % 16000so viel länger als der Einsatz für ID % 17000?

Antworten:


12

Dies ist in vielerlei Hinsicht ein erwartetes Verhalten. Jeder Satz von Komprimierungsroutinen weist abhängig von der Verteilung der Eingabedaten eine weitreichende Leistung auf. Wir gehen davon aus, dass die Datenladegeschwindigkeit für die Speichergröße und die Leistung bei der Laufzeitabfrage gleich ist.

Es gibt eine bestimmte Grenze, wie detailliert eine Antwort sein soll, da VertiPaq eine proprietäre Implementierung ist und die Details ein streng gehütetes Geheimnis sind. Trotzdem wissen wir, dass VertiPaq Routinen enthält für:

  • Wertekodierung (Skalieren und / oder Übersetzen von Werten, um in eine kleine Anzahl von Bits zu passen)
  • Wörterbuchcodierung (Ganzzahlverweise auf eindeutige Werte)
  • Lauflängencodierung (Speichern von Läufen mit wiederholten Werten als [Wert, Anzahl] Paare)
  • Bit-Packing (Speichern des Streams in so wenigen Bits wie möglich)

Typischerweise werden Daten mit Werten oder Wörterbüchern codiert, dann wird RLE oder Bit-Packing angewendet (oder eine Mischung aus RLE und Bit-Packing, die in verschiedenen Unterabschnitten der Segmentdaten verwendet wird). Bei der Entscheidung, welche Techniken angewendet werden sollen, kann ein Histogramm erstellt werden, mit dessen Hilfe ermittelt werden kann, wie maximale Biteinsparungen erzielt werden können.

Wenn Sie den langsamen Fall mit Windows Performance Recorder erfassen und das Ergebnis mit Windows Performance Analyzer analysieren, werden Sie feststellen, dass der Großteil der Ausführungszeit für das Clustering der Daten, das Erstellen von Histogrammen und die optimale Partitionierung benötigt wird Ersparnisse:

WPA-Analyse

Die teuerste Verarbeitung erfolgt für Werte, die mindestens 64 Mal im Segment vorkommen. Dies ist eine Heuristik, um zu bestimmen, wann eine reine RLE wahrscheinlich von Vorteil ist. Die schnelleren Fälle führen zu einer unreinen Speicherung, z. B. einer bitweisen Darstellung, mit einer größeren endgültigen Speichergröße. In den hybriden Fällen werden Werte mit 64 oder mehr Wiederholungen RLE-codiert und der Rest wird bitweise gepackt.

Die längste Dauer tritt auf, wenn die maximale Anzahl unterschiedlicher Werte mit 64 Wiederholungen im größtmöglichen Segment auftritt, dh 1.048.576 Zeilen mit 16.384 Wertesätzen mit jeweils 64 Einträgen. Die Überprüfung des Codes ergibt eine fest codierte Frist für die teure Verarbeitung. Dies kann in anderen VertiPaq-Implementierungen, z. B. SSAS, konfiguriert werden, jedoch nicht in SQL Server, soweit ich das beurteilen kann.

Ein Einblick in die endgültige Speicheranordnung kann mit dem Befehl undokumentiert erhalten werdenDBCC CSINDEX . Dies zeigt den RLE-Header und die Array-Einträge, alle Lesezeichen in den RLE-Daten und eine kurze Zusammenfassung der Bit-Pack-Daten (falls vorhanden).

Weitere Informationen finden Sie unter:


9

Ich kann nicht genau sagen, warum dieses Verhalten auftritt, aber ich glaube, ich habe ein gutes Modell des Verhaltens durch Brute-Force-Tests entwickelt. Die folgenden Schlussfolgerungen gelten nur für das Laden von Daten in eine einzelne Spalte und für Ganzzahlen, die sehr gut verteilt sind.

Zuerst habe ich versucht, die Anzahl der in die CCI eingefügten Zeilen mit zu variieren TOP. Ich habe ID % 16000für alle Tests verwendet. Unten sehen Sie ein Diagramm, in dem Zeilen verglichen werden, die mit der Größe des komprimierten Zeilengruppensegments eingefügt wurden:

Grafik von oben vs Größe

Unten sehen Sie eine Grafik der Zeilen, die zur CPU-Zeit in ms eingefügt wurden. Beachten Sie, dass die X-Achse einen anderen Ausgangspunkt hat:

Top gegen CPU

Wir können sehen, dass die Segmentgröße der Zeilengruppe linear ansteigt und eine kleine Menge an CPU bis zu ungefähr 1 Million Zeilen verbraucht. Zu diesem Zeitpunkt nimmt die Zeilengruppengröße drastisch ab und die CPU-Auslastung nimmt drastisch zu. Es scheint, dass wir für diese Komprimierung einen hohen Preis für die CPU zahlen.

Beim Einfügen von weniger als 1024000 Zeilen ergab sich eine offene Zeilengruppe in der CCI. Das Erzwingen der Komprimierung mit REORGANIZEoder hatte REBUILDjedoch keinen Einfluss auf die Größe. Abgesehen davon fand ich es interessant, dass ich, als ich eine Variable verwendete TOP, mit einer offenen, aber RECOMPILEmit einer geschlossenen Zeilengruppe endete.

Als nächstes testete ich, indem ich den Modulwert variierte, während die Anzahl der Zeilen gleich blieb. Hier ein Beispiel der Daten beim Einfügen von 102400 Zeilen:

╔═══════════╦═════════╦═══════════════╦═════════════╗
 TOP_VALUE  MOD_NUM  SIZE_IN_BYTES  CPU_TIME_MS 
╠═══════════╬═════════╬═══════════════╬═════════════╣
    102400     1580          13504          352 
    102400     1590          13584          316 
    102400     1600          13664          317 
    102400     1601          19624          270 
    102400     1602          25568          283 
    102400     1603          31520          286 
    102400     1604          37464          288 
    102400     1605          43408          273 
    102400     1606          49360          269 
    102400     1607          55304          265 
    102400     1608          61256          262 
    102400     1609          67200          255 
    102400     1610          73144          265 
    102400     1620         132616          132 
    102400     1621         138568          100 
    102400     1622         144512           91 
    102400     1623         150464           75 
    102400     1624         156408           60 
    102400     1625         162352           47 
    102400     1626         164712           41 
╚═══════════╩═════════╩═══════════════╩═════════════╝

Bis zu einem mod-Wert von 1600 erhöht sich die Zeilengruppensegmentgröße linear um 80 Byte für jede weiteren 10 eindeutigen Werte. Es ist ein interessanter Zufall, dass a BIGINTtraditionell 8 Bytes belegt und die Segmentgröße für jeden weiteren eindeutigen Wert um 8 Bytes zunimmt. Ab einem Mod-Wert von 1600 nimmt die Segmentgröße schnell zu, bis sie sich stabilisiert.

Es ist auch hilfreich, die Daten zu betrachten, wenn der Modulwert gleich bleibt und die Anzahl der eingefügten Zeilen geändert wird:

╔═══════════╦═════════╦═══════════════╦═════════════╗
 TOP_VALUE  MOD_NUM  SIZE_IN_BYTES  CPU_TIME_MS 
╠═══════════╬═════════╬═══════════════╬═════════════╣
    300000     5000         600656          131 
    305000     5000         610664          124 
    310000     5000         620672          127 
    315000     5000         630680          132 
    320000     5000          40688         2344 
    325000     5000          40696         2577 
    330000     5000          40704         2589 
    335000     5000          40712         2673 
    340000     5000          40728         2715 
    345000     5000          40736         2744 
    350000     5000          40744         2157 
╚═══════════╩═════════╩═══════════════╩═════════════╝

Es sieht so aus, als ob die eingefügte Anzahl der Zeilen <~ 64 * die Anzahl der eindeutigen Werte ist. Wir sehen eine relativ schlechte Komprimierung (2 Bytes pro Zeile für mod <= 65000) und eine niedrige lineare CPU-Auslastung. Wenn die Anzahl der eingefügten Zeilen> ~ 64 * die Anzahl der eindeutigen Werte ist, sehen wir eine viel bessere Komprimierung und eine höhere, immer noch lineare CPU-Auslastung. Es gibt einen Übergang zwischen den beiden Zuständen, der für mich nicht einfach zu modellieren ist, aber in der Grafik zu sehen ist. Es scheint nicht zu stimmen, dass die maximale CPU-Auslastung angezeigt wird, wenn für jeden eindeutigen Wert genau 64 Zeilen eingefügt werden. Wir können nur maximal 1048576 Zeilen in eine Zeilengruppe einfügen und sehen eine viel höhere CPU-Auslastung und -Komprimierung, sobald mehr als 64 Zeilen pro eindeutigem Wert vorhanden sind.

Unten sehen Sie ein Konturdiagramm, wie sich die CPU-Zeit ändert, wenn sich die Anzahl der eingefügten Zeilen und die Anzahl der eindeutigen Zeilen ändert. Wir können die oben beschriebenen Muster sehen:

Kontur-CPU

Unten sehen Sie eine Konturdarstellung des vom Segment verwendeten Raums. Ab einem bestimmten Punkt stellen wir eine viel bessere Komprimierung fest, wie oben beschrieben:

Konturgröße

Anscheinend arbeiten hier mindestens zwei verschiedene Komprimierungsalgorithmen. Vor diesem Hintergrund ist es sinnvoll, dass beim Einfügen von 1048576 Zeilen die maximale CPU-Auslastung angezeigt wird. Es ist auch sinnvoll, dass beim Einfügen von etwa 16000 Zeilen zu diesem Zeitpunkt die höchste CPU-Auslastung festgestellt wird. 1048576/64 = 16384.

Ich habe alle meine Rohdaten hier hochgeladen, falls jemand sie analysieren möchte.

Erwähnenswert ist, was bei Parallelplänen passiert. Ich habe dieses Verhalten nur bei gleichmäßig verteilten Werten beobachtet. Beim parallelen Einfügen kommt es häufig zu einem Zufallselement, und die Threads sind normalerweise nicht ausgeglichen.

Fügen Sie 2097152 Zeilen in die Staging-Tabelle ein:

DROP TABLE IF EXISTS STG_2097152;
CREATE TABLE dbo.STG_2097152 (ID BIGINT NOT NULL);
INSERT INTO dbo.STG_2097152 WITH (TABLOCK)
SELECT TOP (2097152) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

Diese Einlage ist in weniger als einer Sekunde fertig und weist eine schlechte Komprimierung auf:

DROP TABLE IF EXISTS dbo.CCI_BIGINT;
CREATE TABLE dbo.CCI_BIGINT (ID BIGINT NOT NULL, INDEX CCI CLUSTERED COLUMNSTORE);

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT ID % 16000
FROM dbo.STG_2097152 
OPTION (MAXDOP 2);

Wir können die Wirkung der unsymmetrischen Fäden sehen:

╔════════════╦════════════╦══════════════╦═══════════════╗
 state_desc  total_rows  deleted_rows  size_in_bytes 
╠════════════╬════════════╬══════════════╬═══════════════╣
 OPEN             13540             0         311296 
 COMPRESSED     1048576             0        2095872 
 COMPRESSED     1035036             0        2070784 
╚════════════╩════════════╩══════════════╩═══════════════╝

Es gibt verschiedene Tricks, die wir anwenden können, um das Ausbalancieren der Threads und die gleiche Verteilung der Zeilen zu erzwingen. Hier ist einer von ihnen:

DROP TABLE IF EXISTS dbo.CCI_BIGINT;
CREATE TABLE dbo.CCI_BIGINT (ID BIGINT NOT NULL, INDEX CCI CLUSTERED COLUMNSTORE);

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT FLOOR(0.5 * ROW_NUMBER() OVER (ORDER BY (SELECT NULL)))  % 15999
FROM dbo.STG_2097152
OPTION (MAXDOP 2)

Die Wahl einer ungeraden Zahl für den Modul ist hier wichtig. SQL Server durchsucht die Staging-Tabelle seriell, berechnet die Zeilennummer und verwendet dann die Round-Robin-Verteilung, um die Zeilen auf parallelen Threads zu platzieren. Das bedeutet, dass wir am Ende perfekt ausbalancierte Fäden haben.

balance 1

Der Einsatz dauert ungefähr 40 Sekunden, ähnlich dem seriellen Einsatz. Wir erhalten schön komprimierte Zeilengruppen:

╔════════════╦════════════╦══════════════╦═══════════════╗
 state_desc  total_rows  deleted_rows  size_in_bytes 
╠════════════╬════════════╬══════════════╬═══════════════╣
 COMPRESSED     1048576             0         128568 
 COMPRESSED     1048576             0         128568 
╚════════════╩════════════╩══════════════╩═══════════════╝

Wir können die gleichen Ergebnisse erzielen, indem wir Daten aus der ursprünglichen Staging-Tabelle einfügen:

DROP TABLE IF EXISTS dbo.CCI_BIGINT;
CREATE TABLE dbo.CCI_BIGINT (ID BIGINT NOT NULL, INDEX CCI CLUSTERED COLUMNSTORE);

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT t.ID % 16000 ID
FROM  (
    SELECT TOP (2) ID 
    FROM (SELECT 1 ID UNION ALL SELECT 2 ) r
) s
CROSS JOIN dbo.STG_1048576 t
OPTION (MAXDOP 2, NO_PERFORMANCE_SPOOL);

Hier wird die Round-Robin-Verteilung für die abgeleitete Tabelle verwendet, ssodass ein Scan der Tabelle für jeden parallelen Thread durchgeführt wird:

ausgeglichen 2

Wenn Sie also gleichmäßig verteilte Ganzzahlen einfügen, können Sie eine sehr hohe Komprimierung feststellen, wenn jede einzelne Ganzzahl mehr als 64-mal vorkommt. Dies kann auf einen anderen Komprimierungsalgorithmus zurückzuführen sein. Das Erreichen dieser Komprimierung kann hohe CPU-Kosten verursachen. Kleine Änderungen in den Daten können zu dramatischen Unterschieden in der Größe des komprimierten Zeilengruppensegments führen. Ich vermute, dass es zumindest für diesen Datensatz ungewöhnlich sein wird, den Worst-Case (aus Sicht der CPU) zu sehen. Es ist noch schwieriger zu erkennen, wenn parallele Einfügungen vorgenommen werden.


8

Ich glaube, dass dies mit den internen Optimierungen der Komprimierung für die einzelnen Spaltentabellen und der magischen Zahl der 64 KB zu tun hat, die das Wörterbuch belegt.

Beispiel: Wenn Sie mit MOD 16600 arbeiten , beträgt das Endergebnis der Zeilengruppengröße 1,683 MB , während Sie mit MOD 17000 eine Zeilengruppe mit einer Größe von 2,001 MB erhalten .

Schauen Sie sich nun die erstellten Wörterbücher an (Sie können dafür meine CISL-Bibliothek verwenden , Sie benötigen die Funktion cstore_GetDictionaries oder fragen alternativ sys.column_store_dictionaries DMV ab):

(MOD 16600) 61 KB

Bildbeschreibung hier eingeben

(MOD 17000) 65 KB

Bildbeschreibung hier eingeben

Witzige Sache, wenn Sie Ihrer Tabelle eine weitere Spalte hinzufügen und diese als REALID bezeichnen:

DROP TABLE IF EXISTS dbo.CCI_BIGINT;
CREATE TABLE dbo.CCI_BIGINT (ID BIGINT NOT NULL, REALID BIGINT NOT NULL, INDEX CCI CLUSTERED COLUMNSTORE);

Laden Sie die Daten für den MOD 16600 neu:

TRUNCATE TABLE dbo.CCI_BIGINT;

INSERT INTO dbo.CCI_BIGINT WITH (TABLOCK)
SELECT ID % 16600, ID
FROM dbo.STG_1048576
OPTION (MAXDOP 1);

Diesmal ist die Ausführung schnell, da der Optimierer beschließt, sie nicht zu stark zu überarbeiten und zu komprimieren:

select column_id, segment_id, cast(sum(seg.on_disk_size) / 1024. / 1024 as Decimal(8,3) ) as SizeInMB
    from sys.column_store_segments seg
        inner join sys.partitions part
            on seg.hobt_id = part.hobt_id 
    where object_id = object_id('dbo.CCI_BIGINT')
    group by column_id, segment_id;

Auch wenn es einen kleinen Unterschied zwischen den Zeilengruppengrößen gibt, ist dieser vernachlässigbar (2.000 (MOD 16600) gegenüber 2.001 (MOD 17000)).

Für dieses Szenario ist das Wörterbuch für den MOD 16000 größer als für das erste Szenario mit 1 Spalte (0,63 gegenüber 0,61).

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.