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

Nejbližší zápas, část 1

Karen Ly, Jr. Fixed Income Analyst v RBC, mi dala T-SQL výzvu, která zahrnuje hledání nejbližší shody, nikoli hledání přesné shody. V tomto článku se zabývám zobecněnou, zjednodušenou formou výzvy.

Výzva

Výzva zahrnuje párování řádků ze dvou tabulek, T1 a T2. Pomocí následujícího kódu vytvořte ukázkovou databázi s názvem testdb a v ní tabulky T1 a T2:

 SET NOCOUNT ON; IF DB_ID('testdb') JE NULL CREATE DATABASE testdb; GO USE testdb; DROP TABULKU, POKUD EXISTUJE dbo.T1, dbo.T2; CREATE TABLE dbo.T1 ( klíčový sloupec INT NOT NULL OMEZENÍ IDENTITY PK_T1 PRIMÁRNÍ KLÍČ, hodnota INT NOT NULL, ostatní sloupce BINARY(100) NOT NULL CONSTRAINT DFT_T1_col1 DEFAULT(0xAA) ); CREATE TABLE dbo.T2 ( keycol INT NOT NULL IDENTITY CONSTRAINT PK_T2 PRIMARY KEY, hodnota INT NOT NULL, othercols BINARY(100) NOT NULL CONSTRAINT DFT_T2_col1 DEFAULT(0xBB) );

Jak vidíte, T1 i T2 mají číselný sloupec (v tomto příkladu typ INT) nazvaný val. Úkolem je přiřadit ke každému řádku z T1 řádek z T2, kde je absolutní rozdíl mezi T2.val a T1.val nejnižší. V případě remíz (více odpovídajících řádků v T2) spojte horní řadu na základě hodnot vzestupně, keycol vzestupně. To znamená řádek s nejnižší hodnotou ve sloupci val, a pokud stále máte remízy, řádek s nejnižší hodnotou keycol. Tiebreaker se používá k zaručení determinismu.

Následující kód použijte k naplnění T1 a T2 malými sadami ukázkových dat, abyste mohli zkontrolovat správnost vašich řešení:

 TRUNCATE TABLE dbo.T1; TRUNCATE TABLE dbo.T2; INSERT INTO dbo.T1 (val) VALUES(1),(1),(3),(3),(5),(8),(13),(16),(18),(20),( 21); INSERT INTO dbo.T2 (val) VALUES(2),(2),(7),(3),(3),(11),(11),(13),(17),(19); 

Zkontrolujte obsah T1:

 SELECT klíčový sloupec, val, SUBSTRING(ostatní sloupce, 1, 1) JAKO další sloupce FROM dbo.T1 ORDER BY val, klíčový sloupec;

Tento kód generuje následující výstup:

 keycol val othercols ----------- ----------- --------- 1 1 0xAA 2 1 0xAA 3 3 0xAA 4 3 0xAA 5 5 0xAA 6 8 0xAA 7 13 0xAA 8 16 0xAA 9 18 0xAA 10 20 0xAA 11 21 0xAA

Zkontrolujte obsah T2:

 VYBERTE klíčový sloupec, val, SUBSTRING(ostatní sloupce, 1, 1) JAKO další sloupce FROM dbo.T2 ORDER BY val, klíčový sloupec;

Tento kód generuje následující výstup:

 keycol val othercols ----------- ----------- ---------- 1 2 0xBB 2 2 0xBB 4 3 0xBB 5 3 0xBB 3 7 0xBB 6 11 0xBB 7 11 0xBB 8 13 0xBB 9 17 0xBB 10 19 0xBB

