Konkatenacija string vrednosti iz razlicitih redova (2. deo)

Objavljeno: 18-02-2014 | Autor: Tatjana Mađer | Kategorija: T-SQL

Ознаке:

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.

Komentari (2)

[…] 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 […]