Search code examples
sqlsql-serversql-server-2019

Transpose rows to column without numbers to aggregate


I have login credentials along with the status and URL some have multiple userids and some have one only I want to group them by employee id and transpose the data. How can I do it in SQL Server 2019?

Database:

empid name  desc uid loc status url
100   Smith AA   U1  CA  O      www
100   Smith AA   U2  CA  C      www
100   Smith BB   U3  CA  C      www
100   Smith CC   U4  NY  C      www
101   Adams BB   U5  CA  C      www
101   Adams CC   U6  NY  C      www

Desired Results

emid name  AA loc sta url BB Loc sta url CC loc url
100  Smith U1 CA  O   www U3 CA  C   www U4  NY www
100  Smith U2 CA  C   www
101  Adams                U5 CA  C   www U6 NY www 

Solution

  • You could do a dynamic solution:

    SELECT  *
    INTO #data
    FROM    (
        VALUES  (100, N'Smith', N'AA', N'U1', N'CA', N'O', N'www')
        ,   (100, N'Smith', N'AA', N'U2', N'CA', N'C', N'www')
        ,   (100, N'Smith', N'BB', N'U3', N'CA', N'C', N'www')
        ,   (100, N'Smith', N'CC', N'U4', N'NY', N'C', N'www')
        ,   (101, N'Adams', N'BB', N'U5', N'CA', N'C', N'www')
        ,   (101, N'Adams', N'CC', N'U6', N'NY', N'C', N'www')
    ) t (empid,name,[desc],uid,loc,status,url)
    
    
    DECLARE @table NVARCHAR(667) = '#data'
    ,   @ids nvarchar(max) = '[empid], [name]'
    ,   @group_field sysname = '[desc]'
    
    DECLARE @fields TABLE (pv sysname, label sysname, sort int)
    
    INSERT INTO @fields
    VALUES  ('uid', '@@group@@', 1)
    ,   ('loc', 'loc', 2)
    ,   ('status', 'sta', 3)
    ,   ('url', 'url', 4)
    
    -- Build groups...
    CREATE TABLE #t_groups (grp nvarchar(max) COLLATE database_default)
    
    DECLARE @sql nvarchar(max) = N'
    insert into #t_groups
    select distinct ' + @group_field + '
    from ' + @table
    
    EXEC    sp_executesql @sql
    
    -- build the final sql
    SET @sql = '
        select ' + @ids + ', ' 
    
    SELECT  @sql = @sql + STRING_AGG(CONCAT('max(case when ' , @group_field,'= ',quotename(grp, ''''), ' then ', QUOTENAME(f.pv), ' end', ') as ', REPLACE(f.label, '@@group@@', QUOTENAME(t.grp))), ',') WITHIN GROUP(ORDER BY t.grp, f.sort)
    FROM    #t_groups t
    CROSS JOIN @fields f
    
    /*
    -- Older version without string_agg:
    
    SELECT  @sql = @sql + STUFF(
        (
        SELECT CONCAT(',max(case when ' , @group_field,'= ',quotename(grp, ''''), ' then ', QUOTENAME(f.pv), ' end', ') as ', REPLACE(f.label, '@@group@@', QUOTENAME(t.grp)))
        FROM    #t_groups t
        CROSS JOIN @fields f
        ORDER BY t.grp, f.sort
        FOR XML PATH(''), TYPE).value('.', 'nvarchar(max)'), 1, 1, '')
    */ 
    SET @sql = @sql + '
    from (
        select *
        , row_number() over(partition by ' + @ids + ',' + @group_field + 'order by @@spid) as sort
        from ' + @table + '
        ) t
    group by ' + @ids + ', sort'
    
    --SELECT    @sql
    
    EXEC    (@sql)
    

    I created some variables that holds the information you want to group by etc.

    @ids contains list of columns that should identify a unique row @group_field is the pivoting field that controls how the columns are pivoted

    @fields are the pivoted fields table which maps the fields to a label.

    For dynamic pivots one needs to get a list of groups, which i do by populating the #t_groups table. Then, the rest of the code builds the whole thing together and finally executes the dynamic string. You can use print to figure out the final string.

    Output is:

    empid name AA loc sta url BB loc sta url CC loc sta url
    100 Smith U1 CA O www U3 CA C www U4 NY C www
    100 Smith U2 CA C www NULL NULL NULL NULL NULL NULL NULL NULL
    101 Adams NULL NULL NULL NULL U5 CA C www U6 NY C www