Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SQL Server: collect values in an aggregation temporarily and re-use in the same query

How do I accumulate values in T-SQL? AFAIK there is no ARRAY type.
I want to re-use the values in the same query like demonstrated in this PostgreSQL example using array_agg().

SELECT a[1] || a[i] AS foo
      ,a[2] || a[5] AS bar  -- assuming we have >= 5 rows for simplicity
FROM   (
    SELECT array_agg(text_col ORDER BY text_col) AS a
          ,count(*)::int4 AS i
    FROM   tbl
    WHERE  id between 10 AND 100
    ) x

How would I best solve this with T-SQL?
Best I could come up with are two CTE and subselects:

;WITH x AS (
  SELECT row_number() OVER (ORDER BY name) AS rn
        ,name AS a
  FROM   #t
  WHERE  id between 10 AND 100
  ), i AS (
  SELECT count(*) AS i
  FROM   x
  )
SELECT (SELECT a FROM x WHERE rn = 1) + (SELECT a FROM x WHERE rn = i) AS foo
      ,(SELECT a FROM x WHERE rn = 2) + (SELECT a FROM x WHERE rn = 5) AS bar
FROM   i

Test setup:

CREATE TABLE #t(
 id   INT PRIMARY KEY
,name NVARCHAR(100))

INSERT INTO #t VALUES
 (3 , 'John')
,(5 , 'Mary')
,(8 , 'Michael')
,(13, 'Steve')
,(21, 'Jack')
,(34, 'Pete')
,(57, 'Ami')
,(88, 'Bob')

Is there a simpler way?

like image 735
Erwin Brandstetter Avatar asked Nov 13 '11 03:11

Erwin Brandstetter


People also ask

Can we use multiple aggregate functions in a single query?

The principle when combining two aggregate functions is to use the subquery for calculating the 'inner' statistic. Then the result is used in the aggregate functions of the outer query.

Can an aggregate function be applied on a single row?

Aggregate functions return a single result row based on groups of rows, rather than on single rows. Aggregate functions can appear in select lists and in ORDER BY and HAVING clauses.

Can you use an aggregate function with a join statement?

That's because we will dig further into aggregate functions by pairing them with JOINs. This duo unleashes the full possibilities of SQL aggregate functions and allows us to perform computations on multiple tables in a single query.

How do you filter aggregate data?

To filter records using the aggregate function, use the HAVING clause. Here we calculate the aggregate value: the average price of each product. One is sold by more than one grocer; therefore the average price is calculated for each (in our example, SELECT name, AVG(price) ).


2 Answers

Edit 1: I have added another solution that shows how to simulate ARRAY_AGG on SQL Server (the last answer).

Edit 2: For the solution number 4) I have added the third method for concatenation.

I'm not sure I have I understood correctly your question.

a) Instead of using arrays in SQL Server I would use table variables or XML.

b) To concatenate strings (in this case) I would use SELECT @var = @var + Name FROM tbl statements or XML xqueries.

c) The solution based on CTEs and multiple subqueries (WITH cte AS () FROM SELECT (SELECT * FROM cte.rn=1) + ()...) will generates a lot of scans and logical reads.

Solutions: 1) Table variable + SELECT @var = @var + Name FROM tbl:

--Creating the "array"
DECLARE @Array TABLE
(
    Idx     INT PRIMARY KEY,
    Val     NVARCHAR(100) NOT NULL
);

WITH Base
AS
(
    SELECT  Val = t.name, 
            Idx = ROW_NUMBER() OVER(ORDER BY t.name ASC)
    FROM    #t t
    WHERE  t.id between 10 AND 100
)
INSERT  @Array (Idx, Val)
SELECT  b.Idx, b.Val
FROM    Base b;

--Concatenating all names
DECLARE @AllNames NVARCHAR(4000);
--”Reset”/Init @AllNames
SET     @AllNames = '';
--String concatenation
SELECT  @AllNames = @AllNames + ',' + a.Val
FROM    @Array a;
--Remove first char (',')
SELECT  @AllNames = STUFF(@AllNames, 1, 1, '');
--The final result
SELECT  @AllNames [Concatenating all names - using a table variable];
/*
Concatenating all names - using a table variable
------------------------------------------------
Ami,Bob,Jack,Pete,Steve
*/

