Oracle pivot - one table field split into rows and columns - sql

I have an Oracle 12c table that looks something like this:
Name Class Type
--------------------------
Alice Math S
Alice Female A
Bob Anthropology S
Bob Male A
Charlie Science S
Charlie Tennis A
Charlie Male A
Do Math S
Do Female A
Do Tennis A
Elmer Male A
Elmer Science S
I would like to pivot the table to produce a report that looks like below. Classes of type "S" are rows, and type "A" are columns. And then count the number of people that fall into each category. For example, there are two people that are both Female and take class Math - Alice and Do.
Female Male Tennis Total
-----------------------------------------------
Math 2 0 1 3
Anthropology 0 1 0 1
Science 0 2 1 3
Total 2 3 2 7
I am familiar with pivot and cube in Oracle but I am stuck with getting the Class field split between rows and columns in the report.
I would really appreciate your help.

You can use conditional aggregation as following:
SQL> with dataa(name, class, type) as
2 (
3 select 'Alice', 'Math' ,'S' from dual union all
4 select 'Alice', 'Female' ,'A' from dual union all
5 select 'Bob', 'Anthropology' ,'S' from dual union all
6 select 'Bob', 'Male' ,'A' from dual union all
7 select 'Charlie', 'Science' ,'S' from dual union all
8 select 'Charlie', 'Tennis' ,'A' from dual union all
9 select 'Charlie', 'Male' ,'A' from dual union all
10 select 'Do', 'Math' ,'S' from dual union all
11 select 'Do', 'Female' ,'A' from dual union all
12 select 'Do', 'Tennis' ,'A' from dual union all
13 select 'Elmer', 'Male' ,'A' from dual union all
14 select 'Elmer', 'Science' ,'S' from dual
15 ),
16 -- your query starts from here
17 CTE AS
18 (
19 SELECT S.CLASS AS CLASS,
20 COALESCE(SUM(CASE WHEN A.CLASS = 'Female' THEN 1 END),0) AS Female,
21 COALESCE(SUM(CASE WHEN A.CLASS = 'Male' THEN 1 END),0) AS Male,
22 COALESCE(SUM(CASE WHEN A.CLASS = 'Tennis' THEN 1 END),0) AS Tennis,
23 COALESCE(SUM(CASE WHEN A.CLASS IN ('Female','Male','Tennis') THEN 1 END),0) AS TOTAL
24 FROM DATAA S JOIN DATAA A ON (A.name = S.name)
25 WHERE S.type = 'S' AND A.TYPE = 'A'
26 GROUP BY S.CLASS
27 )
28 SELECT CLASS, Female, Male, Tennis, TOTAL FROM CTE
29 UNION ALL
30 SELECT 'Total' AS CLASS, SUM(Female), SUM(Male), SUM(Tennis), SUM(TOTAL) FROM CTE;
CLASS FEMALE MALE TENNIS TOTAL
------------ ---------- ---------- ---------- ----------
Science 0 2 1 3
Anthropology 0 1 0 1
Math 2 0 1 3
Total 2 3 2 7
SQL>
Cheers!!

