How to use a where clause with a join - sql

Table Student
Id
Name
1
Name
2
Name
Table Classes
Id
Name
Number
1
class1
2
2
class1
3
and a join table
StudentId
ClassId
Income
1
1
5
2
2
6
and the query
select sum(scr.Income)
from Student s
left join StudentClassRelation scr on scr.classId = s.Id
left join Class c on c.Id = scr.classId and c.Number > 4
I want to move c.Number > 4 on the second line in order to receive only the income from classes with number greater than 4. I cannot change the query significantly because it is a part of a bigger one. I need to filter StudentClassRelation somehow
CREATE TABLE Student
(
Id INT PRIMARY KEY,
Name VARCHAR(100),
);
CREATE TABLE Class
(
Id INT PRIMARY KEY,
Name VARCHAR(100),
Number int
);
CREATE TABLE StudentClassRelation
(
Income int,
StudentID INT NOT NULL,
ClassID INT NOT NULL,
FOREIGN KEY (StudentID) REFERENCES Student(Id),
FOREIGN KEY (ClassID) REFERENCES Class(Id),
UNIQUE (StudentID, ClassID)
);
INSERT INTO Class (Id, Name, Number)
VALUES (1, '1', 1), (2, '5', 5)
INSERT INTO Student (Id, Name)
VALUES (1, '1'), (2, '5')
INSERT INTO StudentClassRelation (StudentID, ClassID, Income)
VALUES (1, 1, 10), (2, 1, 20), (2, 2, 5)

To keep the join to StudentClassRelation as a LEFT JOIN and apply a filter based on a column in the class table, you could use LEFT JOIN to a subquery that uses an INNER JOIN on Class, e.g.
SELECT SUM(scr.Income)
FROM Student AS s
LEFT JOIN
( SELECT scr.StudentId, scr.Income
FROM StudentClassRelation AS scr
INNER JOIN Class AS c
ON c.Id = scr.ClassId
WHERE c.Number > 4
) AS scr
ON scr.StudentId = s.Id;
You can however rewrite this in a less verbose way as follows:
SELECT SUM(scr.Income)
FROM Student AS s
LEFT JOIN (StudentClassRelation AS scr
INNER JOIN Class AS c
ON c.Id = scr.ClassId
AND c.Number > 4)
ON scr.StudentId = s.Id;
The execution plans of the two are exactly the same, which is "better" would be entirely personal preference as to which you find more readable. You spend more time reading code than writing it, so the least verbose method does not equate to the best method.
Also worth noting, that if this was the entire query, there is no difference at all from simply using INNER JOIN throughout
SELECT SUM(scr.Income)
FROM Student AS s
INNER JOIN StudentClassRelation AS scr
ON scr.classId = s.Id
INNER JOIN Class AS c
ON c.Id = scr.classId
AND c.Number > 4;
But since you have mentioned that this is part of a larger query, I will assume that there is more to it than just the posted sample, and there is in fact a need for the LEFT JOIN to StudentClassRelation, e.g. If you were to do something like:
SELECT s.Id, Income = SUM(scr.Income)
FROM Student AS s
LEFT JOIN (StudentClassRelation AS scr
INNER JOIN Class AS c
ON c.Id = scr.ClassId
AND c.Number > 4)
ON scr.StudentId = s.Id
GROUP BY s.Id;
This would yield different results to the version with an INNER JOIN to both tables
SELECT s.Id, Income = SUM(scr.Income)
FROM Student AS s
INNER JOIN StudentClassRelation AS scr
ON scr.classId = s.Id
INNER JOIN Class AS c
ON c.Id = scr.classId
AND c.Number > 4
GROUP BY s.Id;

Here a workaround (2options) if u dont want to change the rest of your query
select sum(scr.Income) from Student s
left join StudentClassRelation scr on scr.classId = s.Id
and EXISTS(select top 1 1 from Class where Class.Id = scr.classId and Class.Number > 4)
--> 5
select sum(scr.Income) from Student s
left join StudentClassRelation scr on scr.classId = s.Id
and (select top 1 Class.Number from Class where Class.Id = scr.classId) > 4
--> 5
Is it working as expected?
EDIT: my bad, the solution provided by #GarethD is way better (using inner join instead of left join)