Zde je požadovaný výsledek pro daná ukázková data:

 keycol1 val1 othercols1 keycol2 val2 othercols2 ----------- ----------- ---------- ---------- -- ----------- ---------- 1 1 0xAA 1 2 0xBB 2 1 0xAA 1 2 0xBB 3 3 0xAA 4 3 0xBB 4 3 0xAA 4 3 0xBB 5 5 0xAA 4 3 0xBB 6 8 0xAA 3 7 0xBB 7 13 0xAA 8 13 0xBB 8 16 0xAA 9 17 0xBB 9 18 0xAA 9 17 0xBB 10 20 0xAA 10 BB19 0xAA 10 BB19 0x AA 10 BB19 0x 10 BB19 0x 10 BB 19 00x 

Než začnete pracovat na výzvě, prozkoumejte požadovaný výsledek a ujistěte se, že rozumíte logice shody, včetně logiky nerozhodného výsledku. Zvažte například řádek v T1, kde keycol je 5 a val je 5. Tento řádek má několik nejbližších shod v T2:

 keycol val othercols ----------- ----------- --------- 4 3 0xBB 5 3 0xBB 3 7 0xBB

Ve všech těchto řádcích je absolutní rozdíl mezi T2.val a T1.val (5) 2. Pomocí logiky rozdělování na základě pořadí val vzestupně, keycol vzestupně na nejvyšším řádku zde je ten, kde val je 3 a keycol je 4.

Malé sady vzorových dat použijete ke kontrole platnosti vašich řešení. Ke kontrole výkonu potřebujete větší sady. Pomocí následujícího kódu vytvořte pomocnou funkci nazvanou GetNums, která generuje sekvenci celých čísel v požadovaném rozsahu:

 DROP FUNCTION IF EXISTS dbo.GetNums; GO VYTVOŘIT NEBO ZMĚNIT FUNKCI dbo.GetNums(@nízká JAKO BIGINT, @vysoká JAKO BIGINT) VRÁTÍ TABULKU JAKO NÁVRAT S L0 JAKO (VYBRAT c Z (VYBRAT 1 UNION VŠECHNY VYBRAT 1) JAKO D(c)), L1 JAKO (VYBRAT 1 JAKO c OD L0 JAKO KŘÍŽOVÉ PŘIPOJENÍ L0 JAKO B), L2 JAKO (VYBERTE 1 JAKO C Z L1 JAKO KŘÍŽOVÉ SPOJE L1 JAKO B), L3 AS (VYBERTE 1 JAKO C Z L2 JAKO KŘÍŽOVÉ SPOJE L2 JAKO B), L4 AS (VYBERTE 1 JAKO c Z L3 JAKO CŘÍŽOVÉ PŘIPOJENÍ L3 JAKO B), L5 AS (VYBERTE 1 JAKO C Z L4 JAKO CŘÍŽOVÉ SPOJE L4 JAKO B), Nums AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL) ) AS rownum FROM L5) SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n FROM Nums ORDER BY rownum; GO

K naplnění T1 a T2 velkými sadami ukázkových dat použijte následující kód:

 DECLARE @numrowsT1 AS INT =1000000, @maxvalT1 AS INT =10000000, @numrowsT2 AS INT =1000000, @maxvalT2 AS INT =10000000; TRUNCATE TABLE dbo.T1; TRUNCATE TABLE dbo.T2; INSERT INTO dbo.T1 WITH(TABLOCK) (val) SELECT ABS(CHECKSUM(NEWID())) % @maxvalT1 + 1 AS val FROM dbo.GetNums(1, @numrowsT1) AS Nums; INSERT INTO dbo.T2 WITH(TABLOCK) (val) SELECT ABS(CHECKSUM(NEWID())) % @maxvalT2 + 1 AS val FROM dbo.GetNums(1, @numrowsT2) AS Nums;

Proměnné @numrowsT1 a @numrowsT2 řídí počet řádků, kterými mají být tabulky naplněny. Proměnné @maxvalT1 a @maxvalT2 řídí rozsah různých hodnot ve sloupci val, a proto ovlivňují hustotu sloupce. Výše uvedený kód vyplní tabulky 1 000 000 řádky a pro sloupec val v obou tabulkách používá rozsah 1 – 10 000 000. To má za následek nízkou hustotu ve sloupci (velký počet odlišných hodnot s malým počtem duplikátů). Použití nižších maximálních hodnot povede k vyšší hustotě (menší počet odlišných hodnot, a tedy více duplikátů).