You have the destination "row" value and the destination "column" value in separate rows. In order to do a GROUP BY CUBE, you need to put them together in the same input row using a JOIN. So do a JOIN, then a GROUP BY CUBE, then a PIVOT. To keep things straight, use the GROUPING function to see when you are grouping "rows" and when you are grouping "columns". This becomes a lot of work, and you wind up getting NULLs in some places where you want zeroes. Here is the solution for curiosity's sake:
with data(name, class, type) as (
select 'Alice', 'Math' ,'S' from dual union all
select 'Alice', 'Female' ,'A' from dual union all
select 'Bob', 'Anthropology' ,'S' from dual union all
select 'Bob', 'Male' ,'A' from dual union all
select 'Charlie', 'Science' ,'S' from dual union all
select 'Charlie', 'Tennis' ,'A' from dual union all
select 'Charlie', 'Male' ,'A' from dual union all
select 'Do', 'Math' ,'S' from dual union all
select 'Do', 'Female' ,'A' from dual union all
select 'Do', 'Tennis' ,'A' from dual union all
select 'Elmer', 'Male' ,'A' from dual union all
select 'Elmer', 'Science' ,'S' from dual
)
, joined_data as (
select s.class row_class, a.class col_class
from data s
join data a
on s.type = 'S' and a.type = 'A' and s.name = a.name
)
, cubed_data as (
select case grouping(col_class) when 1 then 'Total' else col_class end col_class,
row_class, grouping(row_class) gr_row, count(*) cnt
from joined_data
group by cube(row_class, col_class)
)
select case gr_row when 1 then 'Total' else row_class end " ",
"Female", "Male", "Tennis", "Total"
from cubed_data
pivot(
sum(cnt) for col_class in (
'Female' as "Female",'Male' as "Male",'Tennis' as "Tennis",'Total' as "Total"
)
)
order by gr_row, row_class;
Female Male Tennis Total
------------ ---------- ---------- ---------- ----------
Anthropology 1 1
Math 2 1 3
Science 2 1 3
Total 2 3 2 7
Regards,
Stew

For this requirement, I prefer the solution by Tejash to the PIVOT demonstration in my other answer. However, I would suggest using ROLLUP instead of UNION ALL. Two lines are changed and the UNION ALL is left out.
with data(name, class, type) as (
select 'Alice', 'Math' ,'S' from dual union all
select 'Alice', 'Female' ,'A' from dual union all
select 'Bob', 'Anthropology' ,'S' from dual union all
select 'Bob', 'Male' ,'A' from dual union all
select 'Charlie', 'Science' ,'S' from dual union all
select 'Charlie', 'Tennis' ,'A' from dual union all
select 'Charlie', 'Male' ,'A' from dual union all
select 'Do', 'Math' ,'S' from dual union all
select 'Do', 'Female' ,'A' from dual union all
select 'Do', 'Tennis' ,'A' from dual union all
select 'Elmer', 'Male' ,'A' from dual union all
select 'Elmer', 'Science' ,'S' from dual
)
SELECT case grouping(S.CLASS) when 1 then 'Total' else s.class end AS CLASS,
COALESCE(SUM(CASE WHEN A.CLASS = 'Female' THEN 1 END),0) AS Female,
COALESCE(SUM(CASE WHEN A.CLASS = 'Male' THEN 1 END),0) AS Male,
COALESCE(SUM(CASE WHEN A.CLASS = 'Tennis' THEN 1 END),0) AS Tennis,
count(*) AS TOTAL
FROM DATA S JOIN DATA A ON (A.name = S.name)
WHERE S.type = 'S' AND A.TYPE = 'A'
GROUP BY rollup(S.CLASS);
CLASS FEMALE MALE TENNIS TOTAL
------------ ---------- ---------- ---------- ----------
Anthropology 0 1 0 1
Math 2 0 1 3
Science 0 2 1 3
Total 2 3 2 7
Regards,
Stew

Related

I want to count city column's values which are repeated

id city
1 London
2 Rome
3 London
4 Rome
Expected output like this:
London Rome
2 2
Using case expression...
How can I solve this query?
This is a standard aggregation + group by + having problem.
Sample data:
SQL> with test (id, city) as
2 (select 1, 'London' from dual union all
3 select 2, 'Rome' from dual union all
4 select 3, 'London' from dual union all
5 select 4, 'Rome' from dual union all
6 select 5, 'Zagreb' from dual
7 )
Query:
8 select city,
9 count(*) cnt
10 from test
11 group by city
12 having count(*) > 1;
CITY CNT
------ ----------
London 2
Rome 2
SQL>
What does case expression have to do with it?
You can get your output using conditional aggregation:
SELECT COUNT(CASE city WHEN 'London' THEN 1 END) AS London,
COUNT(CASE city WHEN 'Rome' THEN 1 END) AS Rome
FROM table_name
WHERE city IN ('London', 'Rome')
Which, for the sample data:
CREATE TABLE table_name (id, city) AS
SELECT 1, 'London' FROM DUAL UNION ALL
SELECT 2, 'Rome' FROM DUAL UNION ALL
SELECT 3, 'London' FROM DUAL UNION ALL
SELECT 4, 'Rome' FROM DUAL;
Outputs:
LONDON
ROME
2
2
However, in this case, it is unclear how to handle your requirement for repeated values if the values were not repeated.
fiddle

