Writing Nested SQL queries - sql

I have 3 tables as StudentData which includes data of students, Subjects table which has data of all subjects offered and Marks which has the marks obtained by students for each subject.Marks table maps to StudentData table by StudentId and Subjects table by SubjectId
What I want to do is select the maximum marks for each subject and the name of the student like follows
So I wrote a Oracle PL/SQL Query as follows,
select MAX(marks)
from
(select Marks ,subjects.name as SJN ,studentdata.name
from (studentdata inner Join marks On studentdata.studentid = marks.studentid)
Inner Join subjects On subjects.subjectid = marks.subjectid)
where SJN in (select name from subjects);
But it gives only one result.Please help me to develop a query to get my expected result set.

Something like this? Lines #1 - 26 represent sample data (you don't type that); query you need begins at line #28.
SQL> with
2 -- sample data
3 studentdata (studentid, name, course) as
4 (select 1, 'olivier', 'it' from dual union all
5 select 2, 'noah', 'business' from dual union all
6 select 3, 'jack', 'business' from dual union all
7 select 4, 'mason', 'it' from dual union all
8 select 5, 'julion', 'it' from dual),
9 subjects (subjectid, name) as
10 (select 1, 'java' from dual union all
11 select 2, 'business stg' from dual union all
12 select 3, 'python' from dual union all
13 select 4, 'statistics' from dual union all
14 select 5, 'mgt accounting' from dual union all
15 select 7, 'social studies' from dual union all
16 select 8, 'ess english' from dual),
17 marks (id, studentid, subjectid, marks) as
18 (select 1, 1, 1, 56 from dual union all
19 select 2, 1, 2, 78 from dual union all
20 select 3, 1, 7, 83 from dual union all
21 select 4, 1, 3, 45 from dual union all
22 select 5, 1, 5, 63 from dual union all
23 --
24 select 6, 2, 1, 99 from dual union all
25 select 7, 3, 2, 10 from dual union all
26 select 8, 4, 7, 83 from dual)
27 --
28 select b.name subject, s.name student, m.marks
29 from marks m join subjects b on b.subjectid = m.subjectid
30 join studentdata s on s.studentid = m.studentid
31 where m.marks = (select max(m1.marks)
32 from marks m1
33 where m1.subjectid = m.subjectid
34 )
35 order by b.name, s.name;
SUBJECT STUDENT MARKS
-------------- ------- ----------
business stg olivier 78
java noah 99
mgt accounting olivier 63
python olivier 45
social studies mason 83
social studies olivier 83
6 rows selected.
SQL>

You can use the analytical function ROW_NUMBER as follows:
SELECT SJN, MARKS, STUNAME FROM
(SELECT
MARKS.MARKS,
SUBJECTS.NAME AS SJN,
STUDENTDATA.NAME AS STUNAME,
ROW_NUMBER() OVER (PARTITION BY SUBJECTS.SUBJECTID
ORDER BY MARKS.MARKS DESC NULLS LAST) AS RN
FROM
STUDENTDATA
INNER JOIN MARKS ON STUDENTDATA.STUDENTID = MARKS.STUDENTID
INNER JOIN SUBJECTS ON SUBJECTS.SUBJECTID = MARKS.SUBJECTID)
WHERE RN = 1;

The first thing that comes to mind: select the best mark per subject, then select the student(s) with that mark in that subject:
select s.name as subject, m.marks, sd.name as studentname
from marks m
join studentdata sd on sd.studentid = m.studentid
join subjects s on s.subjectid = m.subjectid
where (m.subjectid, m.marks) in
(
select subjectid, max(marks)
from marks
group by subjetid
);
As you see, we select twice from marks. This can be avoided with a window function:
select s.name as subject, m.marks, sd.name as studentname
from
(
select
subjectid,
marks,
max(marks) over (partition by subjectid) as max_marks
from marks
) m
join studentdata sd on sd.studentid = m.studentid
join subjects s on s.subjectid = m.subjectid
where m.marks = m.max_marks;
Another option is to join and to check that no better marks exist for the subject:
select s.name as subject, m.marks, sd.name as studentname
from marks m
join studentdata sd on sd.studentid = m.studentid
join subjects s on s.subjectid = m.subjectid
where not exists
(
select null
from marks m2
where m2.subjectid = m.subjectid
and m2.marks > m.marks
);
Which of these options is best, I cannot tell. Decide for the one that you find most readable. Regardless of which query you choose, this index should help the DBMS finding the highest marks quickly:
create index idx on marks(subjectid, marks);

Related

Printing data from sub-queries SQL

