V Closest Match, část 1, jsem se zabýval výzvou T-SQL, kterou mi poskytla Karen Ly, Jr. analytička s pevným příjmem v RBC. Výzva zahrnovala dvě tabulky – T1 a T2, každá s klíčovým sloupcem (keycol), hodnotou (val) a některými dalšími sloupci (reprezentovanými sloupcem nazvaným othercols). Úkolem bylo přiřadit ke každému řádku z T1 řádek z T2, kde T2.val je nejblíže T1.val (absolutní rozdíl je nejnižší nejnižší), s použitím nejnižší hodnoty T2.keycol jako nerozhodného výsledku. Poskytl jsem několik relačních řešení a diskutoval o jejich výkonnostních charakteristikách. Vyzval jsem vás také, abyste se pokusili najít přiměřeně fungující řešení v případech, kdy nemáte povoleno/možnost vytvářet podpůrné indexy. V části 2 jsem ukázal, jak toho lze dosáhnout pomocí iterativních řešení. Od Kamila Konsna, Alejandra Mesy a Radka Čelucha jsem získal několik velmi zajímavých řešení pro čtení a na tato řešení se zaměřuje tento měsíc.
Ukázková data
Pro připomenutí jsem vám poskytl malé i velké sady ukázkových dat, se kterými můžete pracovat. Malé sady pro kontrolu platnosti vašeho řešení a velké sady pro kontrolu jeho výkonu. Pomocí následujícího kódu vytvořte ukázkovou databázi testdb a v ní ukázkové tabulky T1 a T2:
-- Ukázka dbSET NOCOUNT ON; IF DB_ID('testdb') IS NULL CREATE DATABASE testdb;GO USE testdb;GO -- Ukázkové tabulky T1 a T2DROP TABLE IF EXISTS dbo.T1, dbo.T2; CREATE TABLE dbo.T1( keycol INT NOT NULL IDENTITY CONSTRAINT PK_T1 PRIMÁRNÍ KLÍČ, hodnota INT NOT NULL, othercols BINARY(100) NOT NULL CONSTRAINT DFT_T1_col1 DEFAULT(0xAA)); CREATE TABLE dbo.T2( keycol INT NOT NULL IDENTITY CONSTRAINT PK_T2 PRIMÁRNÍ KLÍČ, hodnota INT NOT NULL, othercols BINARY(100) NOT NULL CONSTRAINT DFT_T2_col1 DEFAULT(0xBB));
K naplnění tabulek malými sadami ukázkových dat použijte následující kód:
-- Vyplňte T1 a T2 malými sadami ukázkových dat 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);Malé sady vzorových dat použiji při popisu logiky různých řešení a poskytnu sady dílčích výsledků pro jednotlivé kroky řešení.
Pomocí následujícího kódu vytvořte pomocnou funkci
GetNums
a naplnit tabulky velkými sadami ukázkových dat:-- Pomocná funkce pro generování posloupnosti celých čísel FUNKCE DROP, POKUD EXISTUJE dbo.GetNums;GO VYTVOŘIT NEBO ZMĚNIT FUNKCI dbo.GetNums(@low AS BIGINT, @high AS BIGINT) VRÁTÍ TABLEASRETURN WITH L0 AS (SELECT c FROM 1 UNION VŠECHNY VYBERTE 1) JAKO D(c)), L1 JAKO (VYBERTE 1 JAKO c Z L0 JAKO KŘÍŽOVÉ PŘIPOJENÍ L0 JAKO B), L2 AS (VYBERTE 1 JAKO C Z L1 JAKO KŘÍŽOVÉ SPOJE L1 JAKO B), L3 AS (VYBERTE 1 JAKO c Z L2 JAKO PŘÍČNÉ PŘIPOJENÍ L2 JAKO B), L4 AS (VYBERTE 1 JAKO C Z L3 JAKO PŘÍČNÉ SPOJE L3 JAKO B), L5 AS (VYBERTE 1 JAKO C Z L4 JAKO KŘÍŽOVÉ SPOJE L4 AS 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 -- Naplňte T1 a T2 velkými sadami ukázkových datDECLARE @numrowsT1 AS INT =1000000, @maxvalT1 AS INT =10000000, @numrowsT2 AS INT =1000000, @maxvalT2 AS INT =10000000; ZKRÁTIT TABULKU dbo.T1;ZKRÁTIT TABULKU 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;Pokud chcete své řešení otestovat s podpůrnými indexy, použijte k vytvoření těchto indexů následující kód:
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) INCLUDE(další sloupce);Pokud chcete své řešení otestovat bez podpory indexů, použijte k odebrání těchto indexů následující kód:
PUSTIT INDEX, POKUD EXISTUJE idx_val_key NA dbo.T1;PUSTIT INDEX, POKUD EXISTUJE idx_val_key NA dbo.T2;PUSTIT INDEX, POKUD EXISTUJE idx_valD_key NA dbo.T2;Řešení 1 od Kamila Kosna – Použití dočasné tabulky
Kamil Kosno jich předložil několik – dva se svými originálními nápady a dvě variace na Alejandrova a Radkova řešení. Kamilovo první řešení používá indexovanou dočasnou tabulku. Často se stává, že i když nemáte povoleno vytvářet podpůrné indexy na původních tabulkách, můžete pracovat s dočasnými tabulkami as dočasnými tabulkami můžete vždy vytvářet své vlastní indexy.
Zde je úplný kód řešení:
PUSTIT TABULKU, POKUD EXISTUJE #T2; SELECT MIN(sloupec klíče) AS klíč klíče, valINTO #T2FROM dbo.T2GROUP BY val; CREATE UNIQUE INDEX idx_nc_val_key ON #T2(val, keycol); WITH bestvals AS( SELECT keycol AS keycol1, val AS val1, othercols AS othercols1, CASE WHEN (prev + nxt IS NULL) THEN COALESCE(prev, nxt) WHEN ABS(val - prev)V kroku 1 řešení uložíte minimální klíčový sloupec pro každou jedinečnou hodnotu do dočasné tabulky s názvem #T2 a vytvoříte podpůrný index na (val, klíčový sloupec). Zde je kód implementující tento krok:
PUSTIT TABULKU, POKUD EXISTUJE #T2; SELECT MIN(sloupec klíče) AS klíč klíče, valINTO #T2FROM dbo.T2GROUP BY val; CREATE UNIQUE INDEX idx_nc_val_key ON #T2(val, keycol); SELECT * FROM #T2;Dočasná tabulka je naplněna následujícími daty:
keycol val----------- ----------1 24 33 76 118 139 1710 19V kroku 2 řešení použijete následující:
Pro každý řádek v T1 vypočítejte hodnoty prev a nxt z #T2. Vypočítejte prev jako maximální hodnotu v #T2, která je menší nebo rovna hodnotě v T1. Vypočítejte nxt jako minimální hodnotu v #T2, která je větší nebo rovna hodnotě v T1.
Použijte výraz CASE k výpočtu val2 na základě následující logiky:
- Když je prev nebo nxt null, vrátí první nenull ze dvou nebo NULL, pokud jsou oba NULL.
- Když je absolutní rozdíl mezi val a prev menší než absolutní rozdíl mezi val a nxt, vrátí se prev.
- Když je absolutní rozdíl mezi val a nxt menší než absolutní rozdíl mezi val a prv, vrátí nxt.
- Jinak, pokud je nxt menší než předchozí, vrátí nxt, jinak vrátí předchozí.
Zde je kód implementující tento krok:
SELECT keycol AS keycol1, val AS val1, othercols AS othercols1, CASE WHEN (prev + nxt IS NULL) THEN COALESCE(prev, nxt) WHEN ABS(val - prev)Tento kód generuje následující výstup:
keycol1 val1 othercols1 value2-------- ----- ----------- -----1 1 0xAA 22 1 0xAA 23 3 0xAA 34 3 0xAA 35 5 0xAA 36 8 0xAA 77 13 0xAA 138 16 0xAA 179 18 0xAA 1710 20 0xAA 1911 21 0xAA 19V kroku 3 řešení definujete CTE nazývané bestvals na základě dotazu z kroku 2. Poté spojíte bestvals s #T2, abyste získali klíče, a spojte výsledek s T2, abyste získali zbytek dat (othercols).
Zde je kód implementující tento krok:
SELECT keycol1, val1, SUBSTRING(othercols1, 1, 1) AS othercols1, tempT2.keycol AS keycol2, val2, SUBSTRING(T2.othercols, 1, 1) AS othercols2FROM bestvals AS B INNER JOIN #T2 AS tempT2 ON tempT2 .val =B.val2 INNER JOIN dbo.T2 ON T2.keycol =tempT2.keycol;Plán řešení 1 od Kamila s podpůrné indexy je znázorněno na obrázku 1.
Obrázek 1:Plán řešení 1 od Kamila s indexy
První větev plánu seskupuje a agreguje data z T2 a zapisuje výsledek do dočasné tabulky #T2. Všimněte si, že tato větev používá předobjednaný algoritmus Stream Aggregate, který spoléhá na uspořádané skenování indexu idx_valD_key na T2.
Zde jsou statistiky výkonu pro tento plán:
doba běhu (s):5,55, CPU (s):16,6, logická čtení:6 687 172Plán řešení 1 od Kamila bez podpůrných indexů je na obrázku 2.
Obrázek 2:Plán řešení 1 od Kamila bez indexů
Hlavní rozdíl mezi tímto plánem a předchozím je v tom, že první větev plánu, která zpracovává část seskupení a agregaci v kroku 1, tentokrát nedokáže stáhnout data předobjednaná z podpůrného indexu, takže je stáhne neseřazená z clusteru. index na T2 a poté používá algoritmus Hash Aggregate.
Zde jsou statistiky výkonu pro tento plán:
doba běhu:6,44, CPU:19,3, logická čtení:6 688 277Řešení od Alejandra Mesy – Použití poslední nenulové techniky
Alejandroovo řešení používá poslední nenulovou techniku popsanou zde.
Zde je kompletní kód řešení (zde poskytnut bez vracení othercols):
WITH R1 AS( SELECT keycol, val, tid, MAX( CASE WHEN tid =0 THEN CAST(val AS binary(4)) + CAST(keycol AS binary(4)) END ) OVER( ORDER BY val, tid , klíčová kolonka ŘÁDKY BETWEEN NEBOUNDED PŘEDCHOZÍ A 1 PŘEDCHOZÍ ) AS below_binstr, MIN( CASE WHEN tid =0 THEN CAST(val AS binary(4)) + CAST(keycol AS binary(4)) END ) OVER( ORDER BY val, tid, keycol ŘÁDKY MEZI 1 NÁSLEDUJÍCÍ A NEZAPOMENUTÉ NÁSLEDUJÍCÍ ) JAKO výše_binstr FROM ( SELECT keycol, val, 1 AS tid FROM dbo.T1 UNION ALL SELECT MIN(keycol) AS keycol, val, 0 AS tid FROM dbo.T2 GROUP BY val ) AS T ),R2 AS( SELECT klíčová kolonka, hodnota, CAST(SUBSTRING(pod_binstr, 1, 4) AS int) AS pod_T2_val, CAST(SUBSTRING(pod_binstr, 5, 4) AS int) AS pod_T2_klíč, CAST(SUBSTRING(nad_binstr, 1, 4) AS int) AS above_T2_val, CAST(SUBSTRING(above_binstr, 5, 4) AS int) AS above_T2_keycol FROM R1 WHERE tid =1),R3 AS( SELECT keycol, val, COALESCE(below_T2_val, above_T2 _val) AS under_T2_val, COALESCE(below_T2_keycol, above_T2_keycol) AS below_T2_keycol, COALESCE(above_T2_val, below_T2_val) AS above_T2_val, COALESCE(above_T2_keycol, below_T2_keycol, klíčový col2 RASET2SELECT2 klíč1 ASENTval) ASTENTval ) <=ABS(above_T2_val - val) THEN below_T2_keycol ELSE above_T2_keycol END AS keycol2, CASE WHEN ABS(val - below_T2_val) <=ABS(above_T2_val - val) THEN below_T2_val ELSE above_T2_val END AS val2FROMV kroku 1 řešení sjednotíte řádky z T1 (přiřazení výsledkového sloupce nazvaného tid k 1) s řádky z T2 (přiřazení tid =0) a na základě výsledku definujete odvozenou tabulku s názvem T. Pomocí poslední nenulové techniky, založené na pořadí val, tid, keycol, pro každý aktuální řádek získáte hodnoty posledního řádku tid =0 zřetězené do binárního sloupce nazvaného below_binstr. Podobně získáte hodnoty dalšího řádku tid =0 zřetězené do binárního sloupce s názvem above_binstr.
Zde je kód implementující tento krok:
SELECT keycol, val, tid, MAX( CASE WHEN tid =0 THEN CAST(val AS binary(4)) + CAST(keycol AS binary(4)) END ) OVER( ORDER BY val, tid, keycol ROWS BETWEEN BEZ OMEZENÍ PŘEDCHOZÍ A 1 PŘEDCHOZÍ ) AS below_binstr, MIN( CASE WHEN tid =0 THEN CAST(val AS binary(4)) + CAST(keycol AS binary(4)) END ) OVER( ORDER BY val, tid, keycol ROWS BETWEEN 1 NÁSLEDUJÍCÍ A NEZÁVISLÉ NÁSLEDUJÍCÍ ) JAKO výše_binstrFROM ( SELECT klíčový sloupec, val, 1 AS tid FROM dbo.T1 UNION ALL SELECT MIN(klíčový sloupec) AS klíčový sloupec, val, 0 AS tid FROM dbo.T2 GROUP BY val ) AS T;Tento kód generuje následující výstup:
keycol val tid below_binstr above_binstr----------- ----------- ----------- --------- --------- ---------------------- 1 1 1 NULL 0X00000002000000012 1 1 NULL 0X00000002000000011 2 0 NULL 0X00000003000000044 3 0x0000000200000001 0x00000007000033 3 1 0x0000000300000004 0X00000007000000034 3 1 1 1 0x0000000300000004 0x00000007000000035 5 1 0x0000000300000004 0x00000007000000033 7 0 0x0000000300000004 0x0000000B000000066 8 1 0x0000000700000003 0x0000000B000000066 11 0 0x0000000700000003 0x0000000D000000088 13 0 0x0000000B00000006 0x00000011000000097 13 1 0x0000000D000000 08 0x00000011000000098 16 1 0x0000000d00000008 0x0000000011000000099 17 0 0x0000000d00000008 0x0000000000000000000000000 0000010 0 0x0000001100009 NULL10 20 1 0X1000000010000000A NUL110000000000000000SA1100000000000000SA101000000000000SA10 10 0X000000000A101000000000000000SA10 19 0X000000000A10100000000000000000A10 10.V kroku 2 řešení definujete CTE s názvem R1 na základě dotazu z kroku 1. Dotazujete se na R1, filtrujete pouze řádky, kde tid =1, a extrahujete jednotlivé hodnoty ze zřetězených binárních řetězců.
Zde je kód implementující tento krok:
SELECT keycol, val, CAST(SUBSTRING(below_binstr, 1, 4) AS int) AS below_T2_val, CAST(SUBSTRING(below_binstr, 5, 4) AS int) AS below_T2_keycol, CAST(SUBSTRING(nad_binstr, 1, 4) AS int) AS above_T2_val, CAST(SUBSTRING(above_binstr, 5, 4) AS int) AS above_T2_keycolFROM R1WHERE tid =1;Tento kód generuje následující výstup:
keycol val below_T2_val below_T2_keycol above_T2_val above_T2_keycol----------- ----------- ------------ ------- -------- ------------ ----------------1 1 NULL NULL 2 12 1 NULL NULL 2 13 3 3 4 7 34 3 3 4 7 35 5 3 4 7 36 8 7 3 11 67 13 13 8 17 98 16 13 8 17 99 18 17 9 19 1010 20 19 10 NULL 2 09 NULL NULL NULL NU1 NU1V kroku 3 řešení definujete CTE s názvem R2 na základě dotazu z kroku 2. Poté vypočítáte následující sloupce výsledků:
- below_T2_val:první nenull mezi below_T2_val a above_T2_val
- below_T2_keycol:první nenull mezi below_T2_keycol a above_T2_keycol
- above_T2_val:první nonnull mezi above_T2_val a below_T2_val
- above_T2_keycol:první nenull mezi above_T2_keycol a below_T2_keycol
Zde je kód implementující tento krok:
SELECT keycol, val, COALESCE(below_T2_val, above_T2_val) AS below_T2_val, COALESCE(below_T2_keycol, above_T2_keycol) AS below_T2_keycol, COALESCE(above_T2_val, above_T2_val) AS above_T2_col2_klíč, COAL_col2, COALCE_col2 Tento kód generuje následující výstup:keycol val below_T2_val below_T2_keycol above_T2_val above_T2_keycol----------- ----------- ------------ ------- -------- ------------ ---------------1 1 2 1 2 12 1 2 1 2 13 3 3 4 7 34 3 3 4 7 35 5 3 4 7 36 8 7 3 11 67 13 13 8 17 98 16 13 8 17 99 18 17 9 19 1010 20 19 10 19 19 101 01 19 101 předV kroku 4 řešení definujete CTE s názvem R3 na základě dotazu z kroku 3. Vrátíte keycol alias jako T1_keycol a val alias jako T1_val. Vypočítejte výsledný sloupec T2_keycol jako:pokud je absolutní rozdíl mezi hodnotou a below_T2_val menší nebo roven absolutnímu rozdílu mezi hodnotou above_T2_val a hodnotou, vraťte hodnotu below_T2_keycol, jinak nad_T2_keycol. Vypočítejte výsledný sloupec T2_val jako:pokud je absolutní rozdíl mezi hodnotou a below_T2_val menší nebo roven absolutnímu rozdílu mezi hodnotou above_T2_val a val, vraťte hodnotu pod_T2_val, jinak nad_T2_val.
Zde je kód implementující tento krok:
SELECT keycol AS keycol1, val AS value1, CASE WHEN ABS(val - below_T2_val) <=ABS(above_T2_val - val) THEN below_T2_keycol ELSE above_T2_keycol END AS keycol2, CASE WHEN ABS(val - below_T2_val) <=ABS(nad_T2) val) THEN below_T2_val ELSE above_T2_val END AS val2FROM R3;Plán řešení Alejandro s podpůrnými indexy je znázorněn na obrázku 3.
Obrázek 3:Plán řešení od Alejandra s indexy
Zde jsou statistiky výkonu pro tento plán:
doba běhu:7,8, CPU:12,6, logická čtení:35 886Plán řešení Alejandro bez podpory indexů je znázorněn na obrázku 4.
Obrázek 4:Plán řešení od Alejandra bez indexů
Zde jsou statistiky výkonu pro tento plán:
doba běhu:8,19, CPU:14,8, logická čtení:37 091Všimněte si, že v prvních dvou větvích plánu na obrázku 4 existují dva druhy před operátorem Merge Join (Concatenation) kvůli nedostatku podpůrných indexů. Tyto druhy nejsou v plánu na obrázku 3 potřeba, protože data jsou získávána předobjednaná z podpůrných indexů.
Kamilova variace na Alejandrovo řešení
Kamil v tomto řešení spoléhal i na poslední nenulovou techniku. Zde je úplný kód řešení:
WITH all_vals AS( SELECT keycol, val FROM dbo.T1 UNION ALL SELECT DISTINCT NULL, val FROM dbo.T2),ranges AS( SELECT keycol, val, MAX(CASE WHEN keycol IS NULL THEN val END) OVER (ORDER BY val, klíčová kolonka ŘÁDKY BETWEEN NEBOUNDED PŘEDCHÁZEJÍCÍ A 1 PŘEDCHOZÍ) AS prev, MIN (CASE, WHEN keycol JE NULL THEN val END) NAD (ORDER BY val, keycol ŘÁDKY BETWEEN 1 FOLLOWING A NEBOUNDED FOLLOWING) AS nxt FROM all_val END SELECT keycol AS keycol1, val AS val1, CASE WHEN ABS(val - prev) <=ISNULL(ABS(val - nxt), prev) THEN prev ELSE nxt END AS val2 FROM rozsahy WHERE keycol NENÍ NULL)SELECT keycol1, val1, SUBSTRING(T1.othercols, 1, 1) AS othercols1, val2, T2.keycol AS keycol2, SUBSTRING(T2.othercols, 1, 1) AS othercols2 FROM matched_vals AS M INNER JOIN ( SELECT *, ROW_NUMBER() OVER (PARTITION BY val ORDER BY keycol) AS rn FROM dbo.T2 ) AS T2 ON T2.val =M.val2 AND rn =1 INNER JOIN dbo.T1 ON T1.keycol =keycol1;V kroku 1 řešení definujete CTE nazývané all_vals a rozsahy, kde pomocí poslední nenulové techniky identifikujete pro každou hodnotu v T1 (kde keycol není NULL) rozsahy pod (předchozí) a nad (nxt) hodnoty z T2 ( kde keycol je null).
Zde je kód implementující tento krok:
WITH all_vals AS( SELECT keycol, val FROM dbo.T1 UNION ALL SELECT DISTINCT NULL, val FROM dbo.T2),ranges AS( SELECT keycol, val, MAX(CASE WHEN keycol IS NULL THEN val END) OVER (ORDER BY val, klíčová kolonka ŘÁDKY BETWEEN NEBOUNDED PŘEDCHÁZEJÍCÍ A 1 PŘEDCHÁZEJÍCÍ) AS předchozí, MIN (PŘÍPAD, KDY JE klíčová kolonka NULL THEN val END) NAD (POŘADÍ PODLE hodnoty, klíčová kolonka ŘÁDKY MEZI 1 NÁSLEDUJÍCÍ A NEODPOVĚDĚNÉ NÁSLEDUJÍCÍ) AS nxt VYBERTE Z rozsahu all_val;Tento kód generuje následující výstup:
keycol val prev nxt----------- ----------- ----------- ---------- -1 1 NULL 22 1 NULL 2NULL 2 NULL 3NULL 3 2 73 3 3 74 3 3 75 5 3 7 NULL 7 3 116 8 7 11 NULL 11 7 13NULL 13 11 1717 13 NULL 91 13 1717 13 20 19 NULL11 21 19 NULLV kroku 2 řešení se dotazujete na rozsahy CTE a filtrujete pouze řádky, kde keycol není null. Sloupce keycol a val vrátíte z T1 jako keycol1 a val1. Potom mezi prev a nxt vrátíte hodnotu, která je nejblíže hodnotě 1, jako hodnotu 2.
Zde je kód implementující tento krok:
SELECT keycol AS keycol1, val AS value1, CASE WHEN ABS(val - prev) <=ISNULL(ABS(val - nxt), prev) THEN prev ELSE nxt END AS val2FROM rangesWHERE keycol NENÍ NULL;Tento kód generuje následující výstup:
keycol1 val1 val2----------- ----------- -----------1 1 22 1 23 3 34 3 35 5 36 8 77 13 138 16 179 18 1710 20 1911 21 19V kroku 3 řešení definujete CTE s názvem matched_vals na základě dotazu z kroku 2. Definujete také odvozenou tabulku nazvanou T2, kde pomocí čísel rozdělených řádků zpracováváte logiku rozdělování pro řádky z dbo.T2. Poté spojíte matched_vals, CTE T2 a tabulku T1, abyste zvládli konečnou logiku párování.
Zde je kód implementující tento krok:
SELECT keycol1, val1, SUBSTRING(T1.othercols, 1, 1) AS othercols1, val2, T2.keycol AS keycol2, SUBSTRING(T2.othercols, 1, 1) AS othercols2 FROM matched_vals AS M INNER JOIN ( SELECT * , ROW_NUMBER() OVER (ODDĚLENÍ PODLE hodnoty ORDER BY keycol) AS rn OD dbo.T2 ) AS T2 ON T2.val =M.val2 AND rn =1 VNITŘNÍ PŘIPOJENÍ dbo.T1 NA T1.keycol =keycol1;Plán Kamilovy variace na Alejandrovo řešení s podpůrnými indexy je znázorněn na obrázku 5.
Obrázek 5:Plán Kamilovy variace na Alejandrovo řešení s indexy
Zde jsou statistiky výkonu pro tento plán:
doba běhu:11,5, CPU:18,3, logická čtení:59 218Plán Kamilovy variace na Alejandrovo řešení bez podpory indexů je znázorněn na obrázku 6.
Obrázek 6:Plán Kamilovy variace na Alejandrovo řešení bez indexů
Zde jsou statistiky výkonu pro tento plán:
doba běhu:12,6, CPU:23,5, logická čtení:61 432Podobně jako u plánů pro Alejandrovo řešení také v tomto případě plán na obrázku 5 nevyžaduje explicitní řazení před použitím operátoru Merge Join (zřetězení), protože data jsou načítána předobjednaná z podpůrných indexů a plán na obrázku 6 ano. vyžadují explicitní řazení, protože neexistují žádné podpůrné indexy.
Řešení Radek Celuch – Použití intervalů
Radek přišel s jednoduchým a krásným nápadem. Pro každou odlišnou hodnotu v T2.val vypočítáte interval představující rozsah hodnot z T1.val, který by odpovídal aktuální T2.val.
Zde je úplný kód řešení:
WITH Pre1 AS( SELECT keycol, val, othercols, ROW_NUMBER() OVER(PARTITION BY val ORDER BY keycol) AS rn FROM dbo.T2),Pre2 AS( SELECT keycol, val, othercols, LAG(val) OVER( ORDER BY val) AS předchozí, LEAD(val) OVER(ORDER BY val) AS next FROM Pre1 WHERE rn =1),T2 AS( SELECT keycol, val, othercols, ISNULL(val - (val - prev) / 2 + ( 1 - (hodnota - předchozí) % 2), 0) AS nízké, ISNULL(hodnota + (další - hodnota) / 2, 2147483647) AS high FROM Pre2)SELECT T1.keycol AS keycol1, T1.val AS val1, SUBSTRING( T1.othercols, 1, 1) AS othercols1, T2.keycol AS keycol2, T2.val AS val2, SUBSTRING(T2.othercols, 1, 1) AS othercols2FROM dbo.T1 INNER JOIN T2 ON T1.val MEZI T2.low AND T2.high;V kroku 1 řešení dotazujete T2 a vypočítáte čísla řádků (zavolejte sloupec rn), rozdělené podle val a seřazené podle keycol. Na základě tohoto dotazu definujete CTE s názvem Pre1. Poté se dotazujete na Pre1 a filtrujete pouze řádky, kde rn =1. Ve stejném dotazu pomocí LAG a LEAD vrátíte pro každý řádek hodnotu sloupce val z předchozího řádku (nazývejte jej prev) az následujícího řádku ( zavolejte to příště).
Zde je kód implementující tento krok:
WITH Pre1 AS( SELECT keycol, val, othercols, ROW_NUMBER() OVER(PARTITION BY val ORDER BY keycol) AS rn FROM dbo.T2)SELECT keycol, val, othercols, LAG(val) OVER(ORDER BY value) AS předchozí, LEAD(val) OVER(ORDER BY val) AS nextFROM Pre1WHERE rn =1;Tento kód generuje následující výstup:
keycol val othercols prev next------- ---- ---------- ----------- ---------- -1 2 0xBB NULL 34 3 0xBB 2 73 7 0xBB 3 116 11 0xBB 7 138 13 0xBB 11 179 17 0xBB 13 1910 19 0xBB 17 NULLV kroku 2 řešení definujete CTE s názvem Pre2 na základě dotazu z kroku 1. Dotazujete se Pre2 a vypočítáte pro každý řádek interval [nízká, vysoká], kam by spadala hodnota z T1. Zde jsou výpočty pro nízké a vysoká:
- nízké =ISNULL(hodnota – (hodnota – předchozí) / 2 + (1 – (hodnota – předchozí) % 2), 0)
- vysoká =ISNULL(hodnota + (další – hodnota) / 2, 2147483647)
Kontrola modu 2 při výpočtu dolní hranice se používá ke splnění nižšího požadavku T2.val a filtr čísla řádku se používá ke splnění nižšího požadavku T2.keycol. Hodnoty 0 a 2147483647 se používají jako krajní dolní a horní limity.
Zde je kód implementující tento krok:
SELECT keycol, val, othercols, ISNULL(val - (val - předchozí) / 2 + (1 - (val - předchozí) % 2), 0) AS low, ISNULL(val + (další - val) / 2 , 2147483647) AS highFROM Pre2;Tento kód generuje následující výstup:
keycol val othercols low high------- ---- ---------- ---- -----------1 2 0xBB 0 24 3 0xBB 3 53 7 0xBB 6 96 11 0xBB 10 128 13 0xBB 13 159 17 0xBB 16 1810 19 0xBB 19 2147483647V kroku 3 řešení definujete CTE s názvem T2 na základě dotazu z kroku 2. Poté spojíte tabulku T1 s řádky CTE T2 odpovídajícími na základě hodnoty z T1, které jsou v intervalu [nízká, vysoká] v T2.
Zde je kód implementující tento krok:
SELECT T1.keycol AS keycol1, T1.val AS val1, SUBSTRING(T1.othercols, 1, 1) AS othercols1, T2.keycol AS keycol2, T2.val AS val2, SUBSTRING(T2.othercols, 1, 1) ) JAKO othercols2FROM dbo.T1 INNER JOIN T2 NA T1.val MEZI T2.low A T2.high;Plán Radkova řešení s podpůrnými indexy je na obrázku 7.
Obrázek 7:Plán řešení od Radka s indexy
Zde jsou statistiky výkonu pro tento plán:
doba běhu:10,6, CPU:7,63, logická čtení:3 193 125Plán Radkova řešení bez podpůrných indexů ukazuje obrázek 8.
Obrázek 8:Plán řešení od Radka bez indexů
Zde jsou statistiky výkonu pro tento plán:
doba běhu:18.1, CPU:27.4, logická čtení:5 827 360Zde je výkonnostní rozdíl mezi těmito dvěma plány poměrně podstatný. Plán na obrázku 8 vyžaduje předběžné třídění před výpočtem čísel řádků, což plán na obrázku 7 neumožňuje. Ještě důležitější je, že pro spárování každého intervalu s příslušnými řádky z T1, plán na obrázku 8 vytvoří indexové řazení v podstatě jako alternativu k chybějícímu indexu, zatímco plán na obrázku 7 jednoduše používá existující index idx_val_key na T1. To je hlavní důvod velkých rozdílů ve všech ukazatelích výkonu. Přesto je výkon bez podpory indexů přiměřený.
Kamilova variace na Radkovo řešení
Kamil přišel s variantou Radkova řešení. Zde je úplný kód řešení:
WITH T2_collapsed AS( SELECT keycol AS keycol2, val AS val2 , ROW_NUMBER() OVER (PARTITION BY val ORDER BY keycol) AS rn FROM dbo.T2),ranges AS( SELECT keycol2 AS prevkey, val2 AS prevval, LEAD( keycol2) OVER (ORDER BY val2) AS nxtkey, LEAD(val2) OVER (ORDER BY val2) AS nxtval FROM T2_collapsed WHERE rn =1),nejlépe odpovídá AS( SELECT T1.keycol AS keycol1, T1.val AS val1, SUBSTRING(T1) .othercols, 1, 1) AS othercols1, CASE WHEN ABS(T1.val - prevval) <=ABS(T1.val - nxtval) THEN prevkey ELSE nxtkey END AS keycol2, CASE WHEN ABS(T1.val - prevval) <=ABS(T1.val - nxtval) THEN prevval ELSE nxtval END AS val2 FROM rozsahů INNER JOIN dbo.T1 ON prevval <=T1.val AND T1.valHere’s Kamil’s own description of this solution:
This solution is close to Radek's idea of checking against low and high range, although the ranges are based purely on actual T2 values. We get each row and the lead values only for each row in collapsed T2 (first step always gets rid of duplicate keys if found, by using row number or min(keycol) grouped by val on t2). The key concepts are how to check low and high range not to get duplicates – lower range inclusive, higher range exclusive, as well as handling the outliers (if present) by creating a separate set looking at the lowest and max values in T2 (UNION ALL bit). The check against the max value is done with inclusive range to complement the case of T1 vals being equal to the top T2 vals.
The plan for Kamil’s variation on Radek’s solution with supporting indexes is shown in Figure 9.
Figure 9:Plan for Kamil’s variation on Radek’s solution with indexes
Here are the performance stats for this plan:
run time:8.59, CPU:8.5, logical reads:3,206,849The plan for Kamil’s variation on Radek’s solution without supporting indexes is shown in Figure 10.
Figure 10:Plan for Kamil’s variation on Radek’s solution without indexes
Here are the performance stats for this plan:
run time:14, CPU:24.5, logical reads:5,870,596The plan in Figure 9 is serial and most of the calculations are done based on preordered data obtained from the supporting indexes. The plan in Figure 10 is parallel, plus there are a few sorts, and even an index spool activity. The performance measures of the latter plan are substantially higher than the former plan, but the performance in both cases is reasonable.
Solution 2 by Kamil Kosno – Using ranking, offset and aggregate window functions
Kamil came up with another original approach that is based on a combination of ranking, offset and aggregate window functions. Here’s the complete solution’s code:
WITH A AS( SELECT 1 AS t, keycol,val, DENSE_RANK() OVER(ORDER BY val) AS rnk FROM dbo.T1 UNION ALL SELECT NULL, MIN(keycol), val, 0 FROM dbo.T2 GROUP BY val),B AS( SELECT t, keycol, val, CASE WHEN t =1 THEN DENSE_RANK() OVER (ORDER BY val, t) - rnk ELSE 0 END AS grp, LAG(CASE WHEN t IS NULL THEN val END) OVER(ORDER BY val, t) AS prev, LAG(CASE WHEN t IS NULL THEN keycol END) OVER(ORDER BY val, t) AS prevkey, LEAD(CASE WHEN t IS NULL THEN val END) OVER(ORDER BY val, t) AS nxt, LEAD(CASE WHEN t IS NULL THEN keycol END) OVER(ORDER BY val, t) AS nxtkey FROM A),C AS( SELECT keycol AS keycol1, val AS val1, MAX (prev) OVER(PARTITION BY grp ORDER BY prev DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS mxprev, MAX (prevkey) OVER(PARTITION BY grp ORDER BY prev DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS mxprevkey, MAX (nxt) OVER(PARTITION BY grp ORDER BY nxt ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS mxnxt, MAX (nxtkey) OVER(PARTITION BY grp ORDER BY nxt ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS mxnxtkey FROM B WHERE t =1),D AS( SELECT keycol1, val1, CASE WHEN ABS(val1 - mxprev) <=ISNULL(ABS(val1 - mxnxt), mxprev) THEN mxprevkey ELSE mxnxtkey END AS keycol2, CASE WHEN ABS(val1 - mxprev) <=ISNULL(ABS(val1 - mxnxt), mxprev) THEN mxprev ELSE mxnxt END AS val2 FROM C)SELECT D.keycol1, D.val1, D.keycol2, D.val2, SUBSTRING(T1.othercols, 1, 1) AS othercols1, SUBSTRING(T2.othercols, 1, 1) AS othercols2FROM D INNER JOIN dbo.T1 ON T1.keycol =D.keycol1 INNER JOIN dbo.T2 ON T2.keycol =D.keycol2;In Step 1 of the solution you unify the following result sets:
- Rows from T1 with a result column called t assigned with the constant 1 and column rnk representing dense rank values ordered by val
- Group rows from T2 by val and return val and min keycol for each group, with the result column t assigned with NULL and rnk with 0
Zde je kód implementující tento krok:
SELECT 1 AS t, keycol,val, DENSE_RANK() OVER(ORDER BY val) AS rnkFROM dbo.T1UNION ALLSELECT NULL, MIN(keycol), val, 0FROM dbo.T2GROUP BY val;Tento kód generuje následující výstup:
t keycol val rnk----------- ----------- ----------- --------------------1 1 1 11 2 1 11 3 3 21 4 3 21 5 5 31 6 8 41 7 13 51 8 16 61 9 18 71 10 20 81 11 21 9NULL 1 2 0NULL 4 3 0NULL 3 7 0NULL 6 11 0NULL 8 13 0NULL 9 17 0NULL 10 19 0In Step 2 of the solution you define a CTE called A based on the query from Step 1. You query A and compute group identifiers (grp) for T1 values that fall between ranges of distinct T2 values. This is done using an islands technique where you subtract rnk (dense ranks for only T1 values) from rnk2 (dense ranks for unified T1 values and distinct T2 values).
Using the LAG and LEAD functions, you compute for each T1 row the prev/next val and keycol values from T2, if present, and NULL otherwise. These calculations return values for the first and last rows in each group, if present, but NULLs for intermediate rows in groups.
Zde je kód implementující tento krok:
SELECT t, keycol, val, rnk, DENSE_RANK() OVER (ORDER BY val) AS rnk2, CASE WHEN t =1 THEN DENSE_RANK() OVER (ORDER BY val, t) - rnk ELSE 0 END AS grp, LAG(CASE WHEN t IS NULL THEN val END) OVER(ORDER BY val, t) AS prev, LAG(CASE WHEN t IS NULL THEN keycol END) OVER(ORDER BY val, t) AS prevkey, LEAD(CASE WHEN t IS NULL THEN val END) OVER(ORDER BY val, t) AS nxt, LEAD(CASE WHEN t IS NULL THEN keycol END) OVER(ORDER BY val, t) AS nxtkeyFROM A;Tento kód generuje následující výstup:
t keycol val rnk rnk2 grp prev prevkey nxt nxtkey----- ------ --- --- ---- --- ----- ------- ----- -------1 1 1 1 1 0 NULL NULL NULL NULL1 2 1 1 1 0 NULL NULL 2 1NULL 1 2 0 2 0 NULL NULL 3 4NULL 4 3 0 3 0 2 1 NULL NULL1 3 3 2 3 2 3 4 NULL NULL1 4 3 2 3 2 NULL NULL NULL NULL1 5 5 3 4 2 NULL NULL 7 3NULL 3 7 0 5 0 NULL NULL NULL NULL1 6 8 4 6 3 7 3 11 6NULL 6 11 0 7 0 NULL NULL 13 8NULL 8 13 0 8 0 11 6 NULL NULL1 7 13 5 8 5 13 8 NULL NULL1 8 16 6 9 5 NULL NULL 17 9NULL 9 17 0 10 0 NULL NULL NULL NULL1 9 18 7 11 6 17 9 19 10NULL 10 19 0 12 0 NULL NULL NULL NULL1 10 20 8 13 7 19 10 NULL NULL1 11 21 9 14 7 NULL NULL NULL NULLIn Step 3 you define a CTE called B based on the query from Step 2. You Query B and filter only original T1 rows (where t =1). In each group, you return the first row's prev and prevkey values, and last row's nxt and nxtkey values. Instead of just using a window partition clause, a window frame specification is used to define the minimal frame with the desired row to reduce I/O against the spool.
Zde je kód implementující tento krok:
SELECT keycol AS keycol1, val AS val1, MAX (prev) OVER(PARTITION BY grp ORDER BY prev DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS mxprev, MAX (prevkey) OVER(PARTITION BY grp ORDER BY prev DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS mxprevkey, MAX (nxt) OVER(PARTITION BY grp ORDER BY nxt ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS mxnxt, MAX (nxtkey) OVER(PARTITION BY grp ORDER BY nxt ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS mxnxtkeyFROM BWHERE t =1;Tento kód generuje následující výstup:
keycol1 val1 mxprev mxprevkey mxnxt mxnxtkey----------- ----------- ----------- ----------- ----------- -----------2 1 NULL NULL 2 11 1 NULL NULL 2 15 5 3 4 7 33 3 3 4 7 34 3 3 4 7 36 8 7 3 11 68 16 13 8 17 97 13 13 8 17 99 18 17 9 19 1010 20 19 10 NULL NULL11 21 19 10 NULL NULLIn Step 4 you define a CTE called C based on the query from Step 3. You query C to compute keycol2 and val2 based on the closest match.
Zde je kód implementující tento krok:
SELECT keycol1, val1, CASE WHEN ABS(val1 - mxprev) <=ISNULL(ABS(val1 - mxnxt), mxprev) THEN mxprevkey ELSE mxnxtkey END AS keycol2, CASE WHEN ABS(val1 - mxprev) <=ISNULL(ABS(val1 - mxnxt), mxprev) THEN mxprev ELSE mxnxt END AS val2FROM C;Tento kód generuje následující výstup:
keycol1 val1 keycol2 val2----------- ----------- ----------- -----------2 1 1 21 1 1 25 5 4 33 3 4 34 3 4 36 8 3 78 16 9 177 13 8 139 18 9 1710 20 10 1911 21 10 19In Step 5 you define a CTE called D based on the query from Step 4. You then join D with T1 and T2 to get the other needed columns.
Zde je kód implementující tento krok:
SELECT D.keycol1, D.val1, D.keycol2, D.val2, SUBSTRING(T1.othercols, 1, 1) AS othercols1, SUBSTRING(T2.othercols, 1, 1) AS othercols2FROM D INNER JOIN dbo.T1 ON T1.keycol =D.keycol1 INNER JOIN dbo.T2 ON T2.keycol =D.keycol2;The plan for solution 2 by Kamil with supporting indexes is shown in Figure 11.
Figure 11:Plan for solution 2 by Kamil with indexes
Here are the performance stats for this plan:
run time:18.1, CPU:34.4, logical reads:56,736The plan for solution 2 by Kamil without supporting indexes is shown in Figure 12.
Figure 12:Plan for solution 2 by Kamil without indexes
Here are the performance stats for this plan:
run time:20.3, CPU:38.9, logical reads:59,012As you can see, the Plan in Figure 12 involves a couple of extra sorts compared to the plan in Figure 1 to order the data for the two queries in the CTE A—one to support the dense rank calculation and the other to support the grouped query. Other than that, the rest of the work is similar in both. In the grand scheme of things, there’s a bit of extra CPU work and consequentially time penalty, but still both queries perform fairly reasonably.
Závěr
This article concludes the series on the closest match challenge. It’s great to see the different creative ideas that Kamil, Alejandro and Radek came up with. Thanks to all of you for sending your solutions!