How to filter records based on another grouped by column in a same table

I have a sample table as below:
Target output:
I have tried with below script but only number of count produced as output:
select ID, count(COUNTRY) from test
group by COUNTRY
having count(COUNTRY)=3;
Could anyone here please help how can I get the targeted output? Thanks.
Here's one option - a subquery:
Sample data:
SQL> with test (id, name, country) as
2 (select 1, 'Mike' , 'Australia' from dual union all
3 select 2, 'Jason', 'Australia' from dual union all
4 select 3, 'Lee' , 'China' from dual union all
5 select 4, 'Simon', 'India' from dual union all
6 select 5, 'Alex' , 'Malaysia' from dual union all
7 select 6, 'John' , 'Australia' from dual
8 )
Query:
9 select id, country
10 from test
11 where country in (select country
12 from test
13 group by country
14 having count(*) = 3
15 )
16 order by id;
ID COUNTRY
---------- ---------
1 Australia
2 Australia
6 Australia
SQL>
You can use analytic functions and only scan the table once:
SELECT id, name, country
FROM (
SELECT id, name, country,
COUNT(*) OVER (PARTITION BY country) AS people_per_country
FROM table_name
)
WHERE people_per_country = 3;
Which, for the sample data:
CREATE TABLE table_name (id, name, country) AS
SELECT 1, 'Mike' , 'Australia' FROM DUAL UNION ALL
SELECT 2, 'Jason', 'Australia' FROM DUAL UNION ALL
SELECT 3, 'Lee' , 'China' FROM DUAL UNION ALL
SELECT 4, 'Simon', 'India' FROM DUAL UNION ALL
SELECT 5, 'Alex' , 'Malaysia' FROM DUAL UNION ALL
SELECT 6, 'John' , 'Australia' FROM DUAL;
Outputs:
ID
NAME
COUNTRY
1
Mike
Australia
2
Jason
Australia
6
John
Australia
db<>fiddle here

Transfer to data in right place in Oracle

I have a table like below. My aim is to count apple and mango under fruit.
I want to do this using case when statement. I run this query but it didn't work.
This works:
CASE
WHEN Food_type = 'vegetables'
AND Names IN ('apple', 'mango')
THEN 1
ELSE 0
END IN (0)
CASE
WHEN Names IN ('apple', 'orange', 'mango')
THEN 'fruit'
END Food_type2
Output:
Names
Food_type
apple
vegetables
eggplant
vegetables
carrot
vegetables
orange
fruit
onion
vegetables
spinach
vegetables
mango
vegetables
How can I do this ? Thank you.
This? Sample data till line #9; query begins at line #10.
SQL> with test (names, food_type) as
2 (select 'apple' , 'vegetables' from dual union all
3 select 'eggplant', 'vegetables' from dual union all
4 select 'carrot' , 'vegetables' from dual union all
5 select 'orange' , 'fruit' from dual union all
6 select 'onion' , 'vegetables' from dual union all
7 select 'spinach' , 'vegetables' from dual union all
8 select 'mango' , 'vegetables' from dual
9 )
10 select case when names in ('apple', 'mango') or food_type = 'fruit' then 'fruit'
11 else 'vegetables'
12 end food_type,
13 count(*)
14 from test
15 group by
16 case when names in ('apple', 'mango') or food_type = 'fruit' then 'fruit'
17 else 'vegetables'
18 end;
FOOD_TYPE COUNT(*)
---------- ----------
vegetables 4
fruit 3
SQL>

list the singer details who had not performed in any movie

