Konkatenacija string vrednosti iz razlicitih redova (2. deo)
Objavljeno: 18-02-2014 | Autor: Tatjana Mađer | Kategorija: T-SQL
Ознаке: collation
2
Vrlo često se programeri susreću sa zahtevom da prikažu u aplikaciji izveštaj koji sadrži sumarne rezultate gde je potrebno prikazati u posebnoj koloni svakog sumarnog reda sve članove koji učestvuju u agregatnom redu.
U prethodnom delu ovog članka, objašnjeni su načini konkatenacije string vrednosti ukoliko je unapred poznat i ograničen broj redova i stringova koje treba spojiti.
Nepoznat broj redova
Slučajevi sa kojima se developeri najčešće susreću su oni kada je broj redova nepoznat. U ovim slučajevima je kod komplikovaniji i zahteva napredno poznavanje transakcionog SQL jezika. Varijanti za rešenje ovog problema ima više i najinteresantnije će biti objašnjene u nastavku.
a) Korišćenje rekurzije uz pomoć CTE analogno generisanju hijerarhije. CASE u rekurzivnom delu upita je stavljen da bi se izbegao zarez na prvom mestu rezultata konkatenacije, što se može postići i na drugi način. Glavni nedostatak ove metode je to što brzina izvršavanja ovog upita postaje nezadovoljavajuća sa porastom broja nivoa. Treba obratiti pažnju i na sledeće: s obzirom na to da je podrazumevani limit za broj nivoa u rekurziji 100, preporuka je koristiti hint MAXRECURSION koji omogućava definisanje maksimalnog broja nivoa u rekurziji, gde je omogućeno definisati i beskonačan broj redova (Maxrecursion 0) što je i urađeno u narednom primeru.
WITH CTE ( CrtacId, Konkatenacija, Naziv, Duzina,EpizodaID ) AS ( SELECT e.CrtacID as NazivCrtaca, CAST( '' AS NVARCHAR(MAX) ), CAST( '' AS NVARCHAR(MAX) ), 0, 0 FROM dbo.CrtaneEpizode E GROUP BY e.CrtacID UNION ALL SELECT e.CrtacID, CAST( Konkatenacija + CASE WHEN Duzina = 0 THEN '' ELSE ', ' END + e.Naziv AS NVARCHAR(MAX) ), CAST( e.Naziv AS NVARCHAR(MAX)), Duzina + 1, e.EpizodaID FROM CTE c INNER JOIN dbo.CrtaneEpizode E ON E.CrtacID=C.CrtacID WHERE e.EpizodaID > c.EpizodaID ) SELECT CrtacId, Konkatenacija, Duzina FROM ( SELECT CrtacId, Konkatenacija,Duzina, rank= RANK() OVER ( PARTITION BY CrtacId ORDER BY Duzina DESC ) FROM CTE ) D WHERE rank = 1 OPTION (Maxrecursion 0)
b) Rekurzija uz pomoć CTE gde se koristi poseban CTE za prvi a poseban za ostale nivoe rekurzije. Ovaj upit ima bolje performanse u odnosu na prethodni. Pošto je u pitanju rekurzija kao i u prethodnom primeru, potrebno je obratiti pažnju na iste napomene.
;WITH Ranked ( CrtacId, rnk, Naziv ) AS ( SELECT CrtacId, ROW_NUMBER() OVER( PARTITION BY CrtacId ORDER BY CrtacId ), CAST( Naziv AS NVARCHAR(MAX) ) FROM CrtaneEpizode) ,AnchorRanked ( CrtacId, rnk, Naziv ) AS ( SELECT CrtacId, rnk, Naziv FROM Ranked WHERE rnk = 1 ) ,RecurRanked ( CrtacId, rnk, Naziv ) AS ( SELECT CrtacId, rnk, Naziv FROM AnchorRanked UNION ALL SELECT Ranked.CrtacId, Ranked.rnk, RecurRanked.Naziv + ', ' + Ranked.Naziv FROM Ranked INNER JOIN RecurRanked ON Ranked.CrtacId = RecurRanked.CrtacId AND Ranked.rnk = RecurRanked.rnk + 1 ) SELECT CrtacId, MAX( Naziv ) FROM RecurRanked GROUP BY CrtacId OPTION (Maxrecursion 0)
c) Konkatenacija korišćenjem FOR XML metoda sa PATH modom koji nema ograničenja što se tiče broja nivoa, a i performanse su neuporedivo bolje u odnosu na rekurzivne upite. U nastavku su data dva načina:
--c1 SELECT p1.CrtacID, ( SELECT Naziv + ',' FROM CrtaneEpizode p2 WHERE p2.CrtacID = p1.CrtacID ORDER BY Naziv FOR XML PATH('') ) AS Konkatenacija FROM CrtaneEpizode p1 GROUP BY CrtacID --c2 SELECT DISTINCT CrtacID, left(Konkatenacija,len(konkatenacija)-1) FROM dbo.CrtaneEpizode p1 CROSS APPLY ( SELECT Naziv + ',' FROM CrtaneEpizode p2 WHERE p2.CrtacID = p1.CrtacID ORDER BY Naziv FOR XML PATH('') ) as D ( Konkatenacija )
d) Dinamički SQL koristeći ROW_NUMBER funkciju da bi se pivotirali podaci za konkatenaciju . Nedstatak ovog metoda je taj što upit koji je sklopljen, može biti veoma rogobatan zbog potencijalno velikog broja članova u select statement-u.
DECLARE @r NVARCHAR(MAX), @n INT, @i INT SELECT @i = 1, @r = 'SELECT CrtacId, ' + CHAR(13), @n = (SELECT TOP 1 COUNT( Naziv ) FROM CrtaneEpizode GROUP BY CrtacID ORDER BY COUNT( Naziv ) DESC ) ; WHILE @i <= @n BEGIN SET @r = @r + CASE WHEN @i = 1 THEN 'MAX( CASE Seq WHEN ' + CAST( @i AS NVARCHAR(MAX) ) + ' THEN Naziv ELSE SPACE(0) END ) + ' + CHAR(13) WHEN @i = @n THEN 'MAX( CASE Seq WHEN ' + CAST( @i AS NVARCHAR(MAX) ) + ' THEN '', '' + Naziv ELSE SPACE(0) END ) ' + CHAR(13) ELSE 'MAX( CASE Seq WHEN ' + CAST( @i AS NVARCHAR(MAX) ) + ' THEN '', '' + Naziv ELSE SPACE(0) END ) + ' + CHAR(13) END ; SET @i = @i + 1 ; END SET @r = @r + ' FROM ( SELECT CrtacId, Naziv, ROW_NUMBER() OVER ( PARTITION BY CrtacId ORDER BY Naziv ) FROM CrtaneEpizode p ) D ( CrtacId, Naziv, Seq ) GROUP BY CrtacId;' EXEC( @r );
e) Varijanta sa korišćenjem kursora postoji, ali zbog brzine i resursa koje koristi, treba je izbegavati
DECLARE @tbl TABLE (id INT PRIMARY KEY, list NVARCHAR(MAX)) SET NOCOUNT ON DECLARE @c INT, @p NVARCHAR(MAX), @cNext INT, @pNext NVARCHAR(MAX), @pNaziv NVARCHAR(MAX) DECLARE c CURSOR FOR SELECT CrtacId, EpizodaId, Naziv FROM CrtaneEpizode ORDER BY CrtacId, EpizodaId, Naziv ; OPEN c ; FETCH NEXT FROM c INTO @cNext, @pNext, @pNaziv ; SET @c = @cNext ; WHILE @@FETCH_STATUS = 0 BEGIN IF @cNext > @c BEGIN INSERT @tbl SELECT @c, @p ; SELECT @p = @PNaziv, @c = @cNext ; END ELSE SET @p = COALESCE(@p + ',', SPACE(0)) + @pNaziv ; FETCH NEXT FROM c INTO @cNext, @pNext, @pNaziv END INSERT @tbl SELECT @c, @p ; CLOSE c ; DEALLOCATE c ; SELECT * FROM @tbl ;
Preporuka je konkatenaciju vrednosti ili izraza iz različitih redova u aplikaciji, najbolje i najbrže raditi na klijentskoj strani, s obzirom na to da SQL ima određena ograničenja u ovom domenu. Ukoliko je neophodno raditi je u SQL-u, kursor je metod koji nikako ne treba koristiti, osim ukoiko je verzija sql server-a toliko niska, da ne podržava ni jednu drugu ponuđenu metodu. Rekurzija je dobar metod ukoliko je broj slogova (samim tim i broj nivoa) mali, sa povećanjem broja nivoa, treba je izbegavati. Trenutno najjednostavnija varijanta bez nedostataka je varijanta c), korišćenje FOR XML PATH.
Na primerima sa malim brojem slogova (do 1000) sve metode osim metode a) su pokazale sasvim zadovoljavajuće performanse. Na velikom broju slogova (oko milion), upiti koji su potpuno neprihvatljivi i koji će završiti nakon veoma dugog vremena izvršavanja (a možda i nikad) su a), b), c2 (FOR XML PATH sa CROSS APPLY varijantom) i d ). Najbrža je c1 varijanta sa XML PATH i ona je definitivno preporuka za korišćenje na svim veličinama baza.
[…] Konkatenacija string vrednosti iz razlicitih redova (1. deo) Konkatenacija string vrednosti iz razlicitih redova (2. deo) […]
[…] nekog vremena smo u dva članka (1, 2) opisali proces konkatenacije, odnosno spajanja vrednosti iz kolone u jedan string. Ovog puta […]