Database
 sql >> Baza danych >  >> RDS >> Database

Normalizacja i wydajność w trybie wsadowym

Krótko wspomniałem, że dane w trybie wsadowym są znormalizowane w moim ostatnim artykule Bitmapy trybu wsadowego w SQL Server. Wszystkie dane w partii są reprezentowane przez ośmiobajtową wartość w tym konkretnym znormalizowanym formacie, niezależnie od bazowego typu danych.

To stwierdzenie bez wątpienia rodzi pewne pytania, nie tylko o to, jak dane o długości znacznie większej niż osiem bajtów mogą być w ten sposób przechowywane. W tym artykule omówiono znormalizowaną reprezentację danych wsadowych, wyjaśniono, dlaczego nie wszystkie ośmiobajtowe typy danych mogą zmieścić się w 64 bitach, i pokazano przykład, jak to wszystko wpływa na wydajność w trybie wsadowym.

Demo

Zacznę od przykładu, który pokazuje format danych wsadowych, który ma istotny wpływ na plan wykonania. Będziesz potrzebować SQL Server 2016 (lub nowszy) i Developer Edition (lub odpowiednik), aby odtworzyć pokazane tutaj wyniki.

Pierwszą rzeczą, której będziemy potrzebować, jest tabela bigint numery od 1 do 102 400 włącznie. Liczby te zostaną wkrótce użyte do zapełnienia tabeli magazynu kolumn (liczba wierszy to minimum potrzebne do uzyskania pojedynczego skompresowanego segmentu).

DROP TABLE IF EXISTS #Numbers;
GO
CREATE TABLE #Numbers (n bigint NOT NULL PRIMARY KEY);
GO
INSERT #Numbers (n)
SELECT
    n = ROW_NUMBER() OVER (ORDER BY @@SPID)
FROM master.dbo.spt_values AS SV1
CROSS JOIN master.dbo.spt_values AS SV2
ORDER BY
    n
OFFSET 0 ROWS
FETCH FIRST 102400 ROWS ONLY
OPTION (MAXDOP 1);

Pomyślne zagregowane pushdown

Poniższy skrypt używa tabeli liczb do utworzenia kolejnej tabeli zawierającej te same liczby przesunięte o określoną wartość. Ta tabela wykorzystuje magazyn kolumn jako swoją główną pamięć masową w celu późniejszego wykonania w trybie wsadowym.

DROP TABLE IF EXISTS #T;
GO
CREATE TABLE #T (c1 bigint NOT NULL);
GO
DECLARE
    @Start bigint = CONVERT(bigint, -4611686018427387905);
 
INSERT #T (c1)
SELECT 
    c1 = @Start + N.n
FROM #Numbers AS N;
GO
CREATE CLUSTERED COLUMNSTORE INDEX c ON #T
WITH (MAXDOP = 1);

Uruchom następujące zapytania testowe w nowej tabeli magazynu kolumn:

SELECT 
    c = COUNT_BIG(*) 
FROM #T AS T
WHERE 1 = (SELECT 1) -- Avoid trivial plan
OPTION (MAXDOP 1);
 
SELECT 
    m = MAX(T.c1) 
FROM #T AS T 
WHERE 1 = (SELECT 1) -- Avoid trivial plan
OPTION (MAXDOP 1);
 
SELECT 
    s = SUM(T.c1 + CONVERT(bigint, 4611686018427387904))
FROM #T AS T 
WHERE 1 = (SELECT 1) -- Avoid trivial plan
OPTION (MAXDOP 1);

Dodatek wewnątrz SUM jest uniknięcie przepełnienia. Możesz pominąć WHERE klauzule (aby uniknąć trywialnego planu), jeśli używasz SQL Server 2017.

Wszystkie te zapytania korzystają z agregacji pushdown. Suma jest obliczana w Skanowaniu indeksu kolumn zamiast trybu wsadowego Hash Aggregate operator. Plany powykonawcze pokazują zero wierszy emitowanych przez skanowanie. Wszystkie 102 400 wierszy zostało „zagregowanych lokalnie”.

SUM plan jest pokazany poniżej jako przykład:

Nieudane zagregowane pushdown

Teraz upuść, a następnie odtwórz tabelę testową magazynu kolumn z przesunięciem zmniejszonym o jeden:

DROP TABLE IF EXISTS #T;
GO
CREATE TABLE #T (c1 bigint NOT NULL);
GO
DECLARE 
    -- Note this value has decreased by one
    @Start bigint = CONVERT(bigint, -4611686018427387906);
 
INSERT #T (c1)
SELECT 
    c1 = @Start + N.n
FROM #Numbers AS N;
GO
CREATE CLUSTERED COLUMNSTORE INDEX c ON #T
WITH (MAXDOP = 1);

Uruchom dokładnie te same agregujące zapytania testowe pushdown co poprzednio:

