Common Table Expressions i iterativni ciklusi
Monday, 21.01.2008 – ZidarUpotreba CTE i iterativni procesi
Kako bismo u MS SQL ispisali prvih 50 celih brojeva tako da svaki broj bude u posebnom redu?
1
2
3
…
50
Nešto ovako bi možda pomoglo:
DECLARE @i int SET @i = 0 WHILE @i<=50 BEGIN SET @I = @i + 1 SELECT @i AS i CONTINUE END
Nažalost, svaki SELECT se izvrsava za sebe:
i ----------- 1
(1 row(s) affected)
i ----------- 2
(1 row(s) affected)
i ----------- 3
Ovo je vec bolje:
DECLARE @i int SET @i = 0 WHILE @i<=50 BEGIN SET @I = @i + 1 -- SELECT @i AS i PRINT @i CONTINUE END
Međutim, s tim ne možemo ništa korisno da uradimo. Možda da rezultat cut/paste u Excel pa to da importujemo u SQL bazu, pa onda uradimo.. itd… itd…. Isuviše veliki posao za tako prostu stvar.
Na sreću, MS SQL 2005 ima Common Table Expressions. Probajte ovo:
WITH MyCTE AS ( SELECT 1 AS i UNION ALL SELECT i+1 FROM MyCte WHERE i<=50 ) SELECT * FROM MyCTE
Posto je ovo CTE, imamo posla u stvari sa temp tabelom i rezultat se moze upotrebiti u nekom SQL izrazu na način na koji smo pokazali u jednom od prethodnih napisa.
Pre nego objasnimo kako ovo radi i šta se dešava, evo još jednog prostog primera. Zadatak je ispisati sve dane u zadatom mesecu. Mogu da probam ovako:
WITH Mesec AS -- za mesec Mart 2008 ( SELECT CAST('2008/03/01' AS datetime) AS Dan UNION ALL SELECT Dateadd(day,1,Dan ) AS Dan FROM Mesec WHERE Dan <= CAST('2008/03/30' AS datetime) ) SELECT * FROM Mesec
Možemo da uopštimo problem i da tražimo da se vrate svi datumi između dva zadata datuma (datumski opseg, ‘date range’) Ovako nekako bi moglo:
DECLARE @OdDatuma as datetime, @DoDatuma as datetime SET @OdDatuma = '2007/11/1' SET @DoDAtuma = '2007/11/30' ; WITH OpsegDatuma AS -- ako WITH nije prva linija u batchu, stavlja se tacka zarez ispred ( SELECT @OdDatuma AS Datum UNION ALL SELECT Dateadd(day,1,Datum) AS Datum FROM OpsegDatuma WHERE Dateadd(day,1,Datum) <= @DoDatuma ) SELECT * FROM OpsegDatuma
A možemo da napičemo i funkciju, OpesgDatuma, koja za zadata dva datuma vraća sve dane izmedju njih.
CREATE FUNCTION dbo.OpsegDatuma(@OdDatuma as datetime, @DoDatuma as datetime) RETURNS TABLE AS RETURN ( WITH OpsegDatuma AS ( SELECT @OdDatuma AS Datum UNION ALL SELECT Dateadd(day,1,Datum) AS Datum FROM OpsegDatuma WHERE Dateadd(day,1,Datum) <= @DoDatuma ) SELECT Datum FROM OpsegDatuma )
Sada može da se napiše i ovo:
SELECT * FROM dbo.OpsegDatuma('2007/11/1','2007/11/30')
ali i ovo:
SELECT D.Datum , COALESCE(COUNT(N.NarudzbaID),0) AS [Broj narudzbi na ovaj dan] FROM dbo.OpsegDatuma('2007/11/1','2007/11/30') AS D LEFT JOIN Narudzbe AS N ON D.Datum = F.DatumNarudzbe GROUP BY D.Datum
Poslednji izraz vraća SVE datume u zadatom opsegu, pa i one kad nije bilo narudžbi.
Bez funkcije i bez Common Table Expression ovakav izraz bi bio veoma zahtevan za pisanje.
Vratimo se na pocčtak, da pokušamo da objasnimo šta se dešava. Izraz
WITH MyCTE AS ( SELECT 1 AS i UNION ALL SELECT i+1 FROM MyCte WHERE i<=50 ) SELECT * FROM MyCTE
ima 3 dela, kao i svaki CTE.
WITH MyCTE AS
jeste deklaracija CTE izraza, gde je MyCTE ime koje smo dodelili.
Izraz u zagradi
( SELECT 1 AS i UNION ALL SELECT i+1 AS i FROM MyCte WHERE i<=50 )
je definicija naše temp tabele, šta će CTE da nam vrati.
Na kraju izrazom
SELECT * FROM MyCTE
pozivamo CTE. Ništa novo u odnosu na ono što smo već ranije znali.
Izraz u zagradi je ono što nas interesuje. I tu imamo 3 dela:
SELECT 1 AS i
zove se ‘anchor member’, u slobodnom prevodu ‘prvi red CTE tabele’
UNION ALL
je deo sintakse i jednostavno tako mora da se pise, tako trazi Microsoft
Treći deo je glavna stvar. Tu se dešavaju itaracije.
SELECT i+1 FROM MyCTE WHERE i<=50
Zapazite FROM MyCTE. Izraz u zagradi je dakle unija dva seta, prvi deo vraća jedan red, a drugi iskaz poziva samu CTE tabelu tabelu. Otud rekurzija. Kao u matematici, funkcija poziva samu sebe.
Šta se dešava u pozadini? Anchor member vraća tačno jedan red. U tom momentu CTE ima taj jedan red. To se unionizuje sa izrazom
SELECT i+1 AS i FROM MyCTE
što vracć još jedan red, sa uvećanim i za 1, koliko smo stavili u SELECT i+1
Sada već imamo dva reda u tabeli, vrednsti su 1,2. Ciklus se nastavlja dok važi WHERE uslov.
Posto imamo i=2, to je manje od 50, ciklus se nastavlja dok ne stignemo do 50. Ne pitajte me zašto ni kako, ali tako to radi.
Mnogo detaljnije i lepše objasnjenje imate ovde:
http://www.simple-talk.com/sql/sql-server-2005/sql-server-2005-common-table-expressions/
I zapamtite ime Nigel Rivett. Ot tog gospodina se ima mnogo sta naučiti.
Jos neke važne stvari da na pomenem. WHERE nije obavezno i u tom slucčju će, teorijski, ciklus biti beskonačan. Pošto beskonačnost postoji samo u teoriji, Microsoft je to ograničio na 100. Ako dakle odradite
WITH MyCTE AS ( SELECT 1 AS i UNION ALL SELECT i+1 FROM MyCte ) SELECT * FROM MyCTE
dobićete 101 red. Kakav bi to alat bio da se ne može primeniti na više od 100 ciklusa?
Probajmo
WITH MyCTE AS ( SELECT 1 AS i UNION ALL SELECT i+1 FROM MyCte WHERE i<=1000 ) SELECT * FROM MyCTE
i dobićemo grešku:
Msg 530, Level 16, State 1, Line 1 The statement terminated. The maximum recursion 100 has been exhausted before statement completion.
Microsoft je mislio i na to. Postoji klauzula option (maxrecursion N) koja se dodaje na kraj svega, da bi se povecao broj iteracija, ako treba do vrednosti N.
WITH MyCTE AS ( SELECT 1 AS i UNION ALL SELECT i+1 FROM MyCte WHERE i<=1000 ) SELECT * FROM MyCTE option (maxrecursion 5000)
I ovde ima ograničenje. N u option (maxrecursion N) može da ide do 32767.
Svakako da bi i ovo u praksi bilo malo. Zasto samo 32767? Samo Bog zna, a posto Boga nema, onda ne zna niko (Ovu rečenicu izgovorio je Zoran Radmilovic kao Radovan III, ja nemam ništa stim i ne odgovaram na moguće komentare)
Postoji nedokumentovana opcija (ili bar slabo dokumentovana) (maxrecursion 0)
WITH MyCTE AS ( SELECT 1 AS i UNION ALL SELECT i+1 FROM MyCte WHERE i<=50000 ) SELECT * FROM MyCTE option (maxrecursion 0)
Ako ste probali da odradite poslednjih nekoliko primera, uočite kakvom brzinom se proizvede recimo 50,000 brojeva.
Postoje egzotični nacčni da se isti rezultat dobije jos brzž, ali meni je i manje od 1 sec za 50,000 redova dovoljno brzo.
Svi ostali načini kojih se obični smrtnici mogu setiti su za dva reda velicine sporiji od WITH sa rekurzijom.
Ako želite nešto brze od ovoga, pogledajte knjigu ‘Inside SQL Server 2005: T-SQL Querying’, autori Itzik Ben-Gan, Lubor Kollar i Dejan Sarka. Treći autor je očigledno naš covek, a i Lubor nije daleko, tek da se zna 🙂
Igrajte se neko vreme sa slicnim primerima, i svakako posetite sajt na kome piše Nigel Rivett. Kad svarimo ove ‘jednostavne’ upoterbe CTE izraza, pokušaću da se pozabavim hijerarhijama i mrežama.
Ono što smo videli do sada čini mi se definitivno ubija potrebu za kursorima.