--Concatenating Idx=2 and Idx=5
--”Reset” @AllNames value
SET     @AllNames = '';
--String concatenation
SELECT  @AllNames = @AllNames + ',' + a.Val
FROM    @Array a
WHERE   a.Idx IN (2,5) --or a.Idx IN (2, (SELECT COUNT(*) FROM @Array))
ORDER BY a.Idx ASC;
--Remove first char (',')
SELECT  @AllNames = STUFF(@AllNames, 1, 1, '');
--The final result
SELECT  @AllNames [Concatenating Idx=2 and Idx=5 - using a table variable];
/*
Concatenating Idx=2 and Idx=5 - using a table variable
------------------------------------------------------
Bob,Steve
*/

2) Table variable + PIVOT:

--Concatenating a finite number of elements (names)
SELECT   pvt.[1] + ',' + pvt.[0]    AS [PIVOT Concat_1_and_i(0)]
        ,pvt.[2] + ',' + pvt.[5]    AS [PIVOT Concat_2_and_5]
        ,pvt.*
FROM    
(
        SELECT  a.Idx, a.Val
        FROM    @Array a
        WHERE   a.Idx IN (1,2,5)
        UNION ALL   
        SELECT  0, a.Val --The last element has Idx=0
        FROM    @Array a
        WHERE   a.Idx = (SELECT COUNT(*) FROM @Array)
) src
PIVOT   (MAX(src.Val) FOR src.Idx IN ([1], [2], [5], [0])) pvt;
/*
PIVOT Concat_1_and_i(0) PIVOT Concat_2_and_5
----------------------- --------------------
Ami,Steve               Bob,Steve           
*/

3) XML + XQuery:

SET ANSI_WARNINGS ON;
GO

DECLARE @x XML;
;WITH Base
AS
(
    SELECT  Val = t.name, 
            Idx = ROW_NUMBER() OVER(ORDER BY t.name ASC)
    FROM    #t t
    WHERE   t.id BETWEEN 10 AND 100
)
SELECT  @x = 
(
    SELECT   b.Idx  AS [@Idx]
            ,b.Val  AS [text()]
    FROM    Base b
    FOR XML PATH('Element'), ROOT('Array')
);
/* @x content
<Array>
  <Element Idx="1">Ami</Element>
  <Element Idx="2">Bob</Element>
  <Element Idx="3">Jack</Element>
  <Element Idx="4">Pete</Element>
  <Element Idx="5">Steve</Element>
</Array>
*/