Řešení 1, pomocí jednoho TOP dílčího dotazu

Nejjednodušším a nejzřejmějším řešením je pravděpodobně řešení, které se dotazuje na T1 a pomocí operátoru CROSS APPLY aplikuje dotaz s TOP (1) filtrem seřazením řádků podle absolutního rozdílu mezi T1.val a T2.val pomocí T2.val , T2.keycol jako nerozhodný výsledek. Zde je kód řešení:

 SELECT T1.keycol AS keycol1, T1.val AS hodnota1, SUBSTRING(T1.othercols, 1, 1) AS othercols1, A.keycol AS keycol2, A.val AS val2, SUBSTRING(A.othercols, 1, 1) ) JAKO othercols2 FROM dbo.T1 CROSS APPLY ( SELECT TOP (1) T2.* FROM dbo.T2 ORDER BY ABS(T2.val - T1.val), T2.val, T2.keycol ) AS A;

Pamatujte, že v každé z tabulek je 1 000 000 řádků. A hustota valového sloupce je v obou tabulkách nízká. Bohužel, protože v použitém dotazu není žádný predikát filtrování a řazení zahrnuje výraz, který manipuluje se sloupci, není zde žádný potenciál pro vytváření podpůrných indexů. Tento dotaz vygeneruje plán na obrázku 1.

Obrázek 1:Plán řešení 1

Vnější vstup smyčky skenuje 1 000 000 řádků z T1. Vnitřní vstup smyčky provádí úplné skenování T2 a řazení TopN pro každou odlišnou hodnotu T1.val. V našem plánu se to stane 998 657 krát, protože máme velmi nízkou hustotu. Umístí řádky do indexového zařazování, zakódovaného pomocí T1.val, takže je může znovu použít pro duplicitní výskyty hodnot T1.val, ale máme velmi málo duplikátů. Tento plán má kvadratickou složitost. Mezi všemi očekávanými provedeními vnitřní větve smyčky se očekává, že zpracuje téměř bilion řádků. Když mluvíme o velkém počtu řádků pro zpracování dotazu, jakmile začnete zmiňovat miliardy řádků, lidé již vědí, že máte co do činění s drahým dotazem. Normálně lidé nevyslovují termíny jako biliony, pokud zrovna nehovoří o americkém státním dluhu nebo o krachu akciového trhu. Obvykle se s takovými čísly v kontextu zpracování dotazů nezabýváte. Ale plány s kvadratickou složitostí mohou rychle skončit s takovými čísly. Zpracování pouhých 100 řádků z T1 na mém notebooku trvalo spuštění dotazu v SSMS se zapnutou funkcí Zahrnout statistiku dotazů v reálném čase 39,6 sekund. To znamená, že dokončení tohoto dotazu by mělo trvat asi 4,5 dne. Otázkou je, jestli máte opravdu chuť sledovat plány dotazů v reálném čase? Mohl by to být zajímavý Guinnessův rekord.

Řešení 2, používající dva TOP poddotazy

Za předpokladu, že hledáte řešení, jehož dokončení zabere sekundy, nikoli dny, zde je nápad. V použitém tabulkovém výrazu sjednoťte dva vnitřní TOP (1) dotazy – jeden filtrující řádek, kde T2.val =T1.val (řazeno podle T2.val, T2.keycol). Je snadné vytvářet indexy pro podporu takových dotazů tím, že povolíte jednořádkové vyhledávání. Stále v rámci použitého tabulkového výrazu použijte vnější TOP (1) dotaz proti dvouřádkovému výsledku na základě pořadí ABS(D.val – T1.val), D.val, D.keycol. Bude zapojeno řazení TopN, ale bude použito na dva řádky současně.

