Pivot for the same gender - sql

How do i have multiple pivot. I would like to achieve the result as highlighted below.
For each Grade and each Gender, i would like to have the TotalA and Total B values aligned in 4 columns in a single row. My final result need to contain all 10 columns shown below.
My desired output [Need to contain 2 rows with GENDER column remained]:
I tried with below: But the script removed my Gender column and unable to pivot 2 columns (TotalA, TotalB) into 4 additional columns at the same time.
SELECT *,
[TotalA_Male] = [M],
[TotalB_Female] = [F]
FROM
(
SELECT * FROM table) AS s
PIVOT
(
MAX(TotalA) FOR [Gender] IN ([M],[F])
) AS p

I don't think you want a pivot at all. You are looking to find the partial sum of your total column, grouped by some key columns (it looks like Country and Grade in this case) . Window functions let you perform this partial sum. However, they won't filter by gender. You'll need to use a CASE expression inside the SUM() to only include male or female in your partial sums:
SELECT *,
SUM(CASE WHEN Gender = 'M' THEN TotalA ELSE 0 END) OVER(PARTITION BY Country, Grade) AS TotalA_Male,
SUM(CASE WHEN Gender = 'F' THEN TotalA ELSE 0 END) OVER(PARTITION BY Country, Grade) AS TotalA_Female,
SUM(CASE WHEN Gender = 'M' THEN TotalB ELSE 0 END) OVER(PARTITION BY Country, Grade) AS TotalB_Male,
SUM(CASE WHEN Gender = 'F' THEN TotalB ELSE 0 END) OVER(PARTITION BY Country, Grade) AS TotalB_Female
FROM totals
See also: https://msdn.microsoft.com/en-us/library/ms189461.aspx
Basically, the window functions let you do a GROUP BY as part of a single column expression in the SELECT list. The result of the aggregate and group by is included in every row, just as if it were any other expression. Note how there is no GROUP BY or PIVOT in the rest of the query. The PARTITION BY in the OVER() clause works like a GROUP BY, specifying how to group the rows in the resultset for the purposes of performing the specified aggregation (in this case, SUM()).

You can only pivot on a single column so what you need to to is unpivot those TotalA and TotalB columns into rows and then generate a single column based on gender and the total and use that in a pivot...
select * from (
select
grade,
/* combine the columns for a pivot */
total_gender_details = details + '_' + gender,
totals
from
(values
(1, 'F', cast(7.11321 as float), cast(15.55607 as float)),
(1, 'M', 6.31913, 15.50801),
(2, 'F', 5.26457, 6.94687),
(2, 'M', 6.34666, 9.29783)
) t(grade,gender,totalA,totalB)
/* unpivot the totals into rows */
unpivot (
totals
for details in ([totalA], [totalB])
) up
) t
pivot (
sum(totals)
for total_gender_details in ([totalA_M],[totalA_F],[totalB_M],[totalB_F])
) p

Related

Can I Select DISTINCT on 2 columns and Sum grouped by 1 column in one query?

