Od každého jen jednu (DISTINCT ON)

20
Pro/09
2

Klasický DISTINCT ALL asi všichni znáte. V dnešním článku se podíváme na jeho variantu DISTINCT ON (…), která zajišťuje jedinečnost výsledku jen ve vyjmenovaných sloupcích.

Knížky a autoři

Naším cílem bude zjistit, jakou nejtlustší knížku každý autor napsal a počet jejích stránek. Chceme tedy vypsat: název knížky, počet stran a jméno autora.

Potřebná data najdeme ve dvou tabulkách: autor a kniha ve schématu sbirka.

Kdybychom nepotřebovali vypsat i název každé knížky, mohli bychom výsledek zjistit celkem jednoduše pomocí GROUP BY a agregační funkce max(). Název knížky bychom si mohli následně připojit (JOIN), ale bylo by to dost neefektivní řešení.

Naštěstí můžeme využít DISTINCT ON a dotaz je pak výrazně jednodušší – a to jak na zápis, tak na provedení:

SELECT 	DISTINCT ON (a.id)
	k.nazev,
	k.pocet_stran,
	a.jmeno,
	a.prijmeni
FROM	sbirka.kniha AS k
JOIN 	sbirka.autor AS a ON (k.autor = a.id)
ORDER BY a.id, k.pocet_stran DESC;

Samotný DISTINCT se ve výchozím stavu chová jako DISTINCT ALL, což znamená, že nám z výsledku zbudou jen ty záznamy, které jsou ve všech sloupcích jedinečné (nebudeme mít žádné duplicity). Pokud ale za DISTINCT uvedeme klíčové slovo ON a výčet sloupců, zůstanou nám ve výsledku záznamy jedinečné jen v těchto sloupcích. Jelikož jsme uvedli ON (a.id), budeme mít ve výpisu každého autora jen jednou. Podle tohoto sloupce musíme mít i řazení. Který záznam se od každého autora vezme, záleží na řazení – vezme se ten první – proto: k.pocet_stran DESC;. Pokud budeme chtít mít výsledek seřazený např. podle příjmení a jména autora, můžeme dotaz obalit ještě jedním dotazem a řazení provést v něm.

Více verzí článku

Dejme tomu, že máme redakční systém a verzujeme si články – tzn. uchováváme si i historické verze. Tabulka verzovaných článků by mohla vypadat nějak takhle:

CREATE TABLE verze_clanku
(
  clanek integer NOT NULL, -- ID článku
  verze integer NOT NULL, -- ID verze
  datum timestamp with time zone NOT NULL DEFAULT now(),
  nadpis character varying(255) NOT NULL,
  obsah text NOT NULL,
  CONSTRAINT verze_clanku_pk PRIMARY KEY (clanek, verze)
);

Tímto dotazem získáme od každého článku jeho aktuální verzi a výsledek seřadíme od nejnovějších článků k nejstarším:

SELECT * FROM(
    SELECT  DISTINCT ON (clanek)
	clanek,
	datum,
	nadpis 
    FROM verze_clanku
    ORDER BY clanek, verze DESC
) AS verze
ORDER BY clanek DESC;

Dalším případem, kdy se nám DISTINCT ON bude hodit je hlasování a různé ankety. Buď umožníme každému uživateli hlasovat jen jednou, nebo ho necháme hlasovat, kolikrát chce, ale do výsledků budeme počítat jen jeho poslední hlas.

Závěr a odkazy

DISTINCT ON je velice užitečná konstrukce pro psaní SQL dotazů, ovšem není dostupná na všech platformách. Někdy je potřeba ji suplovat kódem v aplikační vrstvě – řazení v dotazu zůstane stejné, ale při procházení výsledku v aplikaci si z každé skupiny (např. autor nebo článek) vezmeme jen jeden záznam a ostatní přeskočíme (dokud nenarazíme na záznam z následující skupiny). Tento postup je ale dost neefektivní – jednak nám zbytečně bobtná kód aplikace a jednak se mezi databází a aplikací přenášejí data, která vůbec nepotřebujeme. Další možností je, obejít problém složitějším SQL dotazem, ale ani to není moc elegantní. Jak řešíte tuto situaci vy, pokud nemůžete použít DISTINCT ON?

Odkazy:

  • ABC Linuxu – diskuse, která byla inspirací pro tento článek.
  • DISTINCT – oficiální dokumentace PostgreSQL.

MySQL

Anonym
7:07 odpoledne on Leden 8th, 2012

Funguje i v MySQL! Vyzkoušeno, dnes jsem to potřeboval.

Je to docela uzitecna konstrukce, nicmene mam pocit, ze existuje pouze pro PostgreSQL.