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

后端 未结 3 501
终归单人心
终归单人心 2021-01-02 16:37

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

3条回答
  •  说谎
    说谎 (楼主)
    2021-01-02 17:35

    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
    
      Ami
      Bob
      Jack
      Pete
      Steve
    
    */
    
    --Concatenating all names (the result is XML, so a cast is needed)
    DECLARE @r XML; --XML result
    SELECT  @r=@x.query('
    (: $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  @r=@x.query('
    (: $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  @r=@x.query('
    (: $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  @r=@x.query('
    (: $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       JackSteve                             Jack Steve                        Jack,Steve                        Jack Steve
    2       AmiBobPete Ami Pete                          Ami,Pete                          Ami Pete
    

提交回复
热议问题