Dlaczego utworzenie prostej grupy wierszy CCI może potrwać do 30 sekund?

20

Pracowałem nad wersją demonstracyjną z udziałem CCI, kiedy zauważyłem, że niektóre z moich wstawek zajmowały więcej czasu niż oczekiwano. Definicje tabel do odtworzenia:

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

Do testów wstawiam wszystkie 1048576 wierszy z tabeli pomostowej. To wystarczy, aby wypełnić dokładnie jedną skompresowaną grupę wierszy, o ile z jakiegoś powodu nie zostanie przycięta.

Jeśli wstawię wszystkie liczby całkowite mod 17000, zajmie to mniej niż sekundę:

TRUNCATE TABLE dbo.CCI_BIGINT;

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

Czasy wykonania programu SQL Server: czas procesora = 359 ms, czas, który upłynął = 364 ms.

Jeśli jednak wstawię te same liczby całkowite mod 16000, czasami zajmie to ponad 30 sekund:

TRUNCATE TABLE dbo.CCI_BIGINT;

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

Czasy wykonania programu SQL Server: czas procesora = 32062 ms, czas, który upłynął = 32511 ms.

Jest to powtarzalny test, który został wykonany na wielu komputerach. Wydaje się, że istnieje wyraźny wzorzec upływającego czasu, gdy zmienia się wartość mod:

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

Jeśli chcesz sam przeprowadzić testy, możesz zmodyfikować kod testowy, który tu napisałem .

Nie mogłem znaleźć niczego interesującego w sys.dm_os_wait_stats dla wstawki 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 
╚════════════════════════════════════╩══════════════╝

Dlaczego wkładka ID % 16000trwa o wiele dłużej niż wkładka ID % 17000?

Joe Obbish
źródło

Odpowiedzi:

12

Pod wieloma względami jest to oczekiwane zachowanie. Każdy zestaw procedur kompresji będzie miał bardzo różną wydajność w zależności od dystrybucji danych wejściowych. Spodziewamy się handlu szybkością ładowania danych w zależności od wielkości pamięci i wydajności zapytań środowiska wykonawczego.

Istnieje określona granica szczegółowości odpowiedzi, ponieważ tutaj VertiPaq jest zastrzeżoną implementacją, a szczegóły są ściśle strzeżoną tajemnicą. Mimo to wiemy, że VertiPaq zawiera procedury dla:

  • Kodowanie wartości (skalowanie i / lub tłumaczenie wartości w celu dopasowania do niewielkiej liczby bitów)
  • Kodowanie słownika (całkowite odniesienia do unikalnych wartości)
  • Kodowanie długości przebiegu (zapisywanie przebiegów powtarzanych wartości jako par [wartość, liczba])
  • Pakowanie bitów (przechowywanie strumienia w jak najmniejszej liczbie bitów)

Zazwyczaj dane będą kodowane w wartościach lub słownikach, a następnie stosowane będzie RLE lub upakowanie bitów (lub hybryda RLE i upakowanie bitów zastosowane w różnych podsekcjach danych segmentu). Proces decydowania, które techniki zastosować, może obejmować wygenerowanie histogramu, który pomoże ustalić, w jaki sposób można uzyskać maksymalne oszczędności bitów.

Przechwytując powolny przypadek za pomocą narzędzia Windows Performance Recorder i analizując wynik za pomocą narzędzia Windows Performance Analyzer, możemy zauważyć, że zdecydowana większość czasu wykonania jest wykorzystywana na przeglądanie klastrowania danych, tworzenie histogramów i podejmowanie decyzji, jak najlepiej podzielić je na partycje oszczędności:

Analiza WPA

Najdroższe przetwarzanie występuje w przypadku wartości, które pojawiają się co najmniej 64 razy w segmencie. Jest to heurystyka określająca, kiedy czysty RLE może być korzystny. Szybsze przypadki skutkują nieczystym przechowywaniem, np. Bit-upakowanie, z większym końcowym rozmiarem przechowywania. W przypadkach hybrydowych wartości z 64 lub więcej powtórzeniami są kodowane RLE, a reszta jest spakowana bitowo.

Najdłuższy czas trwania występuje, gdy maksymalna liczba różnych wartości z 64 powtórzeniami pojawia się w największym możliwym segmencie, tj. 1 048 576 wierszy z 16 384 zestawami wartości z 64 wpisami każdy. Kontrola kodu ujawnia sztywno określony czas na kosztowne przetwarzanie. Można to skonfigurować w innych implementacjach VertiPaq, np. SSAS, ale nie o ile wiem, w SQL Server.