I have 2 tables: SalesPeople and Customers that have snum and cnum as primary key respectively; both tables have city column as well.
Without using joins, we have to tell the names of customers and salespeople that belong to same city.
I have used nested queries to print the salespeople that belong to the city of customers, but cant figure out how to print customer names with this .
SELECT S.*
FROM SalesPeople S
WHERE City IN(
SELECT City
FROM Customers CX
);
How about this? (Disregard the fact that the WITH factoring clause doesn't exist in Oracle 9i (at least, I think so); you already have those tables).
Sample data:
SQL> with
2 salespeople (snum, city) as
3 (select 1, 'London' from dual union all
4 select 2, 'Paris' from dual union all
5 select 3, 'Rome' from dual
6 ),
7 customers (cnum, city) as
8 (select 100, 'Zagreb' from dual union all
9 select 101, 'Rome' from dual union all
10 select 102, 'Rome' from dual union all
11 select 103, 'Paris' from dual
12 )
Query:
13 select person_num
14 from (select snum as person_num, city from salespeople
15 union
16 select cnum, city from customers
17 )
18 where city = 'Rome';
PERSON_NUM
----------
3
101
102
SQL>

How to get the max count of an attribute with 3 tables?

I need to query which author sold the most books and how many books the author sold.
select a.firstname ||''|| a.lastname as fullname,
max(count(datesold))
from author a,
transaction t,
book b
where a.authorid = b.authorid
and b.bookid = t.bookid
group by
a.firstname,
a.lastname;
It gave me an error of not a single-group group function.
Any idea what is the issue here?
With some sample data
SQL> with
2 author (authorid, firstname, lastname) as
3 (select 1, 'Stephen', 'King' from dual union all
4 select 2, 'Jo' , 'Nesbo' from dual),
5 book (bookid, authorid) as
6 (select 100, 1 from dual union all
7 select 200, 1 from dual union all
8 select 300, 2 from dual
9 ),
10 transaction (trans_id, bookid) as
11 (select 1, 100 from dual union all
12 select 2, 100 from dual union all
13 select 3, 100 from dual union all
14 select 4, 300 from dual
15 ),
query uses the RANK analytic function which ranks rows by number of rows in the transaction table (it says how many books were sold). Finally, fetch row(s) that rank as highest:
16 temp as
17 (select a.firstname || ' ' || a.lastname AS fullname,
18 count(t.bookid) cnt,
19 rank() over (order by count(t.bookid) desc) rnk
20 from author a join book b on a.authorid = b.authorid
21 join transaction t on t.bookid = b.bookid
22 group by a.firstname, a.lastname
23 )
24 select fullname, cnt
25 from temp
26 where rnk = 1;
FULLNAME CNT
------------- ----------
Stephen King 3
SQL>
You can use:
select MAX(a.firstname ||' '|| a.lastname) as fullname,
COUNT(datesold)
from author a
INNER JOIN book b
ON (a.authorid = b.authorid)
INNER JOIN transaction t
ON (b.bookid = t.bookid)
GROUP BY
a.authorid
ORDER BY
COUNT(datesold) DESC
FETCH FIRST ROW ONLY;
Do not aggregate by firstname and lastname as there are many people in the world with identical names and you do not want to count everyone with the same name as a single person.
Which, for the sample data:
CREATE TABLE author (authorid, firstname, lastname, dateofbirth) AS
SELECT 1, 'Alice', 'Adams', DATE '1900-01-01' FROM DUAL UNION ALL
SELECT 2, 'Alice', 'Adams', DATE '1910-01-01' FROM DUAL UNION ALL
SELECT 3, 'Betty', 'Baron', DATE '1920-01-01' FROM DUAL UNION ALL
SELECT 4, 'Carol', 'Corrs', DATE '1930-01-01' FROM DUAL UNION ALL
SELECT 5, 'Carol', 'Corrs', DATE '1940-01-01' FROM DUAL;
CREATE TABLE book (bookid, authorid) AS
SELECT 1, 1 FROM DUAL UNION ALL
SELECT 2, 2 FROM DUAL UNION ALL
SELECT 3, 3 FROM DUAL UNION ALL
SELECT 4, 4 FROM DUAL UNION ALL
SELECT 5, 5 FROM DUAL;
CREATE TABLE transaction (bookid, datesold) AS
SELECT 1, DATE '1970-01-01' FROM DUAL UNION ALL
SELECT 1, DATE '1970-01-02' FROM DUAL UNION ALL
SELECT 1, DATE '1970-01-03' FROM DUAL UNION ALL
SELECT 1, DATE '1970-01-04' FROM DUAL UNION ALL
SELECT 3, DATE '1970-01-01' FROM DUAL UNION ALL
SELECT 4, DATE '1970-01-01' FROM DUAL UNION ALL
SELECT 4, DATE '1970-01-02' FROM DUAL UNION ALL
SELECT 5, DATE '1970-01-01' FROM DUAL UNION ALL
SELECT 5, DATE '1970-01-02' FROM DUAL UNION ALL
SELECT 5, DATE '1970-01-03' FROM DUAL;
Outputs:
FULLNAME
COUNT(DATESOLD)
Alice Adams
4
db<>fiddle here

Oracle Finding a string match from multiple database tables

This is somewhat a complex problem to describe, but I'll try to explain it with an example. I thought I would have been able to use the Oracle Instr function to accomplish this, but it does not accept queries as parameters.
Here is a simplification of my data:
Table1
Person Qualities
Joe 5,6,7,8,9
Mary 7,8,10,15,20
Bob 7,8,9,10,11,12
Table2
Id Desc
5 Nice
6 Tall
7 Short
Table3
Id Desc
8 Angry
9 Sad
10 Fun
Table4
Id Desc
11 Boring
12 Happy
15 Cool
20 Mad
Here is somewhat of a query to give an idea of what I'm trying to accomplish:
select * from table1
where instr (Qualities, select Id from table2, 1,1) <> 0
and instr (Qualities, select Id from table3, 1,1) <> 0
and instr (Qualities, select Id from table3, 1,1) <> 0
I'm trying to figure out which people have at least 1 quality from each of the 3 groups of qualities (tables 2,3, and 4)
So Joe would not be returned in the results because he does not have the quality from each of the 3 groups, but Mary and Joe would since they have at least 1 quality from each group.
We are running Oracle 12, thanks!
Here's one option:
SQL> with
2 table1 (person, qualities) as
3 (select 'Joe', '5,6,7,8,9' from dual union all
4 select 'Mary', '7,8,10,15,20' from dual union all
5 select 'Bob', '7,8,9,10,11,12' from dual
6 ),
7 table2 (id, descr) as
8 (select 5, 'Nice' from dual union all
9 select 6, 'Tall' from dual union all
10 select 7, 'Short' from dual
11 ),
12 table3 (id, descr) as
13 (select 8, 'Angry' from dual union all
14 select 9, 'Sad' from dual union all
15 select 10, 'Fun' from dual
16 ),
17 table4 (id, descr) as
18 (select 11, 'Boring' from dual union all
19 select 12, 'Happy' from dual union all
20 select 15, 'Cool' from dual union all
21 select 20, 'Mad' from dual
22 ),
23 t1new (person, id) as
24 (select person, regexp_substr(qualities, '[^,]+', 1, column_value) id
25 from table1 cross join table(cast(multiset(select level from dual
26 connect by level <= regexp_count(qualities, ',') + 1
27 ) as sys.odcinumberlist))
28 )
29 select a.person,
30 count(b.id) bid,
31 count(c.id) cid,
32 count(d.id) did
33 from t1new a left join table2 b on a.id = b.id
34 left join table3 c on a.id = c.id
35 left join table4 d on a.id = d.id
36 group by a.person
37 having ( count(b.id) > 0
38 and count(c.id) > 0
39 and count(d.id) > 0
40 );
PERS BID CID DID
---- ---------- ---------- ----------
Bob 1 3 2
Mary 1 2 2
SQL>
What does it do?
lines #1 - 22 represent your sample data
T1NEW CTE (lines #23 - 28) splits comma-separated qualities into rows, per every person
final select (lines #29 - 40) are outer joining t1new with each of "description" tables (table2/3/4) and counting how many qualities are contained in there for each of person's qualities (represented by rows from t1new)
having clause is here to return only desired persons; each of those counts have to be a positive number
Maybe this will help:
{1} Create a view that categorises all qualities and allows you to SELECT quality IDs and categories . {2} JOIN the view to TABLE1 and use a join condition that "splits" the CSV value stored in TABLE1.
{1} View
create or replace view allqualities
as
select 1 as category, id as qid, descr from table2
union
select 2, id, descr from table3
union
select 3, id, descr from table4
;
select * from allqualities order by category, qid ;
CATEGORY QID DESCR
---------- ---------- ------
1 5 Nice
1 6 Tall
1 7 Short
2 8 Angry
2 9 Sad
2 10 Fun
3 11 Boring
3 12 Happy
3 15 Cool
3 20 Mad
{2} Query
-- JOIN CONDITION:
-- {1} add a comma at the start and at the end of T1.qualities
-- {2} remove all blanks (spaces) from T1.qualities
-- {3} use LIKE and the qid (of allqualities), wrapped in commas
--
-- inline view: use UNIQUE, otherwise we may get counts > 3
--
select person
from (
select unique person, category
from table1 T1
join allqualities A
on ',' || replace( T1.qualities, ' ', '' ) || ',' like '%,' || A.qid || ',%'
)
group by person
having count(*) = ( select count( distinct category ) from allqualities )
;
-- result
PERSON
Bob
Mary
Tested w/ Oracle 18c and 11g. DBfiddle here.

Get column values separated by semi colon

I have two tables in plsql
tblStudent:-
StudentId Name .........
1 A
2 B
tblDept:-
DeptId DeptName StudentId
1 Dep Aero 1
2 IT 1
3 Dep Maths 1
4 Dep Chemistry 2
I want to get studentId, with all its departments which starts with 'Dep' separated by semi colon, If i pass where StudentId = 1 in SELECT result should look like
StudentId DeptName
1 Dep Aero;Dep Maths
any help please?
You may use LISTAGG to concat and LIKE to filter the records.
SELECT studentid,
LISTAGG(deptname,';') WITHIN GROUP(
ORDER BY deptid
) as deptname
FROM t
WHERE deptname LIKE 'Dep%'
GROUP BY studentid;
If you want an empty list of departments when a student has no entries then you can use an outer join between the two tables, e.g.:
select s.studentid,
listagg(d.deptname, ';') within group (order by d.deptname) as deptnames
from tblstudent s
left join tbldept d on d.studentid = s.studentid
and deptname like 'Dep%'
group by s.studentid;
Demo using CTEs for your sample data, including a third student ID with no matching departments:
-- CTEs for sample data
with tblstudent (studentid, name) as (
select 1, 'A' from dual
union all select 2, 'B' from dual
union all select 3, 'C' from dual
),
tbldept (deptid, deptname, studentid) as (
select 1, 'Dep Aero', 1 from dual
union all select 2, 'IT', 1 from dual
union all select 3, 'Dep Maths', 1 from dual
union all select 4, 'Dep Chemistry', 2 from dual
)
-- actual query
select s.studentid,
listagg(d.deptname, ';') within group (order by d.deptname) as deptnames
from tblstudent s
left join tbldept d on d.studentid = s.studentid
and deptname like 'Dep%'
group by s.studentid;
STUDENTID DEPTNAMES
---------- ------------------------------
1 Dep Aero;Dep Maths
2 Dep Chemistry
3
Your data model looks odd though; you should probably have a department table which only has the department IDs and names, and then another tables that links each student to all of their departments - something like (in CTE form again):
-- CTEs for sample data
with tblstudent (studentid, name) as (
select 1, 'A' from dual
union all select 2, 'B' from dual
union all select 3, 'C' from dual
),
tbldept (deptid, deptname) as (
select 1, 'Dep Aero' from dual
union all select 2, 'IT' from dual
union all select 3, 'Dep Maths' from dual
union all select 4, 'Dep Chemistry' from dual
),
tblstudentdept (studentid, deptid) as (
select 1, 1 from dual
union all select 1, 2 from dual
union all select 1, 3 from dual
union all select 2, 4 from dual
)
-- actual query
select s.studentid,
listagg(d.deptname, ';') within group (order by d.deptname) as deptnames
from tblstudent s
left join tblstudentdept sd on sd.studentid = s.studentid
left join tbldept d on d.deptid = sd.deptid
and deptname like 'Dep%'
group by s.studentid;
STUDENTID DEPTNAMES
---------- ------------------------------
1 Dep Aero;Dep Maths
2 Dep Chemistry
3
Either way, if you only want to see a single student's results when add that as a where clause, right before the group by:
...
where s.studentid = 1
group by s.studentid;
Try this GROUP_CONCAT -
SELECT stud2.studentId,
CAST((SELECT GROUP_CONCAT(CONCAT(dep.depName,'; ') FROM tblDept dep
INNER JOIN tblStudent stud ON (stud.DeptId = dep.DeptId)))
FROM tblStudent stud2
No join is necessary for your query. If you want to do this for a particular student id:
select listagg(d.DeptName, ';') within group (order by d.DeptName)
from tblDept d
where d.studentid = :studentid and
d.DeptName like 'Dep%';
you can alo use this:
select
st.studentid,
listagg(d.DeptName,';') within group( order by d.DeptName )
From tblStudent st
join tblDept d on d.studentid = st.studentid
where DeptName like 'Dep%'
group by st.studentid
sqlFiddle

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