Search code examples
sqlt-sqlpivottranspose

How do I transpose a result set and group by week?


I have a view based on query:

SELECT CONVERT(VARCHAR(10), date, 103) AS date,
       eventid, name, time, pts
FROM results
WHERE DATEPART(yy, date) = 2019;

This provides a data set such as this:

Date        EventID     Name            Time    Points
24/04/2019  10538       Fred Flintstone 22:27   10
24/04/2019  10538       Barney Rubble   22:50   9
24/04/2019  10538       Micky Mouse     23:17   8
24/04/2019  10538       Yogi Bear       23:54   7
24/04/2019  10538       Donald Duck     24:07   6
01/05/2019  10541       Barney Rubble   21:58   10
01/05/2019  10541       Fred Flintstone 22:00   9
01/05/2019  10541       Donald Duck     23:39   8
01/05/2019  10541       Yogi Bear       23:43   7
12/06/2019  10569       Fred Flintstone 22:06   10
12/06/2019  10569       Barney Rubble   22:22   9
12/06/2019  10569       Micky Mouse     23:05   8
12/06/2019  10569       Donald Duck     23:55   7

I need an output row for each name listing the pts per round and a total in the form:

Name            24/04/2019  01/05/2019  12/06/2019  total
Fred Flintstone     10           9          10       29
Barney Rubble        9          10           9       28
Yogi Bear            7           7           7       21
Micky Mouse          8                       8       16
Donald Duck          6           8                   14

There could be up to 16 non-consecutive event dates for the year.