Performance
PerformanceId SingerId MovieId NumberofSongs
1 1 1 2
2 3 1 4
3 2 2 6
4 4 5 3
5 5 5 3
6 2 6 2
7 4 6 5
8 6 4 6
9 6 3 3
10 4 3 4
Singer
SingerId SingerName City DOB Gender
1 A Hyderabad 14-Apr-65 M
2 B Chennai 25-May-84 M
3 C Bangalore 14-Sep-78 F
4 D Hyderabad 17-Jan-70 M
5 E Hyderabad 18-Mar-87 F
6 F Bangalore 23-Aug-75 F
7 g hyderabad 17-Jan-70 M
Movie
MovieId MovieName ReleaseDate
1 AAA 12-Jan-15
2 BBB 19-Sep-12
3 CCC 23-Jul-10
4 DDD 06-Oct-01
5 EEE 08-Nov-05
6 FFF 18-Apr-99
7 GGG 07-Aug-12
I have an issue please clarify me question.
List the Singer details who has not performed in any movies.
I tried this query its not working my query is
select count(a.singer_id) as count_movie,
a.singer_name,
a.city,
a.dob,
a.gender
from singer a
inner join performance b on
a.singer_id=b.singer_id group by
a.singer_name,a.city,a.dob,a.gender
having count(a.singer_id)=0;
Help me out
If I've understood your requirements correctly, I think you can achieve this with just a left outer join, like so:
with performance as (select 1 performanceid, 1 singerid, 1 movieid, 2 numberofsongs from dual union all
select 2 performanceid, 3 singerid, 1 movieid, 2 numberofsongs from dual union all
select 3 performanceid, 2 singerid, 2 movieid, 2 numberofsongs from dual union all
select 4 performanceid, 4 singerid, 5 movieid, 2 numberofsongs from dual union all
select 5 performanceid, 5 singerid, 5 movieid, 2 numberofsongs from dual union all
select 6 performanceid, 2 singerid, 6 movieid, 2 numberofsongs from dual union all
select 7 performanceid, 4 singerid, 6 movieid, 2 numberofsongs from dual union all
select 8 performanceid, 6 singerid, 4 movieid, 2 numberofsongs from dual union all
select 9 performanceid, 6 singerid, 3 movieid, 2 numberofsongs from dual union all
select 10 performanceid, 4 singerid, 3 movieid, 2 numberofsongs from dual union all
select 11 performanceid, 8 singerid, null movieid, 2 numberofsongs from dual),
singer as (select 1 singerid, 'A' singername, 'Hyderabad' city, to_date('14/04/1965', 'dd/mm/yyyy') dbo, 'M' gender from dual union all
select 2 singerid, 'B' singername, 'Chennai' city, to_date('25/05/1984', 'dd/mm/yyyy') dbo, 'M' gender from dual union all
select 3 singerid, 'C' singername, 'Bangalore' city, to_date('14/09/1978', 'dd/mm/yyyy') dbo, 'F' gender from dual union all
select 4 singerid, 'D' singername, 'Hyderabad' city, to_date('17/01/1970', 'dd/mm/yyyy') dbo, 'M' gender from dual union all
select 5 singerid, 'E' singername, 'Hyderabad' city, to_date('18/03/1987', 'dd/mm/yyyy') dbo, 'F' gender from dual union all
select 6 singerid, 'F' singername, 'Bangalore' city, to_date('23/08/1975', 'dd/mm/yyyy') dbo, 'F' gender from dual union all
select 7 singerid, 'g' singername, 'Hyderabad' city, to_date('17/01/1970', 'dd/mm/yyyy') dbo, 'M' gender from dual union all
select 8 singerid, 'H' singername, 'Chennai' city, to_date('23/02/1975', 'dd/mm/yyyy') dbo, 'M' gender from dual),
movie as (select 1 movieid, 'AAA' moviename, to_date('12/01/2015', 'dd/mm/yyyy') releasedate from dual union all
select 2 movieid, 'BBB' moviename, to_date('19/09/2012', 'dd/mm/yyyy') releasedate from dual union all
select 3 movieid, 'CCC' moviename, to_date('23/07/2010', 'dd/mm/yyyy') releasedate from dual union all
select 4 movieid, 'DDD' moviename, to_date('06/10/2001', 'dd/mm/yyyy') releasedate from dual union all
select 5 movieid, 'EEE' moviename, to_date('08/11/2005', 'dd/mm/yyyy') releasedate from dual union all
select 6 movieid, 'FFF' moviename, to_date('18/04/1999', 'dd/mm/yyyy') releasedate from dual union all
select 7 movieid, 'GGG' moviename, to_date('07/08/2012', 'dd/mm/yyyy') releasedate from dual)
select s.*
from singer s
left join performance p on (s.singerid = p.singerid)
where p.movieid is null;
SINGERID SINGERNAME CITY DBO GENDER
---------- ---------- --------- ---------- ------
8 H Chennai 23/02/1975 M
7 g Hyderabad 17/01/1970 M
This works whether or not the movieid in the performance table is null or not (assuming that a null value means a singer sang somewhere that wasn't a movie).
Get the number of songs less than 1 :
SELECT Performance.PerformanceId, Performance.singer_id, Performance.movie_id, Performance.nof_songs, Singer.singer_id AS Expr1, Singer.singer_name, Singer.City,
Singer.DOB, Singer.Gender, movies.movie_id AS Expr2, movies.movie_name, movies.release_date
FROM Performance INNER JOIN
Singer ON Performance.singer_id = Singer.singer_id INNER JOIN
movies ON Performance.movie_id = movies.movie_id
WHERE (Performance.nof_songs < 1)
To get only singer details :
SELECT Singer.singer_id AS Expr1, Singer.singer_name, Singer.City, Singer.DOB, Singer.Gender
FROM Performance INNER JOIN
Singer ON Performance.singer_id = Singer.singer_id INNER JOIN
movies ON Performance.movie_id = movies.movie_id
WHERE (Performance.nof_songs < 1)
If a singer not in performance table it has not performed in any of movie:
SELECT * FROM Singer
WHERE SingerId NOT IN (SELECT SingerId FROM Performance)
Try this
select *
from singer,
performance,
movie
where singer.singerId=performance.singer_id
and performance.movie_id <> movie.movie_id(+);
or
select *
from singer,
performance
where singer.singerId=performance.singer_id
and performance.movie_id NOT IN (select movie_id from movie);
It may help you

Is it possible to compare tuples in oracle-compatible sql?

I'm not 100% if tuples is the term for what I'm talking about but I'm looking at something like this:
Table grades
user grade
------------
Jim B
Bill C
Tim A
Jim B+
I know I can do:
SELECT COUNT(*)
FROM grades
WHERE (
(user = 'Jim' AND grade = 'B')
OR (user = 'Tim' AND grade = 'C')
);
But is there a way to do something more like this?
SELECT COUNT(*)
FROM grades
WHERE (user, grade) IN (('Jim','B'), ('Tim','C'));
EDIT: As a side note, I'd only tested with:
(user, grade) = ('Tim','C')
And that fails, so I assumed IN would fail as well, but I was wrong (thankfully!).
The query you posted should be valid syntax
SQL> ed
Wrote file afiedt.buf
1 with grades as (
2 select 'Jim' usr, 'B' grade from dual
3 union all
4 select 'Bill', 'C' from dual
5 union all
6 select 'Tim', 'A' from dual
7 union all
8 select 'Jim', 'B+' from dual
9 )
10 select *
11 from grades
12 where (usr,grade) in (('Jim','B'),
13 ('Tim','C'),
14* ('Tim','A'))
SQL> /
USR GR
---- --
Jim B
Tim A
You could use a subquery to treat a list of tuples like a table:
SELECT COUNT(*)
FROM grades
JOIN (
SELECT 'Jim' as user, 'B' as grade from dual
UNION ALL
SELECT 'Tim', 'C' from dual
UNION ALL
SELECT 'Pim', 'D' from dual
) as SearchTarget
ON SearchTarget.user = grades.user
and SearchTarget.grade = grades.grade