Zde jsou doporučené indexy pro podporu tohoto řešení:

 CREATE INDEX idx_val_key ON dbo.T1(val, keycol) INCLUDE(othercols); CREATE INDEX idx_val_key ON dbo.T2(val, keycol) INCLUDE(othercols); CREATE INDEX idx_valD_key ON dbo.T2(val DESC, keycol) INCLUDE(othercols);

Zde je úplný kód řešení:

 SELECT T1.keycol AS keycol1, T1.val AS hodnota1, SUBSTRING(T1.othercols, 1, 1) AS othercols1, A.keycol AS keycol2, A.val AS val2, SUBSTRING(A.othercols, 1, 1) ) JAKO othercols2 FROM dbo.T1 CROSS APPLY ( SELECT TOP (1) D.* FROM ( SELECT TOP (1) * FROM dbo.T2 WHERE T2.val =T1.val ŘADÍTE PODLE T2.val, T2.keycol ) JAKO D OBJEDNEJTE PODLE ABS(D.val - T1.val), D.val, D. keycol ) AS A;

Pamatujte, že v každé tabulce máme 1 000 000 řádků, přičemž sloupec val má hodnoty v rozsahu 1 – 10 000 000 (nízká hustota) a jsou nastaveny optimální indexy.

Plán pro tento dotaz je znázorněn na obrázku 2.

Obrázek 2:Plán řešení 2

Sledujte optimální využití indexů na T2, výsledkem je plán s lineárním měřítkem. Tento plán používá zařazování indexů stejným způsobem jako předchozí plán, tj. aby se zabránilo opakování práce ve vnitřní větvi smyčky pro duplicitní hodnoty T1.val. Zde jsou statistiky výkonu, které jsem získal pro provedení tohoto dotazu v mém systému:

 Uplynulo:27,7 s, CPU:27,6 s, logická čtení:17 304 674

Řešení 2 s deaktivovaným zařazováním

Nemůžete se ubránit otázce, zda je zde indexová cívka skutečně prospěšná. Jde o to, abychom se vyhnuli opakování práce pro duplicitní hodnoty T1.val, ale v situaci, jako je ta naše, kde máme velmi nízkou hustotu, existuje velmi málo duplikátů. To znamená, že s největší pravděpodobností je práce spojená se zařazováním větší než pouhé opakování práce pro duplikáty. Existuje jednoduchý způsob, jak to ověřit – pomocí příznaku trasování 8690 můžete zakázat zařazování v plánu, například takto:

 SELECT T1.keycol AS keycol1, T1.val AS hodnota1, SUBSTRING(T1.othercols, 1, 1) AS othercols1, A.keycol AS keycol2, A.val AS val2, SUBSTRING(A.othercols, 1, 1) ) JAKO othercols2 FROM dbo.T1 CROSS APPLY ( SELECT TOP (1) D.* FROM ( SELECT TOP (1) * FROM dbo.T2 WHERE T2.val =T1.val ŘADÍTE PODLE T2.val, T2.keycol ) JAKO D OBJEDNEJTE PODLE ABS(D.val - T1.val), D.val, D. keycol ) JAKO MOŽNOST (QUERYTRACEON 8690);

Získal jsem plán znázorněný na obrázku 3 pro toto provedení:

Obrázek 3:Plán řešení 2, s deaktivovaným zařazováním

Všimněte si, že indexová cívka zmizela a tentokrát se vnitřní větev smyčky provede 1 000 000krát. Zde jsou statistiky výkonu, které jsem získal pro toto provedení:

 Uplynulo:19,18 s, CPU:19,17 s, logická čtení:6 042 148

To je zkrácení doby realizace o 44 procent.

Řešení 2 s upraveným rozsahem hodnot a indexováním