Related

How to select passengers that never flew to a city

I will send the Database Description in an Image.
I tried this Select but I'm afraid that this isn't right
SELECT t.type , a.ICAOId , a.name , ci.id , c.ISOAlpha2ID , p.docReference , ti.docReference , ti.number , p.name , p.surname
FROM dbo.AirportType t
INNER JOIN dbo.Airport a ON t.type = a.type
INNER JOIN dbo.City ci ON a.city = ci.id
INNER JOIN dbo.Country c ON ci.ISOalpha2Id = c.ISOalpha2Id
INNER JOIN dbo.Passenger p ON c.ISOalpha2Id = p.nationality
INNER JOIN dbo.Ticket ti ON p.docReference = ti.docReference
WHERE NOT ci.id = 'Tokyo'
Can you please help to get this right?
enter image description here
You could make a list of the passengers that HAVE flown to the city then use that as a subquery to select the ones not in the list
I am just going to make an example of how it should be done
Subquery:
SELECT p.id FROM passengers
JOIN tickets t ON p.id = t.passengerID
JOIN city c ON c.id = t.cityID
Now you just put that into another query that selects the elements not in it
SELECT * FROM passenger
WHERE id not in (
SELECT p.id FROM passengers
JOIN tickets t ON p.id = t.passengerID
JOIN city c ON c.id = t.cityID
WHERE c.name= 'tokyo'
)
Notice I didn't use your attribute names, you will have to change those.
This was a bit simplified version of what you will have to do because the city is not directly in your tickets table. So you will also have to join tickets, with coupons, and flights to get the people that have flown to a city. But from there it is the same.
Overall I believe this should help you get what you have to do.
A minimal reproducible example is not provided.
Here is a conceptual example, that could be easily extended to a real scenario.
SQL
-- DDL and sample data population, start
DECLARE #passenger TABLE (passengerID INT PRIMARY KEY, passenger_name VARCHAR(20));
INSERT #passenger (passengerID, passenger_name) VALUES
(1, 'Anna'),
(2, 'Paul');
DECLARE #city TABLE (cityID INT PRIMARY KEY, city_name VARCHAR(20));
INSERT #city (cityID, city_name) VALUES
(1, 'Miami'),
(2, 'Orldando'),
(3, 'Tokyo');
-- Already visited cities
DECLARE #passenger_city TABLE (passengerID INT, cityID INT);
INSERT #passenger_city (passengerID, cityID) VALUES
(1, 1),
(2, 3);
-- DDL and sample data population, end
SELECT * FROM #passenger;
SELECT * FROM #city;
SELECT * FROM #passenger_city;
;WITH rs AS
(
SELECT c.passengerID, b.cityID
FROM #passenger AS c
CROSS JOIN #city AS b -- get all possible combinations of passengers and cities
EXCEPT -- filter out already visited cities
SELECT passengerID, cityID FROM #passenger_city
)
SELECT c.*, b.city_name
FROM rs
INNER JOIN #passenger AS c ON c.passengerID = rs.passengerID
INNER JOIN #city AS b ON b.cityID = rs.cityID
ORDER BY c.passenger_name, b.city_name;
Output
passengerID
passenger_name
city_name
1
Anna
Orldando
1
Anna
Tokyo
2
Paul
Miami
2
Paul
Orldando

Figure out the total number of people in an overlapping er database

