Perché possono essere necessari fino a 30 secondi per creare un semplice gruppo di righe CCI?


20

Stavo lavorando a una demo che coinvolge ICC quando ho notato che alcuni dei miei inserti richiedevano più tempo del previsto. Definizioni delle tabelle da riprodurre:

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);

Per i test sto inserendo tutte le 1048576 righe dalla tabella di gestione temporanea. È abbastanza per riempire esattamente un rowgroup compresso purché non venga tagliato per qualche motivo.

Se inserisco tutti i numeri interi mod 17000 ci vuole meno di un secondo:

TRUNCATE TABLE dbo.CCI_BIGINT;

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

Tempi di esecuzione di SQL Server: tempo CPU = 359 ms, tempo trascorso = 364 ms.

Tuttavia, se inserisco gli stessi numeri interi mod 16000 a volte ci vogliono più di 30 secondi:

TRUNCATE TABLE dbo.CCI_BIGINT;

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

Tempi di esecuzione di SQL Server: tempo CPU = 32062 ms, tempo trascorso = 32511 ms.

Questo è un test ripetibile che è stato fatto su più macchine. Sembra che ci sia uno schema chiaro nel tempo trascorso quando il valore della mod cambia:

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

Se vuoi eseguire tu stesso i test, sentiti libero di modificare il codice di test che ho scritto qui .

Non sono riuscito a trovare nulla di interessante in sys.dm_os_wait_stats per l'inserto mod 16000:

╔════════════════════════════════════╦══════════════╗
             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 
╚════════════════════════════════════╩══════════════╝

Perché l'inserto per ID % 16000impiega molto più tempo dell'inserto per ID % 17000?

Risposte:


12

Per molti aspetti, questo è un comportamento previsto. Qualsiasi set di routine di compressione avrà prestazioni ampiamente variabili a seconda della distribuzione dei dati di input. Ci aspettiamo di scambiare la velocità di caricamento dei dati con le dimensioni dello spazio di archiviazione e le prestazioni delle query di runtime.

Esiste un limite definito alla precisione con cui riceverai una risposta, poiché VertiPaq è un'implementazione proprietaria e i dettagli sono un segreto strettamente custodito. Tuttavia, sappiamo che VertiPaq contiene routine per:

  • Codifica del valore (ridimensionamento e / o traduzione dei valori per adattarsi a un numero ridotto di bit)
  • Codifica del dizionario (riferimenti interi a valori univoci)
  • Codifica lunghezza corsa (memorizzazione di serie di valori ripetuti come coppie [valore, conteggio])
  • Bit-packing (memorizzazione del flusso nel minor numero di bit possibile)

In genere, i dati verranno codificati in base al valore o al dizionario, quindi verranno applicati RLE o bit-pack (o un ibrido di RLE e bit-packing utilizzato su diverse sottosezioni dei dati del segmento). Il processo di decisione su quali tecniche applicare può comportare la generazione di un istogramma per determinare come ottenere il massimo risparmio di bit.

Catturando il caso lento con Windows Performance Recorder e analizzando il risultato con Windows Performance Analyzer, possiamo vedere che la maggior parte dei tempi di esecuzione viene consumata guardando il clustering dei dati, costruendo istogrammi e decidendo come partizionarlo al meglio risparmio:

Analisi WPA

L'elaborazione più costosa si verifica per valori che appaiono almeno 64 volte nel segmento. Questo è un metodo euristico per determinare quando è probabile che il puro RLE sia benefico. I casi più veloci comportano una memorizzazione impura , ad esempio una rappresentazione ricca di bit, con una dimensione di archiviazione finale maggiore. Nei casi ibridi, i valori con 64 o più ripetizioni sono codificati RLE e il resto è impacchettato.

La durata più lunga si verifica quando il numero massimo di valori distinti con 64 ripetizioni appare nel segmento più grande possibile, ovvero 1.048.576 righe con 16.384 serie di valori con 64 voci ciascuna. L'ispezione del codice rivela un limite di tempo prestabilito per l'elaborazione costosa. Questo può essere configurato in altre implementazioni di VertiPaq, ad esempio SSAS, ma non in SQL Server per quanto ne so.