SELECT 
    c = COUNT_BIG(*) 
FROM #T AS T
WHERE 1 = (SELECT 1) -- Avoid trivial plan
OPTION (MAXDOP 1);
 
SELECT 
    m = MAX(T.c1) 
FROM #T AS T 
WHERE 1 = (SELECT 1) -- Avoid trivial plan
OPTION (MAXDOP 1);
 
SELECT 
    s = SUM(T.c1 + CONVERT(bigint, 4611686018427387904))
FROM #T AS T 
WHERE 1 = (SELECT 1) -- Avoid trivial plan
OPTION (MAXDOP 1);

Tym razem tylko COUNT_BIG agregacja osiąga agregację pushdown (tylko SQL Server 2017). MAX i SUM agregaty nie. Oto nowa SUM plan do porównania z tym z pierwszego testu:

Wszystkie 102 400 wierszy (w 114 partiach) jest emitowanych przez Skanowanie indeksu kolumnowego , przetwarzane przez Compute Scalar i wysłane do Hash Aggregate .

Dlaczego różnica? Wszystko, co zrobiliśmy, to przesunięcie zakresu liczb przechowywanych w tabeli magazynu kolumn o jeden!

Wyjaśnienie

Wspomniałem we wstępie, że nie wszystkie ośmiobajtowe typy danych mieszczą się w 64 bitach. Ten fakt jest ważny ponieważ wiele optymalizacji wydajności magazynu kolumn i trybu wsadowego działa tylko z danymi o rozmiarze 64-bitowym. Jedną z tych rzeczy jest agregacja pushdown. Istnieje wiele innych funkcji wydajności (nie wszystkie udokumentowane), które działają najlepiej (lub w ogóle) tylko wtedy, gdy dane mieszczą się w 64 bitach.

W naszym konkretnym przykładzie zagregowane przesuwanie w dół jest wyłączone dla segmentu magazynu kolumn, gdy zawiera nawet jeden wartość danych, która nie mieści się w 64 bitach. SQL Server może to określić na podstawie metadanych o minimalnej i maksymalnej wartości skojarzonych z każdym segmentem bez sprawdzania wszystkich danych. Każdy segment jest oceniany osobno.

Zagregowane przesuwanie w dół nadal działa dla COUNT_BIG agregat dopiero w drugim teście. Jest to optymalizacja dodana w pewnym momencie w SQL Server 2017 (moje testy były prowadzone na CU16). Logiczne jest, aby nie wyłączać agregacji pushdown, gdy liczymy tylko wiersze i nie robimy nic z określonymi wartościami danych. Nie mogłem znaleźć żadnej dokumentacji dotyczącej tego ulepszenia, ale w dzisiejszych czasach nie jest to takie niezwykłe.

Na marginesie zauważyłem, że SQL Server 2017 CU16 umożliwia agregację pushdown dla wcześniej nieobsługiwanych typów danych real , float , datetimeoffset i numeric z precyzją większą niż 18 — gdy dane mieszczą się w 64 bitach. Jest to również nieudokumentowane w momencie pisania.

OK, ale dlaczego?

Możesz zadać bardzo rozsądne pytanie:Dlaczego jeden zestaw bigint wartości testowe najwyraźniej mieszczą się w 64 bitach, ale inne nie?

Jeśli zgadłeś, że przyczyna była związana z NULL , daj sobie kleszcza. Mimo że kolumna tabeli testowej jest zdefiniowana jako NOT NULL , SQL Server używa tego samego znormalizowanego układu danych dla bigint czy dane dopuszczają wartości null, czy nie. Są ku temu powody, które będę stopniowo rozpakowywać.

Zacznę od kilku obserwacji:

  • Każda wartość kolumny w partii jest przechowywana w dokładnie ośmiu bajtach (64 bity), niezależnie od bazowego typu danych. Ten układ o stałym rozmiarze sprawia, że ​​wszystko jest łatwiejsze i szybsze. Wykonanie w trybie wsadowym polega na szybkości.
  • Pakiet ma rozmiar 64 KB i zawiera od 64 do 900 wierszy, w zależności od liczby rzutowanych kolumn. Ma to sens, biorąc pod uwagę, że rozmiary danych kolumn są ustalone na 64 bity. Więcej kolumn oznacza, że ​​w każdej partii 64 KB może się zmieścić mniej wierszy.
  • Nie wszystkie typy danych programu SQL Server mogą zmieścić się w 64 bitach, nawet co do zasady. Długi ciąg (by wziąć jeden przykład) może nawet nie zmieścić się w całej partii 64 KB (jeśli jest to dozwolone), nie mówiąc już o pojedynczym wpisie 64-bitowym.