Solution

  • Nothing wrong with PIVOT but, for me, the easiest and most performant way to do this would be to perform a Cross Tab. The syntax is less verbose, more portable, and easier to understand.

    First for some DDL and easily consumable sample data. <<< Learn how to do this it will get you better answers more quickly.

    SET NOCOUNT ON;
    SET DATEFORMAT dmy; -- I need this because I'm American
    
    -- DDL and easily consumable sample data
    DECLARE @Results TABLE 
    (
      [Date]  DATE,
      EventId INT,
      [Name]  VARCHAR(40), -- if indexed, go as narrow as possible
      [Time]  TIME,
      Points  INT,
      INDEX uq_poc_results CLUSTERED([Name],[EventId]) -- a covering index is vital for a query like this
    ); -- note: ^^^ this bad clustered index candidate, I went this route for simplicity
    
    INSERT @Results VALUES
      ('4/04/2019', 10538, 'Fred Flintstone', '22:27',10),
      ('24/04/2019',10538, 'Barney Rubble',   '22:50',9),
      ('24/04/2019',10538, 'Micky Mouse ',    '23:17',8),
      ('24/04/2019',10538, 'Yogi Bear',       '23:54',7),
      ('24/04/2019',10538, 'Donald Duck',     '2307',6),
      ('01/05/2019',10541, 'Barney Rubble',   '21:58',10),
      ('01/05/2019',10541, 'Fred Flintstone', '22:00',9),
      ('01/05/2019',10541, 'Donald Duck',     '23:39',8),
      ('01/05/2019',10541, 'Yogi Bear',       '23:43',7),
      ('12/06/2019',10569, 'Fred Flintstone', '22:06',10),
      ('12/06/2019',10569, 'Barney Rubble',   '22:22',9),
      ('12/06/2019',10569, 'Micky Mouse',     '23:05',8),
      ('12/06/2019',10569, 'Donald Duck',     '23:55',7);
    

    Note that I created a clustered index on (Name,EventId) - I would use a non-clustered index that covered the columns you need in the real world. If you have a lot of rows then you will want that index.

    Basic Cross-Tab

    SELECT [Name]       = r.[Name],
           [24/04/2019] = MAX(CASE r.[Date] WHEN '24/04/2019' THEN r.Points ELSE 0 END),
           [01/05/2019] = MAX(CASE r.[Date] WHEN '01/05/2019' THEN r.Points ELSE 0 END),
           [12/06/2019] = MAX(CASE r.[Date] WHEN '12/06/2019' THEN r.Points ELSE 0 END)
    FROM     @Results AS r
    GROUP BY r.[Name];
    

    Results:

    Name                 24/04/2019   01/05/2019   12/06/2019
    -------------------- ------------ ------------ ------------
    Barney Rubble        9            10           9
    Donald Duck          6            8            7
    Fred Flintstone      0            9            10
    Micky Mouse          8            0            8
    Yogi Bear            7            7            0
    

    To get the total we can wrap this in logic in a subquery and add the columns like this:

    SELECT
      [Name]       = piv.N,
      [24/04/2019] = piv.D1,
      [01/05/2019] = piv.D2,
      [12/06/2019] = piv.D3,
      Total        = piv.D1+piv.D2+piv.D3
    FROM
    (
      SELECT r.[Name],
             MAX(CASE r.[Date] WHEN '24/04/2019' THEN r.Points ELSE 0 END),
             MAX(CASE r.[Date] WHEN '01/05/2019' THEN r.Points ELSE 0 END),
             MAX(CASE r.[Date] WHEN '12/06/2019' THEN r.Points ELSE 0 END)
      FROM     @Results AS r
      GROUP BY r.[Name]
    ) AS piv(N,D1,D2,D3);
    

    Returns:

    Name                24/04/2019  01/05/2019  12/06/2019  Total
    ------------------- ----------- ----------- ----------- -------
    Barney Rubble       9           10          9           28
    Donald Duck         6           8           7           21
    Fred Flintstone     0           9           10          19
    Micky Mouse         8           0           8           16
    Yogi Bear           7           7           0           14
    

    Not only does this get you what you need with very little SQL, you benefit from pre-aggregation inside the subquery. A huge benefit of this approach over PIVOT is how you can do multiple aggregations in one query. Below are two examples of how to use this approach for multiple aggregations; this first using a standard GROUP BY twice, the other using window aggregate functions (.. OVER (partition by, order by..):

    --==== Traditional Approach
    SELECT
      [Name]       = piv.N,
      [24/04/2019] = MAX(piv.D1),
      [01/05/2019] = MAX(piv.D2),
      [12/06/2019] = MAX(piv.D3),
      Total        = MAX(f.Ttl),
      Avg1         = AVG(piv.D1), -- 1st date (24/04/2019)
      Avg2         = AVG(piv.D2), -- 2nd date...
      Avg3         = AVG(piv.D3), -- 3rd date...
      TotalAvg     = AVG(f.Ttl) ,
      Mn           = MIN(f.Ttl) ,
      Mx           = MAX(f.Ttl) 
    FROM
    (
      SELECT r.[Name],
             MAX(CASE r.[Date] WHEN '24/04/2019' THEN r.Points ELSE 0 END),
             MAX(CASE r.[Date] WHEN '01/05/2019' THEN r.Points ELSE 0 END),
             MAX(CASE r.[Date] WHEN '12/06/2019' THEN r.Points ELSE 0 END)
      FROM     @Results AS r
      GROUP BY r.[Name]
    ) AS piv(N,D1,D2,D3)
    CROSS APPLY (VALUES(piv.D1+piv.D2+piv.D3)) AS f(Ttl)
    GROUP BY    piv.N;
    
    --==== Leveraging Window Aggregates
    SELECT
      [Name]       = piv.N,
      [24/04/2019] = piv.D1,
      [01/05/2019] = piv.D2,
      [12/06/2019] = piv.D3,
      Total        = f.Ttl,
      Avg1         = AVG(piv.D1) OVER(PARTITION BY piv.N ORDER BY (SELECT NULL)), -- 1st date (24/04/2019)
      Avg2         = AVG(piv.D2) OVER(PARTITION BY piv.N ORDER BY (SELECT NULL)), -- 2nd date...
      Avg3         = AVG(piv.D3) OVER(PARTITION BY piv.N ORDER BY (SELECT NULL)), -- 3rd date...
      TotalAvg     = AVG(f.Ttl)  OVER(PARTITION BY piv.N ORDER BY (SELECT NULL)),
      Mn           = MIN(f.Ttl)  OVER(PARTITION BY piv.N ORDER BY (SELECT NULL)),
      Mx           = MAX(f.Ttl)  OVER(PARTITION BY piv.N ORDER BY (SELECT NULL))
    FROM
    (
      SELECT r.[Name],
             MAX(CASE r.[Date] WHEN '24/04/2019' THEN r.Points ELSE 0 END),
             MAX(CASE r.[Date] WHEN '01/05/2019' THEN r.Points ELSE 0 END),
             MAX(CASE r.[Date] WHEN '12/06/2019' THEN r.Points ELSE 0 END)
      FROM     @Results AS r
      GROUP BY r.[Name]
    ) AS piv(N,D1,D2,D3)
    CROSS APPLY (VALUES(piv.D1+piv.D2+piv.D3)) AS f(Ttl);
    

    Both Return:

    Name              24/04/2019  01/05/2019  12/06/2019  Total  Avg1   Avg2   Avg3   TotalAvg   Mn     Mx
    ----------------- ----------- ----------- ----------- ------ ------ ------ ------ ---------- ------ ------
    Barney Rubble     9           10          9           28     9      10     9      28         28     28
    Donald Duck       6           8           7           21     6      8      7      21         21     21
    Fred Flintstone   0           9           10          19     0      9      10     19         19     19
    Micky Mouse       8           0           8           16     8      0      8      16         16     16
    Yogi Bear         7           7           0           14     7      7      0      14         14     14
    

    To handle the columns dynamically you need to have a look at: Cross Tabs and Pivots, Part 2 - Dynamic Cross Tabs by Jeff Moden.