Alcune informazioni sulla disposizione finale di archiviazione possono essere acquisite utilizzando il comando non documentatoDBCC CSINDEX . Questo mostra l'intestazione RLE e le voci dell'array, tutti i segnalibri nei dati RLE e un breve riepilogo dei dati del bit-pack (se presenti).

Per ulteriori informazioni, vedere:


9

Non posso dire esattamente perché si stia verificando questo comportamento, ma credo di aver sviluppato un buon modello di comportamento tramite test della forza bruta. Le seguenti conclusioni si applicano solo quando si caricano dati in una singola colonna e con numeri interi molto ben distribuiti.

Innanzitutto ho provato a variare il numero di righe inserite nel CCI usando TOP. Ho usato ID % 16000per tutti i test. Di seguito è riportato un grafico che confronta le righe inserite con la dimensione del segmento del gruppo di righe compresso:

grafico della dimensione superiore vs

Di seguito è riportato un grafico delle righe inserite nel tempo della CPU in ms. Si noti che l'asse X ha un punto di partenza diverso:

top vs cpu

Possiamo vedere che la dimensione del segmento del gruppo di righe aumenta a una velocità lineare e utilizza una piccola quantità di CPU fino a circa 1 milione di righe. A quel punto la dimensione del gruppo di righe diminuisce drasticamente e l'utilizzo della CPU aumenta notevolmente. Sembrerebbe che paghiamo un prezzo pesante in CPU per quella compressione.

Quando ho inserito meno di 1024000 righe ho finito con un rowgroup aperto nel CCI. Tuttavia, forzare la compressione utilizzando REORGANIZEo REBUILDnon ha influito sulla dimensione. Per inciso, ho trovato interessante che quando ho usato una variabile per TOPho finito con un rowgroup aperto ma con RECOMPILEho finito con un rowgroup chiuso.

Successivamente ho testato variando il valore del modulo mantenendo lo stesso numero di righe. Ecco un esempio dei dati quando si inseriscono 102400 righe:

╔═══════════╦═════════╦═══════════════╦═════════════╗
 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 
╚═══════════╩═════════╩═══════════════╩═════════════╝

Fino a un valore mod di 1600 la dimensione del segmento rowgroup aumenta linearmente di 80 byte per ogni 10 valori univoci aggiuntivi. È una coincidenza interessante che BIGINTtradizionalmente occupa 8 byte e la dimensione del segmento aumenta di 8 byte per ogni valore univoco aggiuntivo. Passato un valore mod di 1600, la dimensione del segmento aumenta rapidamente fino a quando non si stabilizza.

È anche utile esaminare i dati quando si lascia lo stesso valore del modulo e si modifica il numero di righe inserite:

╔═══════════╦═════════╦═══════════════╦═════════════╗
 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 
╚═══════════╩═════════╩═══════════════╩═════════════╝

Sembra che quando il numero inserito di righe <~ 64 * è il numero di valori univoci che vediamo una compressione relativamente scarsa (2 byte per riga per mod <= 65000) e un utilizzo lineare e basso della CPU. Quando il numero inserito di righe> ~ 64 * indica il numero di valori univoci, vediamo una compressione molto migliore e un utilizzo della CPU ancora più lineare e lineare. C'è una transizione tra i due stati che non è facile per me modellare ma può essere visto nel grafico. Non sembra vero che vediamo il massimo utilizzo della CPU quando si inseriscono esattamente 64 righe per ogni valore univoco. Piuttosto, possiamo solo inserire un massimo di 1048576 righe in un gruppo di righe e vediamo un utilizzo e una compressione della CPU molto più elevati quando ci sono più di 64 righe per valore univoco.

Di seguito è riportato un diagramma di contorno di come il tempo della cpu cambia al variare del numero di righe inserite e del numero di righe univoche. Possiamo vedere gli schemi sopra descritti:

contour cpu