SQL Server rozwiązuje ten ostatni problem, przechowując 8-bajtowe odniesienie do danych większych niż 64 bity. „Duża” wartość danych jest przechowywana w innym miejscu pamięci. Możesz nazwać ten układ przechowywaniem „poza rzędem” lub „poza partiami”. Wewnętrznie określa się je jako głębokie dane .

Teraz ośmiobajtowe typy danych nie mogą zmieścić się w 64 bitach, gdy dopuszczają wartość null. Weź bigint NULL na przykład . Zakres danych innych niż null może wymagać pełnych 64 bitów, a my nadal potrzebujemy innego bitu, aby wskazać null lub nie.

Rozwiązywanie problemów

Kreatywnym i skutecznym rozwiązaniem tych wyzwań jest zarezerwowanie najmniejszego znaczącego bitu (LSB) wartości 64-bitowej jako flagi. Flaga wskazuje wsadowo przechowywanie danych, gdy LSB jest czysty (ustawiony na zero). Gdy LSB jest ustawiony (do jednego), może to oznaczać jedną z dwóch rzeczy:

  1. Wartość jest pusta; lub
  2. Wartość jest przechowywana poza partiami (to głębokie dane).

Te dwa przypadki różnią się stanem pozostałych 63 bitów. Kiedy są wszystkie zero , wartość to NULL . W przeciwnym razie „wartość” jest wskaźnikiem do głębokich danych przechowywanych gdzie indziej.

Gdy postrzegane jako liczba całkowita, ustawienie LSB oznacza, że ​​wskaźniki do głębokich danych zawsze będą nieparzyste liczby. Nulls są reprezentowane przez (nieparzystą) liczbę 1 (wszystkie pozostałe bity są zerowe). Dane wsadowe są reprezentowane przez parzyste liczb, ponieważ LSB wynosi zero.

To nie oznacza, że ​​SQL Server może przechowywać tylko liczby parzyste w partii! Oznacza to po prostu, że znormalizowana reprezentacja wartości kolumn bazowych zawsze będą miały zerową wartość LSB, gdy są przechowywane „wsadowo”. Za chwilę nabierze to większego sensu.

Wsadowa normalizacja danych

Normalizacja jest wykonywana na różne sposoby, w zależności od bazowego typu danych. Dla bigint proces to:

  1. Jeśli dane są null , zapisz wartość 1 (tylko zestaw LSB).
  2. Jeśli wartość może być przedstawiona w 63 bitach , przesuń wszystkie bity o jedno miejsce w lewo i wyzeruj LSB. Patrząc na wartość jako liczbę całkowitą, oznacza to podwojenie wartość. Na przykład bigint wartość 1 jest znormalizowana do wartości 2. W systemie binarnym jest to siedem całkowicie zerowych bajtów, po których następuje 00000010 . Wartość zerowa LSB oznacza, że ​​są to dane przechowywane w linii. Kiedy SQL Server potrzebuje oryginalnej wartości, przesuwa w prawo 64-bitową wartość o jedną pozycję (odrzuca flagę LSB).
  3. Jeśli wartość nie może być reprezentowane w 63 bitach, wartość jest przechowywana poza partiami jako dane głębokie . Wskaźnik wsadowy ma ustawioną wartość LSB (co czyni go liczbą nieparzystą).

Proces testowania, czy bigint wartość może zmieścić się w 63 bitach to:

  1. Przechowuj surowy* bigint wartość w rejestrze procesora 64-bitowego r8 .
  2. Przechowuj podwójną wartość r8 w rejestrze rax .
  3. Przesuń bity rax jedno miejsce po prawej.
  4. Sprawdź, czy wartości w rax i r8 są równe.

* Uwaga:nie można wiarygodnie określić wartości surowej dla wszystkich typów danych przez konwersję T-SQL na typ binarny. Wynik T-SQL może mieć inną kolejność bajtów i może również zawierać metadane, np. time precyzja ułamkowa sekundy.

Jeśli test w kroku 4 zakończy się pomyślnie, wiemy, że wartość można podwoić, a następnie zmniejszyć o połowę w ciągu 64 bitów — zachowując oryginalną wartość.

Zmniejszony zasięg

Rezultatem tego wszystkiego jest to, że zakres bigint wartości, które mogą być przechowywane w partiach, są zmniejszone o jeden bit (ponieważ LSB nie jest dostępny). Następujące włącznie zakresy bigint wartości będą przechowywane poza partiami jako dane głębokie :

  • -4 611 686 018 427 387 905 do -9 223 372 036 854 775 808
  • +4 611 686 018 427 387 904 do +9 223 372 036 854 775 807

W zamian za zaakceptowanie, że te bigint ograniczenia zakresu, normalizacja umożliwia SQL Serverowi przechowywanie (większości) bigint wartości, wartości null i głębokie odwołania do danych wsadowo . Jest to o wiele prostsze i bardziej oszczędne pod względem przestrzeni niż posiadanie oddzielnych struktur dla wartości null i głębokich odwołań do danych. Ułatwia to również przetwarzanie danych wsadowych za pomocą instrukcji procesora SIMD.