I am trying to find:
the total number of doctors which aren't patients
the total number of patients which aren't doctors
the total number of people who are both patients and doctors
I can't seem to get the correct answer.
SQL:
CREATE TABLE persons (
id integer primary key,
name text
);
CREATE TABLE doctors (
id integer primary key,
type text,
FOREIGN KEY (id) REFERENCES persons(id)
);
CREATE TABLE patients (
id integer primary key,
suffering_from text,
FOREIGN KEY (id) REFERENCES persons(id)
);
INSERT INTO persons (id, name) VALUES
(1, 'bob'), (2, 'james'), (3, 'bill'), (4, 'mark'), (5, 'chloe');
INSERT INTO doctors (id, type) VALUES
(2, 'family doctor'), (3, 'eye doctor'), (5, 'family doctor');
INSERT INTO patients (id, suffering_from) VALUES
(1, 'flu'), (2, 'diabetes');
Select statement:
select count(d.id) as total_doctors, count(pa.id) as total_patients, count(d.id) + count(pa.id) as both_doctor_and_patient
from persons p
JOIN doctors d
ON p.id = d.id
JOIN patients pa
ON p.id = pa.id;
http://www.sqlfiddle.com/#!17/98ae9/2
One option uses left joins from persons and conditional aggrgation:
select
count(dr.id) filter(where pa.id is null) cnt_doctor_not_patient,
count(pa.id) filter(where dr.id is null) cnt_patient_not_doctor,
count(pa.id) filter(where do.id is not null) cnt_patient_and_doctor,
count(*) filter(where dr.id is null and pa.id is null) cnt_persons_not_dotor_nor_patient
from persons pe
left join doctors dr on dr.id = pe.id
left join patients pa on pa.id = pe.id
As a bonus, this gives you an opportunity to count the persons that are neither patient nor doctor. If you don't need that information, then a full join is simpler, and does not require bringing the persons table:
select
count(dr.id) filter(where pa.id is null) cnt_doctor_not_patient,
count(pa.id) filter(where dr.id is null) cnt_patient_not_doctor,
count(pa.id) filter(where dr.id is not null) cnt_patient_and_doctor
from doctors dr
full join patients pa using (id)
You can simply solve this using LEFT JOIN like:
--Aren't doctors:
SELECT count(*) from persons as A left join doctors as B on A.id=B.id where B.id is null
--Aren't patients:
SELECT count(*) from persons as A left join patients as B on A.id=B.id where B.id is null
--Both:
SELECT
(SELECT count(*) from persons as A left join patients as B on A.id=B.id where B.id is not null) +
(SELECT count(*) from persons as A left join doctors as B on A.id=B.id where B.id is not null)
AS summ
Here a CTE alternative:
with doc_not_pat
as(
select count(*) as Doc_Not_Pat
from doctors d
where not exists (select 1 from patients p where p.id = d.id)
),
pat_not_doc as(
select count(*) as Pat_Not_Doc
from patients p
where not exists ( select 1 from doctors d where d.id = p.id)
),
pat_and_doc as(
select count(*) as Pat_And_Doc
from patients p
where exists (select 1 from doctors d where d.id = p.id)
)
select (select Doc_Not_Pat
from doc_not_pat dcp) as Doc_Not_Pat,
(select Pat_Not_Doc
from pat_not_doc) as Pat_Not_Doc,
(select Pat_And_Doc
from pat_and_doc) as Pat_And_Doc

ORACLE SQL check if the row number of the table is n, then perform a join

TABLE student: ID, ID2, NAME, AGE
TABLE class: ID, CLASS_NAME, some other columns
TABLE school: ID2, some other columns.
I am trying to perform below in Oracle SQL:
If the count of the records in TABLE "student" with age>5, is 1,
join the student table with "CLASS" table by "ID", else, join the student table with "school" table by "ID2".
I found I cannot put count in where clause. Can someone help?
I would use window functions:
select s.*, . . .
from (select s.*, sum(case when age > 5 then 1 else 0 end) over () as cnt5
from students s
) s left join
class c
on c.id = s.id and cnt5 = 1 left join
school sch
on sch.id2 = s.id2 and cnt5 <> 1
where c.id is not null or sch.id is not null
you can use left join with case
select s.*, case when age > 5 then COALESCE (c.ID,scl.ID2) as id
from student s
left join class c on s.ID=c.ID
left join school scl on s.ID2=scl.ID2
As Gordon said window functions are an option, but this is also a rare occasion where you can deliberately perform a cross join:
select s.*
,...
from students s
left join
(
select count(*) as RECORD_COUNT
from students
where age > 5
) sub
on 1 = 1
left join class c
on s.id = c.id
and sub.record_count = 1
left join school h
on s.id2 = h.id2
and c.id is null

