Před dvěma týdny jsem na svém blogu uveřejnil SQL hádanku. Jednalo se o to, jak v SQL vybrat z určité tabulky se sloupci Datum a Skupina pět nejnižších dat z každé skupiny, přičemž skupinou se myslí záznamy, kterémají stejnou hodnotu ve sloupci Skupina.
Během pěti minut po zprávičce na mém Twitteru mi přistály v e-mailové schránce 3 správná řešení. Bylo to samozřejmě od lidí, které znám a od kterých jsem to čekal. Evidentně už podobný dotaz někdy museli psát také (anebo četli Altairův článek), proto věděli, jak se to dělá.
Během následujících hodin a dní mi už začaly chodit spíše řešení, kde čtenáři ranking functions v MS SQL Serveru neznali, anebo je nenapadlo, že by se daly použít. Já sám jsem na blog tuto hádanku napsal a až po příchodu prvního řešení jsem si vzpomněl na to, že znám klíčové slovo PARTITION BY, akorát mě nenapadlo ho použít. Ono se to dá i bez něj (že by další hádanka?), ale ideální řešení to není, i když bude pravděpodobně skoro stejně rychlé.
Rozhodně není správné řešení toto, protože tady pro každý řádek pouštíte jeden SELECT dotaz. Pokud řádků bude sto tisíc, děláte sto tisíc a jeden SELECT. Časová náročnost dotazů se sice nedá počítat jen podle počtu prováděných selectů, ale jako takový základní odhad to většinou stačí. Ona si konec konců databáze ty dotazy dost optimalizuje, na druhou stranu na to není dobré vždy spoléhat. Takhle tedy rozhodně ne.
SELECT *
FROM @t [t]
WHERE
[t].[Number] IN (SELECT TOP 4 [Number] FROM @t WHERE [CategoryId] = [t].[CategoryId] ORDER BY [Number])
ORDER BY [t].[CategoryId], [t].[Number]
Správné řešení je využít funkci ROW_NUMBER, která umí záznamy v sadě výsledků očíslovat podle pořadí dle nějakého klíče. Je nutné ji ještě spojit s klíčovým slovem PARTITION BY, které řekne, že se má číslovat v rámci skupin určených stejnou hodnotou nějakého výrazu.
SELECT [t].[CategoryId], [t].[Number]
FROM
(SELECT ROW_NUMBER() OVER (PARTITION BY [CategoryId] ORDER BY [Number]) AS [RowNumber], [CategoryId], [Number] FROM @t) [t]
WHERE [t].[RowNumber] <= 4
To by tedy mělo být ideální a nejrychlejší řešení. U vnořených SELECTů je nutné dávat dobrý pozor. Někdo říká, že by se používat vůbec neměly, někdo na to kašle a používá je všude, protože nezná klíčové slovo JOIN, což rozhodně správné není.
Je nutné si uvědomit, jestli se subselect spouští jednou anebo pro každý řádek. Pokud je v sekci FROM, pak se vytáhne pouze jednou a pak se s ním pracuje. Pokud je v sekci SELECT nebo WHERE, je to problém, protože se spouští pro každý řádek. Pokud je řádků pár a server není zatížený, tak to ani nepoznáte. Pokud ale máte na krku velkou databázi, rozdíly jsou dosti dramatické.