Normalizacja innych typów danych

SQL Server zawiera normalizację kod dla każdego z typów danych obsługiwanych przez wykonanie w trybie wsadowym. Każda procedura jest zoptymalizowana pod kątem wydajnej obsługi przychodzącego układu binarnego i tworzenia głębokich danych tylko wtedy, gdy jest to konieczne. Normalizacja zawsze skutkuje zarezerwowaniem LSB do wskazywania wartości zerowych lub głębokich danych, ale układ pozostałych 63 bitów różni się w zależności od typu danych.

Zawsze w partii

Znormalizowane dane dla następujących typów danych są zawsze przechowywane zbiorczo ponieważ nigdy nie potrzebują więcej niż 63 bitów:

  • date
  • time(n) – przeskalowane wewnętrznie do time(7)
  • datetime2(n) – przeskalowane wewnętrznie do datetime2(7)
  • integer
  • smallint
  • tinyint
  • bit – używa tinyint wdrożenie.
  • smalldatetime
  • datetime
  • real
  • float
  • smallmoney

To zależy

Następujące typy danych mogą być przechowywane dane zbiorcze lub głębokie w zależności od wartości danych:

  • bigint – jak opisano wcześniej.
  • money – ten sam zakres w partii co bigint ale podzielone przez 10 000.
  • numeric/decimal – 18 cyfr dziesiętnych lub mniej w partii niezależnie deklarowanej precyzji. Na przykład decimal(38,9) wartość -999999999.999999999 może być reprezentowana jako 8-bajtowa liczba całkowita -999999999999999999 (f21f494c589c0001 szesnastkowy), który można podwoić do -1999999999999999998 (e43e9298b1380002 szesnastkowy) odwracalnie w ciągu 64 bitów. SQL Server wie, dokąd idzie przecinek dziesiętny ze skali typu danych.
  • datetimeoffset(n) – wsadowo, jeśli wartość czasu działania zmieści się w datetimeoffset(2) niezależnie zadeklarowanej dokładności ułamków sekund.
  • timestamp – format wewnętrzny różni się od wyświetlacza. Na przykład timestamp wyświetlane z T-SQL jako 0x000000000099449A jest reprezentowany wewnętrznie jako 9a449900 00000000 (w szesnastkach). Ta wartość jest przechowywana jako głębokie dane, ponieważ nie mieści się w 64-bitach po podwojeniu (przesunięcie w lewo o jeden bit).

Zawsze głębokie dane

Następujące dane są zawsze przechowywane jako głębokie dane (z wyjątkiem wartości null) :

  • uniqueidentifier
  • varbinary(n) – w tym (max)
  • binary
  • char/varchar(n)/nchar/nvarchar(n)/sysname w tym (max) – te typy mogą również korzystać ze słownika (jeśli jest dostępny).
  • text/ntext/image/xml – używa varbinary(n) wdrożenie.

Żeby było jasne, null dla wszystkich Typy danych kompatybilne z trybem wsadowym są przechowywane wsadowo jako specjalna wartość „jeden”.

Ostateczne myśli

Możesz oczekiwać najlepszego wykorzystania dostępnych optymalizacji magazynu kolumn i trybu wsadowego podczas korzystania z typów danych i wartości mieszczących się w 64 bitach. Będziesz mieć również największe szanse na skorzystanie z przyrostowych ulepszeń produktów w czasie, na przykład najnowszych ulepszeń agregacji pushdown odnotowanych w głównym tekście. Nie wszystkie zalety wydajności będą tak widoczne w planach wykonawczych, a nawet udokumentowane. Niemniej jednak różnice mogą być niezwykle znaczące.

Powinienem również wspomnieć, że dane są znormalizowane, gdy operator planu wykonania w trybie wierszowym dostarcza dane do elementu nadrzędnego w trybie wsadowym lub gdy skanowanie niebędące kolumną generuje partie (tryb wsadowy w magazynie wierszy). Istnieje niewidoczny adapter wiersza do partii, który wywołuje odpowiednią procedurę normalizacji dla każdej wartości kolumny przed dodaniem jej do partii. Unikanie typów danych ze skomplikowaną normalizacją i głębokim przechowywaniem danych może również w tym przypadku przynieść korzyści w zakresie wydajności.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Serializowanie usunięć z klastrowanych indeksów magazynu kolumn

  2. Zalecenia dotyczące rutynowych kopii zapasowych treści

  3. Rozwiązania wyzwań generatora serii liczb – Część 5

  4. MERGE:Aktualizowanie tabel źródłowych i docelowych znajdujących się na oddzielnych serwerach

  5. Jak maskować tabele i zachować integralność referencyjną?