Zakázání zařazování má velký smysl, když máte nízkou hustotu v hodnotách T1.val; navíjení však může být velmi výhodné, když máte vysokou hustotu. Například spusťte následující kód, abyste znovu naplnili T1 a T2 s nastavením ukázkových dat @maxvalT1 a @maxvalT2 na 1000 (1000 maximálních odlišných hodnot):

 DECLARE @numrowsT1 AS INT =1000000, @maxvalT1 AS INT =1000, @numrowsT2 AS INT =1000000, @maxvalT2 AS INT =1000; TRUNCATE TABLE dbo.T1; TRUNCATE TABLE dbo.T2; INSERT INTO dbo.T1 WITH(TABLOCK) (val) SELECT ABS(CHECKSUM(NEWID())) % @maxvalT1 + 1 AS val FROM dbo.GetNums(1, @numrowsT1) AS Nums; INSERT INTO dbo.T2 WITH(TABLOCK) (val) SELECT ABS(CHECKSUM(NEWID())) % @maxvalT2 + 1 AS val FROM dbo.GetNums(1, @numrowsT2) AS Nums;

Znovu spusťte řešení 2, nejprve bez příznaku trasování:

 SELECT T1.keycol AS keycol1, T1.val AS hodnota1, SUBSTRING(T1.othercols, 1, 1) AS othercols1, A.keycol AS keycol2, A.val AS val2, SUBSTRING(A.othercols, 1, 1) ) JAKO othercols2 FROM dbo.T1 CROSS APPLY ( SELECT TOP (1) D.* FROM ( SELECT TOP (1) * FROM dbo.T2 WHERE T2.val =T1.val ŘADÍTE PODLE T2.val, T2.keycol ) JAKO D OBJEDNEJTE PODLE ABS(D.val - T1.val), D.val, D. keycol ) AS A;

Plán tohoto provedení je znázorněn na obrázku 4:

Obrázek 4:Plán řešení 2, s rozsahem hodnot 1 – 1000

Optimalizátor se rozhodl použít jiný plán kvůli vysoké hustotě v T1.val. Všimněte si, že index na T1 je skenován v pořadí klíčů. Takže pro každý první výskyt odlišné hodnoty T1.val se provede vnitřní větev smyčky a výsledek se zařadí do běžného spoolu tabulky (rebind). Poté se pro každý neprvní výskyt hodnoty použije přetočení zpět. S 1 000 odlišnými hodnotami se vnitřní větev provede pouze 1 000krát. Výsledkem jsou vynikající statistiky výkonu:

 Uplynulo:1,16 s, CPU:1,14 s, logická čtení:23 278

Nyní zkuste spustit řešení se zakázaným zařazováním:

 SELECT T1.keycol AS keycol1, T1.val AS hodnota1, SUBSTRING(T1.othercols, 1, 1) AS othercols1, A.keycol AS keycol2, A.val AS val2, SUBSTRING(A.othercols, 1, 1) ) JAKO othercols2 FROM dbo.T1 CROSS APPLY ( SELECT TOP (1) D.* FROM ( SELECT TOP (1) * FROM dbo.T2 WHERE T2.val =T1.val ŘADÍTE PODLE T2.val, T2.keycol ) JAKO D OBJEDNEJTE PODLE ABS(D.val - T1.val), D.val, D. keycol ) JAKO MOŽNOST (QUERYTRACEON 8690);

Mám plán zobrazený na obrázku 5.

Obrázek 5:Plán pro řešení 2, s rozsahem hodnot 1 – 1 000 a zařazováním vypnutým

Je to v podstatě stejný plán znázorněný dříve na obrázku 3. Vnitřní větev smyčky se provede 1 000 000krát. Zde jsou statistiky výkonu, které jsem získal pro toto provedení:

 Uplynulo:24,5 s, CPU:24,2 s, logická čtení:8 012 548

Je zřejmé, že chcete být opatrní a nezakázat zařazování, když máte v T1.val vysokou hustotu.

Život je dobrý, když je vaše situace natolik jednoduchá, že jste schopni vytvořit podpůrné indexy. Realita je taková, že v některých případech v praxi je v dotazu dostatek dodatečné logiky, která znemožňuje vytvoření optimálních podpůrných indexů. V takových případech nebude řešení 2 fungovat dobře.