Pewien wgląd w ostateczną konfigurację pamięci można uzyskać za pomocą nieudokumentowanej DBCC CSINDEXkomendy . To pokazuje pozycje nagłówka i tablicy RLE, wszelkie zakładki do danych RLE oraz krótkie podsumowanie danych pakietu bitów (jeśli istnieją).

Aby uzyskać więcej informacji, zobacz:

Paul White mówi GoFundMonica
źródło
9

Nie mogę dokładnie powiedzieć, dlaczego tak się dzieje, ale wydaje mi się, że opracowałem dobry model tego zachowania za pomocą testów siłowych. Poniższe wnioski dotyczą tylko ładowania danych do pojedynczej kolumny i liczb całkowitych, które są bardzo dobrze rozmieszczone.

Najpierw próbowałem zmienić liczbę wierszy wstawionych do CCI za pomocą TOP. Użyłem ID % 16000do wszystkich testów. Poniżej znajduje się wykres porównujący wiersze wstawione do rozmiaru segmentu skompresowanej grupy wierszy:

wykres góry względem rozmiaru

Poniżej znajduje się wykres wierszy wstawionych do czasu procesora w ms. Zauważ, że oś X ma inny punkt początkowy:

top vs procesor

Widzimy, że rozmiar segmentu grupy rzędów rośnie w tempie liniowym i zużywa niewielką ilość procesora aż do około 1 mln wierszy. W tym momencie rozmiar grupy wierszy dramatycznie spada, a użycie procesora dramatycznie wzrasta. Wydaje się, że za tę kompresję płacimy wysoką cenę procesora.

Wstawiając mniej niż 1024000 wierszy skończyłem z otwartą grupą wierszy w CCI. Wymuszanie kompresji przy użyciu REORGANIZElub REBUILDnie miało wpływu na rozmiar. TOPNawiasem mówiąc, ciekawe było to, że kiedy użyłem zmiennej dla , skończyłem z otwartą grupą wierszy, ale z RECOMPILEskończyłem z zamkniętą grupą wierszy.

Następnie przetestowałem zmieniając wartość modułu, zachowując tę ​​samą liczbę wierszy. Oto próbka danych podczas wstawiania 102400 wierszy:

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

Aż do wartości mod 1600, rozmiar segmentu grupy wierszy zwiększa się liniowo o 80 bajtów na każde dodatkowe 10 unikalnych wartości. To ciekawy zbieg okoliczności, że BIGINTtradycyjnie zajmuje 8 bajtów, a rozmiar segmentu wzrasta o 8 bajtów dla każdej dodatkowej unikalnej wartości. Po przekroczeniu wartości mod wynoszącej 1600 rozmiar segmentu szybko rośnie, aż się ustabilizuje.

Pomocne jest również spojrzenie na dane, gdy pozostawia się wartość modułu równą i zmienia liczbę wstawionych wierszy:

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

Wygląda na to, że gdy wstawiona liczba wierszy <~ 64 * liczba unikalnych wartości widzimy stosunkowo słabą kompresję (2 bajty na wiersz dla mod <= 65000) i niskie, liniowe zużycie procesora. Gdy wstawiona liczba wierszy> ~ 64 * liczba unikalnych wartości, widzimy znacznie lepszą kompresję i wyższe, wciąż liniowe użycie procesora. Pomiędzy tymi dwoma stanami istnieje przejście, które nie jest dla mnie łatwe do modelowania, ale można to zobaczyć na wykresie. Nie wydaje się prawdą, że widzimy maksymalne użycie procesora podczas wstawiania dokładnie 64 wierszy dla każdej unikalnej wartości. Zamiast tego możemy wstawić maksymalnie 1048576 wierszy do grupy wierszy i widzimy znacznie większe użycie procesora i kompresję, gdy jest więcej niż 64 wierszy na unikalną wartość.

Poniżej znajduje się wykres konturowy zmian czasu procesora w miarę zmiany liczby wstawianych wierszy i liczby unikalnych wierszy. Widzimy wzorce opisane powyżej:

konturowy procesor

