sql >> Databáze >  >> RDS >> Database

Dávkový režim Normalizace a výkon

Krátce jsem zmínil, že data v dávkovém režimu jsou normalizována v mém posledním článku Bitmapy v dávkovém režimu na serveru SQL. Všechna data v dávce jsou reprezentována osmibajtovou hodnotou v tomto konkrétním normalizovaném formátu, bez ohledu na základní datový typ.

Toto prohlášení bezpochyby vyvolává některé otázky, v neposlední řadě o tom, jak mohou být data o délce mnohem větší než osm bajtů uložena tímto způsobem. Tento článek se zabývá normalizovanou reprezentací dávkových dat, vysvětluje, proč se ne všechny osmibajtové datové typy vejdou do 64 bitů, a ukazuje příklad, jak to vše ovlivňuje výkon dávkového režimu.

Ukázka

Začnu příkladem, který ukazuje dávkový formát dat, který je důležitým rozdílem v prováděcím plánu. K reprodukci zde uvedených výsledků budete potřebovat SQL Server 2016 (nebo novější) a Developer Edition (nebo ekvivalent).

První věc, kterou budeme potřebovat, je tabulka bigint čísla od 1 do 102 400 včetně. Tato čísla budou brzy použita k naplnění tabulky columnstore (počet řádků je minimum potřebné k získání jednoho komprimovaného 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);

Úspěšné rozšíření souhrnu

Následující skript používá tabulku čísel k vytvoření další tabulky obsahující stejná čísla posunutá o určitou hodnotu. Tato tabulka používá pro své primární úložiště columnstore, aby bylo možné později provést spuštění v dávkovém režimu.

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

Spusťte následující testovací dotazy pro novou tabulku columnstore:

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

Přidání uvnitř SUM je zabránit přetečení. WHERE můžete přeskočit klauzule (abyste se vyhnuli triviálnímu plánu), pokud používáte SQL Server 2017.

Všechny tyto dotazy těží z agregovaného rozšíření dolů. Agregát se vypočítá na Columnstore Index Scan spíše než v dávkovém režimu Hash Aggregate operátor. Plány po provedení ukazují nula řádků emitovaných skenováním. Všech 102 400 řádků bylo „místně agregováno“.

SUM plán je uveden níže jako příklad:

Neúspěšné rozšíření souhrnu

Nyní pusťte a znovu vytvořte testovací tabulku columnstore s offsetem zmenšeným o jedna:

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

Spusťte přesně stejné agregované dotazy na test rozšíření jako dříve:

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

Tentokrát pouze COUNT_BIG agregace dosáhne agregovaného snížení (pouze SQL Server 2017). MAX a SUM agregáty ne. Zde je nový SUM plán pro srovnání s tím z prvního testu:

Všech 102 400 řádků (ve 114 dávkách) je vygenerováno Columnstore Index Scan , zpracováno Výpočetním skalárem a odeslány do Hash Aggregate .

Proč ten rozdíl? Vše, co jsme udělali, bylo vyrovnat rozsah čísel uložených v tabulce columnstore o jednu!

Vysvětlení

V úvodu jsem zmínil, že ne všechny osmibajtové datové typy se vejdou do 64 bitů. Tato skutečnost je důležitá protože mnoho optimalizací výkonu columnstore a dávkového režimu pracuje pouze s daty o velikosti 64 bitů. Agregátní snížení je jednou z těchto věcí. Existuje mnoho dalších funkcí výkonu (ne všechny zdokumentované), které fungují nejlépe (nebo vůbec) pouze tehdy, když se data vejdou do 64 bitů.

V našem konkrétním příkladu je agregované rozšíření dolů deaktivováno pro segment columnstore, pokud obsahuje sudy jeden datová hodnota, která se nevejde do 64 bitů. SQL Server to může určit z minimální a maximální hodnoty metadat spojených s každým segmentem bez kontroly všech dat. Každý segment je hodnocen samostatně.

Agregátní rozšíření stále funguje pro COUNT_BIG agregát až ve druhém testu. Toto je optimalizace přidaná v určitém okamžiku v SQL Server 2017 (moje testy byly spuštěny na CU16). Je logické nezakázat agregované rozšíření, když počítáme pouze řádky a neděláme nic s konkrétními hodnotami dat. Nemohl jsem najít žádnou dokumentaci pro toto vylepšení, ale to není v dnešní době tak neobvyklé.

Jako vedlejší poznámku jsem si všiml, že SQL Server 2017 CU16 umožňuje agregované rozšíření pro dříve nepodporované datové typy real , float , datetimeoffset a numeric s přesností větší než 18 — když se data vejdou do 64 bitů. To také není v době psaní tohoto článku zdokumentováno.

Dobře, ale proč?

Možná se ptáte na velmi rozumnou otázku:Proč je jedna sada bigint testovací hodnoty se zjevně vejdou do 64 bitů, ale ostatní ne?

