sqlsql-serverpivotunpivot

Can I use PIVOT or UNPIVOT to get the desired result and eliminate a slew of UNIONs?


I know I could easily use UNION ALL to get the results I want, but I have to generate 5 tables of each containing 15 rows of metrics/KPI.

To simplify, I have data like this:

user sex state race
usr1 M PA B
usr2 F TX W
usr3 M TX B
usr4 D NE H
usr5 F TX A

As a result, I need to format the data like:

Category Total Percentage
Total 5 100%
Male 2 40%
Female 2 40%
Other/Declined 1 20%
In State 3 60%
Out State 2 40%
Black 2 40%
White 1 20%

I know I could write a unioned query such as:

SELECT
     'Total' AS Category
     , COUNT(ud.user) AS Total
     , '100%' AS Percentage
FROM UserData ud
UNION ALL
SELECT
     'Male' AS Category
     , SUM(CASE WHEN ud.sex = 'M' THEN 1 END) AS Total
     , (SUM(CASE WHEN ud.sex = 'M' THEN 1 END) / COUNT(ud.user)) * 100 AS Percentage
FROM UserData ud
UNION ALL
     ...
FROM ...
UNION ...

If unions really are the only/best/most sensible way, so be it. I feel like there should be a way to do this with a relatively singular query rather than blocks of 15 unioned queries. Is there a way to do this with PIVOT/UNPIVOT or something else, or am I doomed to unions?


Solution

  • It seems the solution here is just a simple UNPIVOT and I was struggling to wrap my head around/conceptualize it. I think needing the percentage was making this seem more complicated than it needed to be.

    None of these answers really got me to the result I needed, however this is what I ended up using which generates the result table in my question perfectly. I'll wait a little bit to see if a better solution is posted before accepting this answer.

    WITH upvt AS (
        SELECT *
        FROM (
            SELECT
                COUNT(ud.userid) AS Total
                , SUM(CASE WHEN ud.sex = 'M' THEN 1 END) AS Male
                , SUM(CASE WHEN ud.sex = 'F' THEN 1 END) AS Female
                , SUM(CASE WHEN ud.sex = 'D' THEN 1 END) AS Declined
                , SUM(CASE WHEN ud.state = 'tx' THEN 1 END) AS [In State]
                , SUM(CASE WHEN ud.state <> 'tx' THEN 1 END) AS [Out State]
                , SUM(CASE WHEN ud.race = 'B' THEN 1 END) AS Black
                , SUM(CASE WHEN ud.race = 'W' THEN 1 END) AS White
                , SUM(CASE WHEN ud.race = 'H' THEN 1 END) AS Hispanic
                , SUM(CASE WHEN ud.race = 'A' THEN 1 END) AS Asian
            FROM UserData ud
        ) p
        UNPIVOT (
            Totals FOR Category IN (Total, Male, Female, Declined, [In State], [Out State], Black, White, Hispanic, Asian)
        ) AS up
    )
    SELECT 
        u.Category
        , u.Totals
        , (CAST(u.Totals AS FLOAT) / CAST(u2.Totals AS FLOAT)) * 100 AS Percentage
    FROM upvt u
    CROSS JOIN (
        SELECT * FROM upvt WHERE Category = 'Total'
    ) u2
    

    SQL Fiddle