Chcete-li demonstrovat výkon řešení 2 bez podpůrných indexů, znovu vyplňte T1 a T2 s nastavením vzorových dat @maxvalT1 a @maxvalT2 na 10000000 (rozsah hodnot 1–10M) a také odstraňte podpůrné indexy:

 DROP INDEX IF EXISTS idx_val_key ON dbo.T1; DROP INDEX IF EXISTS idx_val_key ON dbo.T2; DROP INDEX IF EXISTS idx_valD_key ON dbo.T2; DECLARE @numrowsT1 AS INT =1000000, @maxvalT1 AS INT =10000000, @numrowsT2 AS INT =1000000, @maxvalT2 AS INT =10000000; TRUNCATE TABLE dbo.T1; TRUNCATE TABLE dbo.T2; INSERT INTO dbo.T1 WITH(TABLOCK) (val) SELECT ABS(CHECKSUM(NEWID())) % @maxvalT1 + 1 AS val FROM dbo.GetNums(1, @numrowsT1) AS Nums; INSERT INTO dbo.T2 WITH(TABLOCK) (val) SELECT ABS(CHECKSUM(NEWID())) % @maxvalT2 + 1 AS val FROM dbo.GetNums(1, @numrowsT2) AS Nums;

Znovu spusťte řešení 2 s povolenou funkcí Zahrnout statistiku dotazů v reálném čase v SSMS:

 SELECT T1.keycol AS keycol1, T1.val AS hodnota1, SUBSTRING(T1.othercols, 1, 1) AS othercols1, A.keycol AS keycol2, A.val AS val2, SUBSTRING(A.othercols, 1, 1) ) JAKO othercols2 FROM dbo.T1 CROSS APPLY ( SELECT TOP (1) D.* FROM ( SELECT TOP (1) * FROM dbo.T2 WHERE T2.val =T1.val ŘADÍTE PODLE T2.val, T2.keycol ) JAKO D OBJEDNEJTE PODLE ABS(D.val - T1.val), D.val, D. keycol ) AS A;

Mám plán zobrazený na obrázku 6 pro tento dotaz:

Obrázek 6:Plán řešení 2, bez indexování, s rozsahem hodnot 1 – 1 000 000

Můžete vidět vzor velmi podobný vzoru zobrazenému dříve na obrázku 1, ale tentokrát plán skenuje T2 dvakrát na odlišnou hodnotu T1.val. Složitost plánu je opět kvadratická. Zpracování 100 řádků z T1 na mém notebooku trvalo spuštění dotazu v SSMS se zapnutou funkcí Zahrnout statistiku dotazů v reálném čase 49,6 sekund, což znamená, že dokončení tohoto dotazu by mělo trvat asi 5,7 dne. To by samozřejmě mohlo znamenat dobrou zprávu, pokud se snažíte překonat Guinnessův světový rekord ve sledování živého plánu dotazů.

Závěr

Rád bych poděkoval Karen Ly z RBC za to, že mi předložila tuto pěknou výzvu k nejbližšímu zápasu. Byl jsem docela ohromen jejím kódem pro manipulaci s ním, který obsahoval spoustu extra logiky, která byla specifická pro její systém. V tomto článku jsem ukázal přiměřeně fungující řešení, když jste schopni vytvořit optimální podpůrné indexy. Ale jak jste mohli vidět, v případech, kdy to není možné, jsou samozřejmě časy provedení, které získáte, propastné. Napadá vás řešení, která budou dobře fungovat i bez optimálních podpůrných indexů? Pokračování…


  1. Vyplnění položky stromu skupinou záznamů v Oracle Forms

  2. Vraťte seznam spouštěčů na serveru SQL Server

  3. Jak zastavit/spustit MySQL pomocí MySQL Workbench

  4. Atomic UPSERT v SQL Server 2005