Pokud jste uhodli, že důvod souvisí s NULL , dej si klíště. I když je sloupec testovací tabulky definován jako NOT NULL , SQL Server používá stejné rozložení normalizovaných dat pro bigint zda data umožňují nuly nebo ne. Má to své důvody, které rozbalím kousek po kousku.

Dovolte mi začít několika postřehy:

  • Každá hodnota sloupce v dávce je uložena v přesně osmi bytech (64 bitech) bez ohledu na základní datový typ. Toto rozložení s pevnou velikostí vše usnadňuje a urychluje. Spouštění v dávkovém režimu je o rychlosti.
  • Šarže má velikost 64 kB a obsahuje 64 až 900 řádků v závislosti na počtu promítaných sloupců. To dává smysl vzhledem k tomu, že velikost dat sloupců je pevně nastavena na 64 bitů. Více sloupců znamená, že se do každé 64kB dávky vejde méně řádků.
  • Ne všechny datové typy SQL Serveru se vejdou do 64 bitů, a to ani v principu. Dlouhý řetězec (abych uvedl jeden příklad) by se možná ani nevešel do celé 64kB dávky (pokud by to bylo povoleno), natož do jediné 64bitové položky.

SQL Server řeší tento poslední problém uložením 8bajtového odkazu na data větší než 64 bitů. „Velká“ datová hodnota je uložena jinde v paměti. Toto uspořádání můžete nazvat „mimořádkové“ nebo „mimodávkové“ úložiště. Interně se označuje jako hluboká data .

Osmibajtové datové typy se nyní nemohou vejít do 64 bitů, pokud lze použít hodnotu Null. Vezměte bigint NULL například . Nenulový datový rozsah může vyžadovat celých 64 bitů a my stále potřebujeme další bit, abychom označili hodnotu null nebo ne.

Řešení problémů

Kreativním a efektivním řešením těchto výzev je rezervace nejnižšího významného bitu (LSB) 64bitové hodnoty jako příznak. Příznak označuje v dávce úložiště dat, když je LSB prázdný (nastaveno na nulu). Když je LSB nastaveno (k jedné), může to znamenat jednu ze dvou věcí:

  1. Hodnota je null; nebo
  2. Hodnota je uložena mimo dávku (jedná se o hluboká data).

Tyto dva případy se odlišují stavem zbývajících 63 bitů. Když jsou všichni nula , hodnota je NULL . Jinak je „hodnota“ ukazatelem na hluboká data uložená jinde.

Při zobrazení jako celé číslo znamená nastavení LSB, že ukazatele na hluboká data budou vždy liché čísla. Nuly jsou reprezentovány (lichým) číslem 1 (všechny ostatní bity jsou nula). Data v dávce jsou reprezentována sudým čísla, protože LSB je nula.

To není znamená, že SQL Server může ukládat pouze sudá čísla v rámci dávky! Znamená to pouze, že normalizovaná reprezentace Hodnoty základního sloupce budou mít vždy nulovou hodnotu LSB, když jsou uloženy „in-batch“. Za chvíli to bude dávat větší smysl.

Dávková normalizace dat

Normalizace se provádí různými způsoby v závislosti na základním datovém typu. Pro bigint proces je:

  1. Pokud jsou data null , uložte hodnotu 1 (pouze nastaveno LSB).
  2. Pokud lze hodnotu vyjádřit v 63 bitech , posuňte všechny bity o jedno místo doleva a vynulujte LSB. Při pohledu na hodnotu jako na celé číslo to znamená zdvojnásobení hodnota. Například bigint hodnota 1 je normalizována na hodnotu 2. V binární podobě, tedy sedm nulových bajtů následovaných 00000010 . Hodnota LSB nula znamená, že se jedná o data uložená inline. Když SQL Server potřebuje původní hodnotu, posune 64bitovou hodnotu doprava o jednu pozici (vyhodí příznak LSB).
  3. Pokud hodnota nelze být reprezentován v 63 bitech, hodnota je uložena mimo dávku jako hluboká data . Ukazatel v dávce má nastaveno LSB (což je liché číslo).

Proces testování, zda je bigint hodnota, která se vejde do 63 bitů, je:

  1. Uložte nezpracovaný* bigint hodnotu v registru 64bitového procesoru r8 .
  2. Uložte dvojnásobnou hodnotu r8 v registru rax .
  3. Posunout bity rax o jedno místo vpravo.
  4. Otestujte, zda jsou hodnoty v rax a r8 jsou si rovni.

* Upozorňujeme, že nezpracovanou hodnotu nelze spolehlivě určit pro všechny datové typy převodem T-SQL na binární typ. Výsledek T-SQL může mít jiné pořadí bajtů a může obsahovat i metadata, např. time přesnost na zlomek sekund.

Pokud test v kroku 4 projde, víme, že hodnotu lze zdvojnásobit a poté snížit na polovinu v rámci 64 bitů – při zachování původní hodnoty.