Select all products with prices in EAV scheme

It's a generic SQL question. I have a query that selects all rows from the Products with extra information from other tables. The problem is that it's EAV scheme and the last relation is somehow reversed and joins break.
The requirements are:
list all Products with Groups
if 'Price' in Values table is available, add this information
explicitly: if 'Price' is not available, there should be Product row without Price information
Products can't be repeated
Additionally: DISTINCT is out of question
I have a working query (below) that uses a subquery to filter values, but I need to get rid of it. I can only uses joins.
SQL Fiddle : http://sqlfiddle.com/#!15/0576b/8
create table Products (
id int primary key,
groupId int,
code varchar(100)
);
create table Groups (
id int primary key,
code varchar(100)
);
create table Values (
id int primary key,
productId int,
typeId int,
value varchar(100)
);
create table ValueTypes (
id int primary key,
name varchar(100)
);
insert into Products values (1, 1, 'P1');
insert into Products values (2, 2, 'P2');
insert into Groups values (1, 'C1');
insert into Groups values (2, 'C2');
insert into Values values (1, 1, 1, 'Aqua');
insert into Values values (2, 1, 2, '$5');
insert into ValueTypes values (1, 'Name');
insert into ValueTypes values (2, 'Price');
My query that works:
SELECT *
FROM Products p
INNER JOIN Groups g ON p.groupId = g.id
LEFT JOIN Values v ON v.productId = p.id AND v.typeId = (SELECT id FROM ValueTypes WHERE name = 'Price')
The question is, how to rewrite it to use joins instead of subquery?
I tried:
SELECT *
FROM Products p
INNER JOIN Groups g ON p.groupId = g.id
LEFT JOIN Values v ON v.productId = p.id
LEFT JOIN ValueTypes vt ON vt.id = v.typeId AND vt.name = 'Price'
But it returns repeated product P1. INNER JOIN on the other hand omits Products without a 'Price' value.
Define JOIN order explicitly
SELECT *
FROM Products p
INNER JOIN Groups g ON p.groupId = g.id
LEFT JOIN ( --product price
SELECT productId, value
FROM Values v2
JOIN ValueTypes vt ON vt.id = v2.typeId AND vt.name = 'Price'
) v ON v.productId = p.id;
EDIT
2 more JOIN versions. Optimizer produces different plan as compared to above version
SELECT *
FROM ValueTypes vt
INNER JOIN Products p ON vt.name = 'Price'
INNER JOIN Groups g ON p.groupId = g.id
LEFT JOIN Values v ON v.productId = p.id AND v.typeId = vt.id;
or slightly different v3
SELECT *
FROM (SELECT id FROM ValueTypes WHERE name = 'Price') vt
CROSS JOIN Products p
INNER JOIN Groups g ON p.groupId = g.id
LEFT JOIN Values v ON v.productId = p.id AND v.typeId = vt.id
You can do as this:
SELECT p.id, p.code, g.id, g.code,
max(case when vt.name='Price'
then v.value
else null end) as price
FROM Products p
LEFT JOIN Groups g ON p.groupId = g.id
LEFT JOIN Values v ON v.productId = p.id
LEFT join ValueTypes vt ON v.typeId = vt.id
group by p.id, p.code, g.id, g.code
See it working here: http://sqlfiddle.com/#!15/0576b/36

SQL query for master-detail

Master table contains ID and PersonName.
Course table contains ID, CourseName.
Detail table contains ID, MasterID, CourseID, StartDate,EndDate
I want to create report that shows list of persons (PersonName) and the only last course they took (so every person is listed only once):
PersonName - CourseName - StartDate - EndDate
select m.PersonName, c.CourseName
from Master m
join Detail d on d.MasterID = m.ID
join Course c on c.ID = d.CourseID
where d.StartDate = (select max(d2.StartDate)
from Detail d2
where d2.MasterID = m.ID
)
Select personname,coursename from details
inner join course on course.id = details.courseid
inner join master on master.id = details.masterid
inner join (select max(startdate) , courseid,masterid
from details group by masterid,courseid ) as tb1
on tb1.courseid = details.courseid and tb1.masterid = details.masterid