Crosstab query in PostgreSQL - sql

I would like the result of this query in a crosstab:
SELECT district, sex ,count(sex)
FROM table1
GROUP BY sex, district
ORDER BY district;
district | sex | count
---------+-----+-----
dis_1 | M | 2
dis_1 | F | 4
dis_1 | NI | 1
dis_2 | M | 5
dis_2 | F | 2
Like this:
district | M | F | NI
---------+---+---+---
dis_1 | 2 | 4 | 1
dis_2 | 5 | 2 | 0
I did some testing without success, as the query below:
SELECT row_name AS district,
category_1::varchar(10) AS m,
category_2::varchar(10) AS f,
category_3::varchar(10) AS ni,
category_4::int AS count
FROM crosstab('select district, sex, count(*)
from table1 group by district, sex')
AS ct (row_name varchar(27),
category_1 varchar(10),
category_2 varchar(10),
category_3 varchar(10),
category_4 int);

This crosstab function produces exactly what you asked for (except for simplified data types):
SELECT *
FROM crosstab('
SELECT district, sex, count(*)::int
FROM table1
GROUP BY 1,2
ORDER BY 1,2'
,$$VALUES ('M'), ('F'), ('NI')$$)
AS ct (district text
,"M" int
,"F" int
,"NI" int);
You had a couple of errors in your attempt.
Find details and explanation in this closely related answer:
PostgreSQL Crosstab Query

You can use an aggregate function with a CASE expression to get the result in columns:
select district,
sum(case when sex ='M' then 1 else 0 end) M,
sum(case when sex ='F' then 1 else 0 end) F,
sum(case when sex ='NI' then 1 else 0 end) NI
from table1
group by district
order by district

Related

Removing group of results if total is 0

I am using the following table to create a stacked bar chart - its quite a bit larger than this:
ID | Name | foodEaten | total
1 | Sam | Burger | 3
1 | Sam | Pizza | 1
1 | Sam | Kebab | 0
1 | Sam | Cheesecake| 3
1 | Sam | Sandwich | 5
2 | Jeff | Burger | 0
2 | Jeff | Pizza | 0
2 | Jeff | Kebab | 0
2 | Jeff | Cheesecake| 0
2 | Jeff | Sandwich | 0
I need to find a way to remove results like Jeff. Where the entire total for what he ate is 0. I can't think of the easiest way to achieve this. I've tried grouping the entire result by Id and creating a total, but its just not happening.
If the person has eaten a total of 0 food, then he needs to be excluded. But if he hasn't, and he hasn't eaten any kebabs, as shown in my above table, this needs to be included in the result!
So the output needed is:
ID | Name | foodEaten | total
1 | Sam | Burger | 3
1 | Sam | Pizza | 1
1 | Sam | Kebab | 0
1 | Sam | Cheesecake| 3
1 | Sam | Sandwich | 5
Assuming that you want the data as it appears, and not the aggregate out and then exclude:
WITH CTE AS (
SELECT ID,
[Name],
foodEaten,
total,
SUM(total) OVER (PARTITION BY [Name]) AS nameTotal
FROM YourTable)
SELECT ID,
[Name],
foodEaten,
total
FROM CTE
WHERE nameTotal > 0;
select id, name, foodEaten, sum(total) as total from <table> group by ID having sum(total) > 0
Does this work for you?
You can try below -
select id,name
from tablename a
group by id,name
having sum(total)>0
OR
DEMO
select * from tablename a
where not exists (select 1 from tablename b where a.id=b.id group by id,name
having sum(total)=0)
Try this
;WITH CTE (ID , Name , foodEaten , total)
AS
(
SELECT 1 , 'Sam' , 'Burger' , 3 UNION ALL
SELECT 1 , 'Sam' , 'Pizza' , 1 UNION ALL
SELECT 1 , 'Sam' , 'Kebab' , 2 UNION ALL
SELECT 1 , 'Sam' , 'Cheesecake', 3 UNION ALL
SELECT 1 , 'Sam' , 'Sandwich' , 5 UNION ALL
SELECT 2 , 'Jeff' , 'Burger' , 0 UNION ALL
SELECT 2 , 'Jeff' , 'Pizza' , 0 UNION ALL
SELECT 2 , 'Jeff' , 'Kebab' , 0 UNION ALL
SELECT 2 , 'Jeff' , 'Cheesecake', 0 UNION ALL
SELECT 2 , 'Jeff' , 'Sandwich' , 0
)
SELECT ID , Name ,SUM( total) AS Grandtotal
FROM CTE
GROUP BY ID , Name
HAVING SUM( total) >0
Result
ID Name Grandtotal
----------------------
1 Sam 14
Using DELETE with HAVING SUM(total) = 0 will remove the group of result which their total is 0
DELETE FROM TableName
WHERE ID IN (SELECT Id FROM TableName GROUP BY ID HAVING SUM(total) = 0)
or if you want to remvoe and select only the records which has sum of total is zero, then
SELECT * FROM TableName
WHERE ID NOT IN (SELECT Id FROM TableName GROUP BY ID HAVING SUM(total) = 0)
Assuming total is never negative, then probably the most efficient method is to use exists:
select t.*
from t
where exists (select 1
from t t2
where t2.name = t.name and
t2.total > 0
);
In particular, this can take advantage of an index on (name, total).

Category and Sub category in one column in sql

How we can show category and subcategory in one column in sql. Both columns are present in same table TableA.
Example: TableA
--------------------------------------
| category | subcategory | Values |
--------------------------------------
| Bird | parrot | 5 |
| Bird | pigeon | 10 |
| Animal | lion | 2 |
| Animal | Tiger | 5 |
--------------------------------------
Output table :
-------------------
| NEW | Value |
--------------------
| Bird | 15 |
| parrot | 5 |
| Piegon | 10 |
| Animal | 7 |
| lion | 2 |
| Tiger | 5 |
--------------------
In the output, New is a column where I want both category and sub category.
Sample Query to generate data:
CREATE TABLE #TEMP
(
catgory nvarchar(200),
sub_category nvarchar(200),
[values] nvarchar(200),
)
INSERT INTO #TEMP VALUES ('Bird','parrot',5)
INSERT INTO #TEMP VALUES ('Bird','pigeon',10)
INSERT INTO #TEMP VALUES ('Animal','lion',2)
INSERT INTO #TEMP VALUES ('Animal','Tiger',5)
Where the logic is:
I want category and sub category together, Where category should show the sum of all sub category values and should be in order as I have output table
select category, sum(values) values from table group by category
union
select subcategory, values from table
WITH cte AS
(
SELECT category,value FROM yourtable WHERE category IS NOT NULL
UNION ALL
SELECT subcategory, value FROM yourtable WHERE subcategory IS NOT NULL
)
SELECT Company as new, value FROM cte
You can use cte :
with cte as (
select t.*,
dense_rank() over (order by category) as seq,
sum([values]) over (partition by category) as sums
from table t
)
select t.cat as new, (case when cat_name = 'category' then sums else c.[values] end) as Value
from cte c cross apply
( values ('category', category), ('sub_category', sub_category) ) t(cat_name, cat)
order by seq, (case when cat_name = 'category' then 1 else 2 end);
Try this.
select new, [values] from (select catgory, sub_category as 'new', [values] from temp
union all
select catgory, catgory as 'new', sum([values]) from temp group by catgory) order by catgory
Output:
new values
-------------
parrot 5
pigeon 10
Bird 15
lion 2
lion 2
Tiger 5
Animal 9
Needs UNION ALL after getting the sums with the main table:
select
case t.sub_category
when '' then t.category
else sub_category
end new,
t.value
from (
select category, '' sub_category, sum([values]) value from temp group by category
union all
select category, sub_category, [values] value from temp
) t
order by t.category, t.sub_category
See the demo.
Results:
> new | value
> :----- | ----:
> Animal | 7
> lion | 2
> Tiger | 5
> Bird | 15
> parrot | 5
> pigeon | 10
A simple way to do this uses grouping sets:
select coalesce(subcategory, category) as new, sum(val)
from temp
group by grouping sets ( (category), (subcategory, category))
order by category, subcategory
Here is a db<>fiddle.
Table structure:
Id, name, parentcategoryId
1, animals, null
2, fish, 1
Example mssql:
Select name, subcats = STUFF((
SELECT ',' + NAME
FROM category as cat1 where cat1.parentcategoryId = cat. parentcategoryId
FOR XML PATH('')
), 1, 1, '') from category as cat where parentcategoryId = null
Preview:
Name, subcats
Animals, fish

SQL ORACLE: group by column, get counts from other column depending on different where clauses

I will explain by example:
Say I have a dataset like this:
id | city | age | gender
1 | London | Y | M
2 | Milan | Y | F
3 | London | O | M
4 | London | O | F
I want to have a row for each city, one for London and one for milan.
In each row I need to have a column for each of these:
the total number of entries for each city
the total number of young (age = Y) for each city
the total number of old (age = O) for each city
the total number of males (gender = M) for each city
the total number of female (gender = F) for each city.
The end result should be like this:
city | n_id n_Y n_O n_M n_F
---------------------------------------
London | 3 1 2 2 1
Milan | 1 1 0 0 1
Any help will be great.
Edit: so far I have
SELECT city, COUNT(id) FROM tablename GROUP BY city
Use conditional aggregation:
select city, count(*) as n_id,
sum(case when age = 'Y' then 1 else 0 end) as n_Y,
sum(case when age = 'O' then 1 else 0 end) as n_O,
sum(case when gender = 'M' then 1 else 0 end) as n_M,
sum(case when gender = 'F' then 1 else 0 end) as n_F
from t
group by city;

How to combine group by, where and logical operators?

I have the following data structure
| Name | Contract Type |
|:------|--------------:|
| Frank | Student |
| Frank | Staff |
| Frank | Staff |
| John | Internship |
| John | Student |
| John | Staff |
| Tim | Internship |
| Tim | Staff |
I want to get the Names of employees who have had a student contract and another contract in theire history. My approach was
Select Name, count(Name) from table
where ContractType = ('Student') AND (ContractType = ('Staff') OR ContractType = ('Internship') )
group by Name having count (Name) > 1
The problem here is that I still get all employees. Anyone who can help me get this accomplished?
Group by the Name and take only those groups having at least once the Student contract and in total more than 1 contract
Select Name
from your_table
group by Name
having sum(case when ContractType = 'Student' then 1 else 0 end) > 0
and count(distinct ContractType) > 1
Using GROUP BY:
SELECT Name
FROM dbo.YourTable
GROUP BY Name
HAVING SUM(CASE WHEN [Contract Type] = 'Student' THEN 1 ELSE 0 END) >= 1
AND SUM(CASE WHEN [Contract Type] <> 'Student' THEN 1 ELSE 0 END) >= 1;
But you can also use EXISTS:
SELECT DISTINCT A.Name
FROM dbo.YourTable A
WHERE EXISTS(SELECT 1 FROM dbo.YourTable
WHERE Name = A.Name
AND [Contract Type] = 'Student')
AND EXISTS( SELECT 1 FROM dbo.YourTable
WHERE Name = A.Name
AND [Contract Type] <> 'Student');
You can also use a GROUP BY and then IN:
SELECT name
FROM table
GROUP BY name
HAVING COUNT(*) > 1
AND name IN
(SELECT name
FROM table
WHERE contracttype = 'student')
Tested here: http://sqlfiddle.com/#!9/38fa36/1

Calculating percentage within a group

given a table that for the following commands:
select sex, count(*) from my_table group by sex;
select sex, employed, count(*) from my_table group by sex, employed;
gives:
sex | count
-------+------
male | 1960
female | 1801
and:
sex | employed | count
---------+----------+-------
male | f | 1523
male | t | 437
female | f | 1491
female | t | 310
I'm having a difficulty writing a query that will calculate percentage of employed within each sex group. So the output should look like this:
sex | employed | count | percent
---------+----------+--------+-----------
male | f | 1523 | 77.7% (1523/1960)
male | t | 437 | 22.3% (437/1960)
female | f | 1491 | 82.8% (1491/1801)
female | t | 310 | 17.2% (310/1801)
May be too late, but for upcoming searchers, possible solution could be:
select sex, employed, COUNT(*) / CAST( SUM(count(*)) over (partition by sex) as float)
from my_table
group by sex, employed
By IO Statistics this seems to be most effective solution - may be dependant on number of rows to be queried - tested on numbers above ...
The same attitude could be used for getting male / female percentage:
select sex, COUNT(*) / CAST( SUM(count(*)) over () as float)
from my_table
group by sex
Regards,
Jan
You can do it with a sub-select and a join:
SELECT t1.sex, employed, count(*) AS `count`, count(*) / t2.total AS percent
FROM my_table AS t1
JOIN (
SELECT sex, count(*) AS total
FROM my_table
GROUP BY sex
) AS t2
ON t1.sex = t2.sex
GROUP BY t1.sex, employed;
I can't think of other approaches off the top of my head.