Snížený rozsah

Výsledkem toho všeho je, že rozsah bigint hodnoty, které lze uložit v dávce, se sníží o jeden bit (protože LSB není k dispozici). Následující včetně rozsahů bigint hodnoty budou uloženy mimo dávku jako hluboká data :

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

Na oplátku za souhlas s tím, že tyto bigint omezení rozsahu, normalizace umožňuje SQL Serveru ukládat (většinu) bigint hodnoty, hodnoty null a podrobné datové odkazy v dávce . Je to mnohem jednodušší a prostorově efektivnější než mít samostatné struktury pro možnost nullability a hluboké datové odkazy. Také to značně usnadňuje zpracování dávkových dat pomocí instrukcí procesoru SIMD.

Normalizace jiných datových typů

SQL Server obsahuje normalizaci kód pro každý z datových typů podporovaných prováděním v dávkovém režimu. Každá rutina je optimalizována tak, aby efektivně zvládla příchozí binární rozložení a pouze v případě potřeby vytvářela hluboká data. Normalizace vždy vede k tomu, že LSB je vyhrazeno pro indikaci nulových nebo hlubokých dat, ale rozložení zbývajících 63 bitů se liší podle typu dat.

Vždy v dávce

Normalizovaná data pro následující typy dat se vždy ukládají v dávce protože nikdy nepotřebují více než 63 bitů:

  • date
  • time(n) – interně změněno na time(7)
  • datetime2(n) – interně změněno na datetime2(7)
  • integer
  • smallint
  • tinyint
  • bit – používá tinyint implementace.
  • smalldatetime
  • datetime
  • real
  • float
  • smallmoney

Záleží

Následující typy dat mohou být uloženy v dávce nebo hloubková data v závislosti na hodnotě dat:

  • bigint – jak bylo popsáno dříve.
  • money – stejný rozsah v dávce jako bigint ale děleno 10 000.
  • numeric/decimal – 18 desetinných číslic nebo méně v dávce bez ohledu deklarované přesnosti. Například decimal(38,9) hodnotu -999999999,999999999 lze vyjádřit jako 8bajtové celé číslo -999999999999999999 (f21f494c589c0001 hex), který lze zdvojnásobit na -1999999999999999998 (e43e9298b1380002 hex) reverzibilně v rámci 64 bitů. SQL Server ví, kam jde desetinná čárka ze stupnice datového typu.
  • datetimeoffset(n) – v dávce, pokud je hodnota za běhu se vejde do datetimeoffset(2) bez ohledu s deklarovanou přesností ve zlomcích sekund.
  • timestamp – interní formát se liší od zobrazení. Například timestamp zobrazeno z T-SQL jako 0x000000000099449A je interně reprezentováno jako 9a449900 00000000 (v hex). Tato hodnota je uložena jako hluboká data, protože se po zdvojnásobení nevejde do 64bitů (o jeden bit posunuta doleva).

Vždy hluboká data

Následující údaje jsou vždy uloženy jako hluboká data (kromě hodnot null) :

  • uniqueidentifier
  • varbinary(n) – včetně (max)
  • binary
  • char/varchar(n)/nchar/nvarchar(n)/sysname včetně (max) – tyto typy mohou také používat slovník (pokud je k dispozici).
  • text/ntext/image/xml – používá varbinary(n) implementace.

Aby bylo jasno, vše má hodnotu null Datové typy kompatibilní s dávkovým režimem se ukládají v dávce jako speciální hodnota ‚one‘.

Závěrečné myšlenky

Při použití datových typů a hodnot, které se vejdou do 64 bitů, můžete očekávat to nejlepší z dostupných optimalizací pro ukládání sloupců a dávkový režim. Budete mít také největší šanci těžit z postupných vylepšení produktu v průběhu času, například nejnovějších vylepšení agregovaného snížení uvedených v hlavním textu. Ne všechny výhody výkonu budou tak viditelné v prováděcích plánech nebo dokonce zdokumentované. Nicméně rozdíly mohou být extrémně významné.

Měl bych také zmínit, že data jsou normalizována, když operátor plánu provádění v režimu řádků poskytuje data nadřazenému v dávkovém režimu nebo když skenování bez úložiště sloupců vytváří dávky (dávkový režim na úložišti řádků). Existuje neviditelný adaptér typu řádek na dávku, který před přidáním do dávky zavolá příslušnou normalizační rutinu pro každou hodnotu sloupce. Vyhýbání se datovým typům s komplikovanou normalizací a hlubokým ukládáním dat může i zde přinést výkonnostní výhody.


  1. Připojení k Lotus Notes z Java

  2. Jak funguje funkce REGEXP_SUBSTR() v MySQL

  3. Jak změnit výchozí profil pošty databáze pro uživatele v SQL Server (T-SQL)

  4. Pochopení rozdílu mezi operátory EXCEPT a NOT IN