Poniżej znajduje się konturowa przestrzeń użyta przez segment. Po pewnym momencie zaczynamy widzieć znacznie lepszą kompresję, jak opisano powyżej:

rozmiar konturu

Wygląda na to, że działają tu co najmniej dwa różne algorytmy kompresji. Biorąc pod uwagę powyższe, sensowne jest, abyśmy widzieli maksymalne użycie procesora podczas wstawiania 1048576 wierszy. Sensowne jest również to, że widzimy największe zużycie procesora w tym momencie, gdy wstawiamy około 16000 wierszy. 1048576/64 = 16384.

Upload wszystkie moje dane surowe tutaj w przypadku gdy ktoś chce je analizować.

Warto wspomnieć o tym, co dzieje się z równoległymi planami. Obserwowałem to zachowanie tylko przy równomiernie rozłożonych wartościach. Podczas wstawiania równoległego często występuje element losowości, a wątki są zwykle niezrównoważone.

Umieść 2097152 wierszy w tabeli pomostowej:

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;

Ta wkładka kończy się w mniej niż sekundę i ma słabą kompresję:

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

Widzimy efekt niezrównoważonych wątków:

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

Istnieją różne sztuczki, które możemy zrobić, aby wymusić zrównoważenie wątków i taki sam rozkład rzędów. Oto jeden z nich:

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)

Ważny jest tutaj wybór liczby nieparzystej dla modułu. Program SQL Server skanuje szeregowo tabelę pomostową, oblicza numer wiersza, a następnie używa okrągłej dystrybucji robin do umieszczenia wierszy w równoległych wątkach. Oznacza to, że otrzymamy idealnie zrównoważone wątki.

bilans 1

Wkładka zajmuje około 40 sekund, co jest podobne do wkładki szeregowej. Otrzymujemy ładnie skompresowane grupy wierszy:

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

Możemy uzyskać te same wyniki, wstawiając dane z oryginalnej tabeli pomostowej:

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

W tym przypadku dla tabeli pochodnej wykorzystywany jest okrągły rozkład robin, swięc jeden skan tabeli jest wykonywany dla każdego równoległego wątku:

zrównoważony 2

Podsumowując, podczas wstawiania równomiernie rozmieszczonych liczb całkowitych można zauważyć bardzo wysoką kompresję, gdy każda unikalna liczba całkowita pojawia się więcej niż 64 razy. Przyczyną może być inny algorytm kompresji. Osiągnięcie tej kompresji może wymagać wysokich kosztów procesora. Małe zmiany w danych mogą prowadzić do dramatycznych różnic w rozmiarze skompresowanego segmentu grupy wierszy. Podejrzewam, że widzenie najgorszego przypadku (z perspektywy procesora) będzie rzadkie na wolności, przynajmniej w przypadku tego zestawu danych. Jeszcze trudniej to dostrzec podczas wykonywania równoległych wstawek.

Joe Obbish
źródło
8

Uważam, że ma to związek z wewnętrznymi optymalizacjami kompresji dla tabel jednokolumnowych i magiczną liczbą 64 KB zajmowaną przez słownik.

Przykład: jeśli korzystasz z MOD 16600 , końcowy wynik wielkości grupy wierszy wyniesie 1,683 MB , natomiast uruchomienie MOD 17000 da ci grupę wierszy o wielkości 2,001 MB .

Teraz spójrz na utworzone słowniki (możesz do tego użyć mojej biblioteki CISL , będziesz potrzebować funkcji cstore_GetDictionaries lub alternatywnie przejdź do zapytania sys.column_store_dictionaries DMV):

(MOD 16600) 61 KB

wprowadź opis zdjęcia tutaj

(MOD 17000) 65 KB

wprowadź opis zdjęcia tutaj

Zabawne, jeśli dodasz kolejną kolumnę do swojego stołu i nazwijmy ją PRAWDZIWĄ:

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

Załaduj ponownie dane dla 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);

Tym razem wykonanie będzie szybkie, ponieważ optymalizator zdecyduje się nie przepracowywać i zbyt mocno go skompresować:

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;

Mimo niewielkiej różnicy między wielkościami grupy rzędów, będzie ona nieistotna (2.000 (MOD 16600) vs. 2.001 (MOD 17000))

W tym scenariuszu słownik dla MOD 16000 będzie większy niż w pierwszym scenariuszu z 1 kolumną (0,63 vs 0,61).

Niko Neugebuer
źródło