Is it possible to write one query, where I would group by 2 columns in a table to get the count of total members plus get a sum of one column in that same table, but grouped by one column?
For example, the data looks like this
I want to get a count on distinct combinations of columns "OHID" and "MemID" and get the SUM of the "Amount" column grouped by OHID. The result is supposed to look like this
I was able to get the count correct using this query below
SELECT count(*) as TotCount
from (Select DISTINCT OHID, MemID
from #temp) AS TotMembers
However, when I try to use this query below to get all the results together, I am getting a count of 15 and a totally different total sum.
SELECT t.OHID,
count(TotMembers.MemID) as TotCount,
sum(t.Amount) as TotalAmount
from (Select DISTINCT OHID, MemID
from #temp) AS TotMembers
join #temp t on t.OHID = TotMembers .OHID
GROUP by t.OHID
If I understand correctly, you want to consider NULL as a valid value. The rest is just aggregation:
select t.ohid,
(count(distinct t.memid) +
(case when count(*) <> count(t.memid) then 1 else 0 end)
) as num_memid,
sum(t.amount) as total_amount
from #temp t
group by t.ohid,
The case logic might be a bit off-putting. It is just adding 1 if any values are NULL.
You might find this easier to follow with two levels of aggregation:
select t.ohid, count(*), sum(amount)
from (select t.ohid, t.memid, sum(t.amount) as amount
from #temp t
group by t.ohid, t.memid
) t
group by t.ohid

How to transform rows to columns using Postgres SQL?

I have the data in the following structure
Desired Output
Postgres (starting in 9.4) supports the filter syntax for conditional aggregation. So I would recommend:
SELECT customer_id,
MAX(value) FILTER (WHERE name = 'age') as age,
MAX(value) FILTER (WHERE name = 'marketing_consent') as marketing_consent,
MAX(value) FILTER (WHERE name = 'gender') as gender
FROM t
GROUP BY customer_id
SELECT customer_id,
MAX(CASE WHEN name='age' THEN value ELSE NULL END) AS age,
MAX(CASE WHEN name='marketing_consent' THEN value ELSE NULL END) AS marketing_consent,
MAX(CASE WHEN name='gender' THEN value ELSE NULL END) AS gender
FROM table
GROUP BY customer_id;
You just group by customer_id and pick out each value in its own column. You need an aggregate function on the various values columns for syntactical reasons, which is why each column has a Max function.
Not e that it would of course be better to store the data in a normalized fashion. Also, with newer versions of postgres, you can use filter instead of case, which I find a bit more readable.

Aggregate the long tail of a group by query into "others"

I have a table with one dimension and one metric:
name metric
A 4
A 9
B 27
C 9
D 6
I want to group by the dimension and then group the long tail of the results into an 'others' or 'the rest of the data' label.
For example my query should return all the names that the sum of their metrics are greater than 10 and group the rest into 'others':
name metric
A 13
others 15
B 27
I can get this result by aggregating twice:
with T as (
select
name
, (case when sum(metric) > 10 then name else 'others' end) as group_name
, sum(metric) as metric
from MyData
group by name
)
select
group_name as name
, sum(metric) as metric
from T
group by group_name
order by metric
Can I do this in a single operation without using sub queries?
SQL Snippet
I'm pretty certain this requires two levels of aggregation, because the original data doesn't have the information for grouping the names. You need one aggregation to classify the names and one to calculate the final results.
That said, I would write this as:
select (case when sum_metric > 10 then name else 'others' end) as group_name,
sum(sum_metric) as metric
from (select name, sum(metric) as sum_metric
from mydata
group by name
) t
group by group_name;
That said, you could use select distinct and window function for something inscrutable such as:
select distinct (case when sum(metric) > 10 then name else 'others' end),
sum(sum(metric)) over (partition by (case when sum(metric) > 10 then name else 'others' end)) as metric
from mydata
group by name;
However, select distinct is really doing another aggregation. So this eliminates the subquery but not the work.

Convert table into grouped statistics of same table

In MS-SQL, I have a table hasStudied(sid, ccode, grade) (student id, course code, grade) which keeps track of the past courses a student has studied and the grade they've gotten.
As output of my query, I want to return a list of courses, with the percentage of passing (= not 'F') students in the column next to it, in descending order by that percentage, like this:
C1 : 85
C3 : 70
C2 : 67
etc.
I have currently managed to break them into two separate tables, one containing coursecode and the number of people passing the course, one containing coursecode and the number of people who have read the course.
This is done by two relatively simple statements, but requires me to do a lot of inefficient calculating in java.
Is there any way to make this in a single query?
Assuming you do not have two entries with the same student under one course, this should do it:
SELECT
ccode,
ROUND((passed::numeric(15,2) / taken_course::numeric(15,2)) * 100, 0) AS percentage_passed
FROM(
SELECT
ccode,
sum(CASE WHEN grade > 2 THEN 1 ELSE 0 END) AS passed,
count(1) AS taken_course
FROM
hasStudied
GROUP BY ccode
) foo
ORDER BY ccode
-- since you want to order DESC by values, instead do
-- ORDER BY percentage_passed
I think you are looking for the usage of cte:
create table #temp(StId int, ccode varchar(5), grade varchar(1))
insert into #temp Values (1,'A1','A'),(1,'A1','F'),(2,'A2','B'),(3,'A2','F'),(4,'A2','F'),(4,'A3','F'),(5,'A3','F')
;with cte as (
select ccode
from #temp
group by ccode
)
select cte.ccode,ratioOfPass = cast(sum(case when t.grade <> 'F' then 1.0 else 0.0 end) as float) / count(*)
from cte
inner join #temp t on t.ccode = cte.ccode
group by cte.ccode
While calculating, use sum with case-when and do not forget to cast the value of sum to float.

Avg Sql Query Always Returns int

I have one column for Farmer Names and one column for Town Names in my table TRY.
I want to find Average_Number_Of_Farmers_In_Each_Town.
Select TownName ,AVG(num)
FROM(Select TownName,Count(*) as num From try Group by TownName) a
group by TownName;
But this query always returns int values. How can i get values in float too?
;WITH [TRY]([Farmer Name], [Town Name])
AS
(
SELECT N'Johny', N'Bucharest' UNION ALL
SELECT N'Miky', N'Bucharest' UNION ALL
SELECT N'Kinky', N'Ploiesti'
)
SELECT AVG(src.Cnt) AS Average
FROM
(
SELECT COUNT(*)*1.00 AS Cnt
FROM [TRY]
GROUP BY [TRY].[Town Name]
) src
Results:
Average
--------
1.500000
Without ... *1.00 the result will be (!) 1 (AVG(INT 2 , INT 1) -truncated-> INT 1, see section Return types).
Your query is always returning int logically because the average is not doing anything. Both the inner and the outer queries are grouping by town name -- so there is one value for each average, and that average is the count.
If you are looking for the overall average, then something like:
Select AVG(cast(cnt as float))
FROM (Select TownName, Count(*) as cnt
From try
Group by TownName
) t
You can also do this without the subquery as:
select cast(count(*) as float) /count(distinct TownName)
from try;
EDIT:
The assumption was that each farmer in the town has one row in try. Are you just trying to count the number of distinct farmers in each town? Assuming you have a field like FarmerName that identifies a given farmer, that would be:
select TownName, count(distinct FarmerName)
from try
group by TownName;