Di seguito è riportato un diagramma di contorno dello spazio utilizzato dal segmento. Dopo un certo punto iniziamo a vedere una compressione molto migliore, come descritto sopra:

dimensione del contorno

Sembra che ci siano almeno due diversi algoritmi di compressione al lavoro qui. Considerato quanto sopra, ha senso che vedremo il massimo utilizzo della CPU quando si inseriscono 1048576 righe. Ha anche senso che si veda il maggior utilizzo della CPU a quel punto quando si inseriscono circa 16000 righe. 1048576/64 = 16384.

Ho caricato tutti i miei dati grezzi qui nel caso qualcuno volesse analizzarli.

Vale la pena ricordare cosa succede con i piani paralleli. Ho osservato questo comportamento solo con valori distribuiti uniformemente. Quando si fa un inserto parallelo c'è spesso un elemento di casualità e i thread sono generalmente sbilanciati.

Inserisci 2097152 righe nella tabella di gestione temporanea:

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;

Questo inserto termina in meno di un secondo e ha una compressione scarsa:

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);

Possiamo vedere l'effetto dei thread sbilanciati:

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

Ci sono vari trucchi che possiamo fare per forzare l'equilibrio dei thread e avere la stessa distribuzione di righe. Eccone uno:

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)

La scelta di un numero dispari per il modulo è importante qui. SQL Server esegue la scansione della tabella di gestione temporanea in serie, calcola il numero di riga, quindi utilizza la distribuzione round robin per posizionare le righe su thread paralleli. Ciò significa che finiremo con fili perfettamente bilanciati.

saldo 1

L'inserto richiede circa 40 secondi, il che è simile all'inserto seriale. Otteniamo rowgroup ben compressi:

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

È possibile ottenere gli stessi risultati inserendo i dati dalla tabella di gestione temporanea originale:

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);

Qui viene utilizzata la distribuzione round robin per la tabella derivata, squindi viene eseguita una scansione della tabella su ciascun thread parallelo:

bilanciato 2

In conclusione, quando si inseriscono numeri interi distribuiti uniformemente, è possibile vedere una compressione molto elevata quando ciascun numero intero appare più di 64 volte. Ciò può essere dovuto all'uso di un diverso algoritmo di compressione. Ci può essere un costo elevato nella CPU per ottenere questa compressione. Piccole modifiche ai dati possono portare a notevoli differenze nelle dimensioni del segmento di rowgroup compresso. Ho il sospetto che vedere il caso peggiore (dal punto di vista della CPU) sia raro in natura, almeno per questo set di dati. È ancora più difficile vedere quando si eseguono inserti paralleli.


8

Credo che ciò abbia a che fare con le ottimizzazioni interne della compressione per le tabelle a colonna singola e il numero magico dei 64 KB occupati dal dizionario.

Esempio: se si esegue con MOD 16600 , il risultato finale della dimensione del gruppo di righe sarà 1.683 MB , mentre l'esecuzione di MOD 17000 fornirà un gruppo di righe con dimensioni di 2.001 MB .

Ora dai un'occhiata ai dizionari creati (puoi usare la mia libreria CISL per questo, avrai bisogno della funzione cstore_GetDictionaries, o in alternativa vai a query sys.column_store_dictionaries DMV):

(MOD 16600) 61 KB

inserisci qui la descrizione dell'immagine

(MOD 17000) 65 KB

inserisci qui la descrizione dell'immagine

Cosa divertente, se aggiungerai un'altra colonna al tuo tavolo e chiamiamola REALID:

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

Ricarica i dati per MOD 16600:

TRUNCATE TABLE dbo.CCI_BIGINT;

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

Questa volta l'esecuzione sarà veloce, perché l'ottimizzatore deciderà di non sovraccaricare e comprimerlo troppo:

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;

Anche se ci sarà una piccola differenza tra le dimensioni del gruppo di file, sarà trascurabile (2.000 (MOD 16600) vs 2.001 (MOD 17000))

Per questo scenario, il dizionario per il MOD 16000 sarà più grande rispetto al primo scenario con 1 colonna (0,63 vs 0,61).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.