--Concatenating all names (the result is XML, so a cast is needed)
DECLARE @r XML; --XML result
SELECT  @[email protected]('
(: $e = array element :)
for $e in (//Array/Element)
    return string($e)
');
SELECT  REPLACE(CONVERT(NVARCHAR(4000), @r), ' ', ',') AS [Concatenating all names - using XML];
/*
Concatenating all names - using XML
-----------------------------------
Ami,Bob,Jack,Pete,Steve
*/

--Concatenating Idx=1 and all names
SELECT  @[email protected]('
(: $e = array element :)
for $e in (//Array/Element[@Idx=1], //Array/Element)
    return string($e)
');
SELECT  REPLACE(CONVERT(NVARCHAR(4000), @r), ' ', ',') AS [Concatenating Idx=1 and all names - using XML];
/*
Concatenating Idx=1 and all names - using XML
---------------------------------------------
Ami,Ami,Bob,Jack,Pete,Steve
*/

--Concatenating Idx=1 and i(last name)
DECLARE @i INT;
SELECT  @[email protected]('
(: $e = array element :)
for $e in (//Array/Element[@Idx=1], //Array/Element[@Idx=count(//Array/Element)])
    return string($e)
');
SELECT  REPLACE(CONVERT(NVARCHAR(4000), @r), ' ', ',') AS [Concatenating Idx=1 and i(last name) - using XML];
/*
Concatenating Idx=1 and i(last name) - using XML
------------------------------------------------
Ami,Steve
*/


--Concatenating Idx=2 and Idx=5
SELECT  @[email protected]('
(: $e = array element :)
for $e in (//Array/Element[@Idx=2], //Array/Element[@Idx=5])
    return string($e)
');
SELECT  REPLACE(CONVERT(NVARCHAR(4000), @r), ' ', ',') AS [Concatenating Idx=2 and Idx=5 - using XML (method 1)];
/*
Concatenating Idx=2 and Idx=5 - using XML (method 1)
----------------------------------------------------
Bob,Steve
*/

--Concatenating Idx=2 and Idx=5
SELECT  @x.value('(//Array/Element)[@Idx=2][1]', 'NVARCHAR(100)')
        + ','
        + @x.value('(//Array/Element)[@Idx=5][1]', 'NVARCHAR(100)') AS [Concatenating Idx=2 and Idx=5 - using XML (method 2)];;
/*
Concatenating Idx=2 and Idx=5 - using XML (method 2)
----------------------------------------------------
Bob,Steve
*/

4) If the question is how to simulate ARRAY_AGG on SQL Server then, one answer might be: by using XML. Example:

SET ANSI_WARNINGS ON;
GO

DECLARE @Test TABLE
(
     Id         INT PRIMARY KEY
    ,GroupID    INT NOT NULL
    ,Name       NVARCHAR(100) NOT NULL
);

INSERT INTO @Test (Id, GroupID, Name)
VALUES
 (3 , 1, 'John')
,(5 , 1, 'Mary')
,(8 , 1, 'Michael')
,(13, 1, 'Steve')
,(21, 1, 'Jack')
,(34, 2, 'Pete')
,(57, 2, 'Ami')
,(88, 2, 'Bob');

WITH BaseQuery
AS
(
        SELECT  a.GroupID, a.Name
        FROM    @Test a
        WHERE   a.Id BETWEEN 10 AND 100 
)
SELECT  x.*
        , CONVERT(XML,x.SQLServer_Array_Agg).query
        ('
        for $e in (//Array/Element[@Idx=1], //Array/Element[@Idx=count(//Array/Element)])
            return string($e)
        ') AS [Concat Idx=1 and Idx=i (method 1)]
        , CONVERT(XML,x.SQLServer_Array_Agg).query('
            let $a :=  string((//Array/Element[@Idx=1])[1])
            let $b :=  string((//Array/Element[@Idx=count(//Array/Element)])[1])
            let $c :=  concat($a , "," , $b) (: " is used as a string delimiter :)
            return $c
        ') AS [Concat Idx=1 and Idx=i (method 2)]
        , CONVERT(XML,x.SQLServer_Array_Agg).query
        ('
        for $e in (//Array/Element[@Idx=(1,count(//Array/Element))])
            return string($e)
        ') AS [Concat Idx=1 and Idx=i (method 3)]
FROM
(
    SELECT  a.GroupID
        ,(SELECT ROW_NUMBER() OVER(ORDER BY b.Name) AS [@Idx]
                ,b.Name AS [text()]
        FROM    BaseQuery b
        WHERE   a.GroupID = b.GroupID 
        ORDER BY b.Name
        FOR XML PATH('Element'), ROOT('Array') ) AS SQLServer_Array_Agg
    FROM    BaseQuery a
    GROUP BY a.GroupID
) x;

Results:

GroupID SQLServer_Array_Agg                                                                                        Concat Idx=1 and Idx=i (method 1) Concat Idx=1 and Idx=i (method 2) Concat Idx=1 and Idx=i (method 3)
------- ---------------------------------------------------------------------------------------------------------- --------------------------------- --------------------------------- ---------------------------------
1       <Array><Element Idx="1">Jack</Element><Element Idx="2">Steve</Element></Array>                             Jack Steve                        Jack,Steve                        Jack Steve
2       <Array><Element Idx="1">Ami</Element><Element Idx="2">Bob</Element><Element Idx="3">Pete</Element></Array> Ami Pete                          Ami,Pete                          Ami Pete
like image 180
Bogdan Sahlean Avatar answered Oct 14 '22 06:10

Bogdan Sahlean


If you're just collecting some values to reuse, try a table variable rather than a temp table

DECLARE @t TABLE 
(
    id INT PRIMARY KEY,
    name NVARCHAR(100)
)

INSERT @t VALUES (3 , 'John')
-- etc

The table variable is in-memory only, instead of going in the tempdb database like a temp table does.

Check Should I use a #temp table or table variable for more information.

like image 25
Kirk Broadhurst Avatar answered Oct 14 '22 04:10

Kirk Broadhurst