How to make LEFT JOIN with row having max date? - sql

I have two tables in Oracle DB
Person (
id
)
Bill (
id,
date,
amount,
person_id
)
I need to get person and amount from last bill if exist.
I trying to do it this way
SELECT
p.id,
b.amount
FROM Person p
LEFT JOIN Bill b
ON b.person_id = p.id AND b.date = (SELECT MAX(date) FROM Bill WHERE person_id = 1)
WHERE p.id = 1;
But this query works only with INNER JOIN. In case of LEFT JOIN it throws ORA-01799 a column may not be outer-joined to a subquery
How can I get amoun from the last bill using left join?

Please try the below avoiding sub query to be outer joined
SELECT
p.id,
b.amount
FROM Person p
LEFT JOIN(select * from Bill where date =
(SELECT MAX(date) FROM Bill b1 WHERE person_id = 1)) b ON b.person_id = p.id
WHERE p.id = 1;

What you are looking for is a way to tell in bills, for each person, what is the latest record, and that one is the one to join with. One way is to use row_number:
select * from person p
left join (select b.*,
row_number() over (partition by person_id order by date desc) as seq_num
from bills b) b
on p.id = b.person_id
and seq_num = 1

You cannot have a subquery inside an ON statement.
Instead you need to convert your LEFT JOIN statement into a whole subquery.
Not tested but this should work.
SELECT
p.id,
b.amount
FROM Person p
LEFT JOIN (
SELECT id FROM Bill
WHERE person_id = p.id
AND date = (SELECT date FROM Bill WHERE person_id = 1)) b
WHERE p.id = 1;
I'm not quite sure why you would want to filter for the date though.
Simply filtering for the person_id should do the trick

you should join Person and Bill to the result for max date in bill related to person_id
select Person.id, bill.amount
from Person
left join bill on bill.person_id = person.id
left join (
select person_id, max(date) as max_date
from bill
group by person_id ) t on t.person_id = Person.id and b.date = t.max_date

Hey you can do like this
SELECT
p.id,
b.amount
FROM Person p
LEFT JOIN Bill b
ON b.person_id = p.id AND b.date = (SELECT max(date) FROM Bill WHERE person_id = 1)
WHERE p.id = 1

SELECT
p.id,
b.amount
FROM Person p
LEFT JOIN Bill b
ON b.person_id = p.id
WHERE (SELECT max(date) FROM bill AS sb WHERE sb.person_id=p.id LIMIT 1)=b.date;

SELECT
p.id,
c.amount
FROM Person p
LEFT JOIN (select b.person_id as personid,b.amount as amount from Bill b where b.date1= (select max(date1) from Bill where person_id=1)) c
ON c.personid = p.id
WHERE p.id = 1;

try this
select * from person p
left join (select MAX(id) KEEP (DENSE_RANK FIRST ORDER BY date DESC)
from bills b) b
on p.id = b.person_id

I use GREATEST() function in join condition:
SELECT
p.id,
b.amount
FROM Person p
LEFT JOIN Bill b
ON b.person_id = p.id
AND b.date = GREATEST(b.date)
WHERE p.id = 1

This allows you to grab the whole row if necessary and grab the top x rows
SELECT p.id
,b.amount
FROM person p
LEFT JOIN
(
SELECT * FROM
(
SELECT date
,ROW_NUMBER() OVER (PARTITION BY person_id ORDER BY date DESC) AS row_num
FROM bill
)
WHERE row_num = 1
) b ON p.id = b.person_id
WHERE p.id = 1
;

Related

How to select only 1 record from a group with a unique condition

I have the following query. This query allows me to produce a list of children and their familymember contacts (contactpupilID).
select s.studentnr,pc.pupilid, pc.contactpupilid, p2.mainmail
from student s
join pupil p on p.id = s.pupilid
join pupilcontact pc on pc.pupilid = p.id
join pupil p2 on p2.id = pc.contactpupilid
where CURRENT_DATE between pc.validfrom and pc.validuntil
order by pc.pupilid
Each child can have 0 to 3 familycontacts (0 because no contact has been added during registration).
Each familycontact (contactpupilid) has an email field. However there are cases where all familycontacts have an email or 1 of them or none of them.
My list needs to select a child with a familycontact(contactpupilid) that has an email. The familycontact that is selected should be the one that has an email.
If none of the familycontacts have an email then it should select the 1st familycontact by default.
This is how it needs to look like
How would I complete this task?
I don't know what you mean by "first record" because SQL tables are unordered. I can assume you mean the one with the smallest contactpupilid.
What you have described is what distinct on does:
select distinct on (s.studentnr) s.studentnr, pc.pupilid, pc.contactpupilid, p2.mainmail
from student s join
pupil p
on p.id = s.pupilid join
pupilcontact pc
on pc.pupilid = p.id join
pupil p2
on p2.id = pc.contactpupilid
where CURRENT_DATE between pc.validfrom and pc.validuntil
order by s.studentnr, (p2.mainmail is not null) desc;
Use ROW_NUMBER() window function in your query to rank the rows that contain an email first:
with cte as (
select s.studentnr, pc.pupilid, pc.contactpupilid, p2.mainmail,
row_number() over (partition by s.studentnr order by p2.mainmail is not null desc, pc.contactpupilid) rn
from student s
join pupil p on p.id = s.pupilid
join pupilcontact pc on pc.pupilid = p.id
join pupil p2 on p2.id = pc.contactpupilid
where CURRENT_DATE between pc.validfrom and pc.validuntil
)
select studentnr, pupilid, contactpupilid, mainmail
from cte
where rn = 1
order by pupilid;
You can do it with CTE like this
with temp as (
select s.studentnr,pc.pupilid, pc.contactpupilid, p2.mainmail,row_number() over (partition by pupilid order by pupilid) as row_number
from student s
join pupil p on p.id = s.pupilid
join pupilcontact pc on pc.pupilid = p.id
join pupil p2 on p2.id = pc.contactpupilid
where CURRENT_DATE between pc.validfrom and pc.validuntil
order by pc.pupilid
)
select *
from temp
where row_number = 1

Getting oldest Date SQL Complexity

I have a problem which I cannot resolve no matter what without using code, instead of SQL SCRIPT.
I have 2 tables
Person
ID Name Type
1 A A1
2 B A2
3 C A3
4 D A4
5 E A6
PersonHomes
HOMEID Location PurchaseDate PersonID
1 CA 20160101 1
2 CT 20160202 1
3 DT 20160101 2
4 BT 20170102 3
5 CT 20160303 1
6 CA 20160101 2
PersonID is foreign key of Person Table
There are no other rowz in the tables
So, we have to show detail of EACH person WITH home
The rule to write output is
IF Person has SINGLE entry in PersonHomes then use it
IF Person has MORE than ONE entry in PersonHomes then we have to look at purchase date, IF they are different then USE the PersonHomes ROW with OLDEST date in it. AND DELETE OTHER ROWS OF HIM
IF Person has MORE than ONE entry in PersonHomes then we have to look at purchase date, and IF DATES are SAME then USE the ROW with LOWER ID AND DELETE THE OTHER ROWS of HIM
This is very easy to do in code but using SQL it is complex
What I tried was to
WITH PERSON (
SELECT * FROM Person)
SELECT * FROM PERSON
INNER JOIN PersonHomes ON Person.ID = PersonHomes.PersonID
WHERE PersonHomes.PersonID = CASE WHEN (COUNT (*) FROM PersonHomes...)
Then I think I can write SQL function ?
I am stuck, Please help!
SAMPLE OUTPUT for PERSON A
ID NAME Type HOMEID Location PurchaseDate
1 A A1 5 CT 20160303
For PERSON B
ID NAME Type HOMEID Location PurchaseDate
1 A A2 3 DT 20160101
Aiden
It is not so easy to get desired output with SQL. we should write more than one sql queries.
First I created a temp table which consists of home details:
select PersonID, count(*) as HomeCount, count(distinct PurchaseDate) as
PurchaseDateCount, min(PurchaseDate) oldestPurchaseDate, min(HOMEID) as
LowerHomeID into #PersonHomesAbstractTable from PersonHomes group by PersonID
Then for the output of your first rule:
select p.ID, p.NAME, p.Type, ph.HOMEID, ph.Location, ph.PurchaseDate from Person p
inner join #PersonHomesAbstractTable a on p.ID = a.PersonID
inner join PersonHomes ph on p.ID = ph.PersonID
where a.HomeCount = 1
For the output of your second rule:
select p.ID, p.NAME, p.Type, ph.HOMEID, ph.Location, ph.PurchaseDate
from Person p inner join #PersonHomesAbstractTable a on p.ID = a.PersonID
inner join PersonHomes ph on p.ID = ph.PersonID and
ph.PurchaseDate = a.oldestPurchaseDate
where a.HomeCount > 1 and a.PurchaseDateCount <> 1
And finally for the output of your third rule:
select p.ID, p.NAME, p.Type, ph.HOMEID, ph.Location, ph.PurchaseDate
from Person p inner join #PersonHomesAbstractTable a on p.ID = a.PersonID
inner join PersonHomes ph on p.ID = ph.PersonID and
ph.HOMEID = a.LowerHomeID
where a.HomeCount > 1 and a.PurchaseDateCount = 1
Of course there are some other ways, but now this way is come to my mind.
If you want to delete undesired rows, you can use scripts below:
delete from PersonHomes where HOMEID in
(
select ph.HOMEID from #PersonHomesAbstractTable a
inner join PersonHomes ph on a.PersonID = ph.PersonID and
ph.PurchaseDate <> a.oldestPurchaseDate
where a.HomeCount > 1 and a.PurchaseDateCount <> 1
union
select p.HOMEID from #PersonHomesAbstractTable a
inner join PersonHomes ph on a.PersonID = ph.PersonID and
ph.HOMEID <> a.LowerHomeID
where a.HomeCount > 1 and a.PurchaseDateCount = 1
)
You seem to have a prioritization query. I would solve this using row_number():
select ph.*
from (select ph.*,
row_number() over (partition by personid
order by purchasedate asc, homeid asc
) as seqnum
from personhomes ph
) ph
where seqnum = 1;
This doesn't actually change the data in the table. Although you say delete, it seems like you just want a result set with one home per person.
This is shortest approach got by Link
;WITH cte AS
(
SELECT *, RowN = ROW_NUMBER() OVER (PARTITION BY ID ORDER BY AddressMoveDate DESC) FROM Address
)
DELETE FROM cte WHERE RowN > 1

SQL: How to save order in sql query?

I have PostgreSQL database and I try to print all my users (Person).
When I execute this query
-- show owners
-- sorted by maximum cars amount
SELECT p.id
FROM car c JOIN person p ON c.person_id = p.id
GROUP BY p.id
ORDER BY COUNT(p.name) ASC;
I get all owners sorted by cars amount
Output: 3 2 4 1
And all order goes wrong when I try to link owner id.
SELECT *
FROM person p
WHERE p.id IN (
SELECT p.id
FROM car c JOIN person p ON c.person_id = p.id
GROUP BY p.id
ORDER BY COUNT(p.name) ASC);
Output: 1 2 3 4 and other data
You see than order is wrong. So here is my question how can I save that order?
Instead Of subquery use join. Try this.
SELECT p.*
FROM person p
JOIN (SELECT p.id,
Count(p.NAME)cnt
FROM car c
JOIN person p
ON c.person_id = p.id
GROUP BY p.id) b
ON p.id = b.id
ORDER BY cnt ASC
Untangle the mess. Aggregate first, join later:
SELECT p.*
FROM person p
JOIN (
SELECT person_id, count(*) AS ct
FROM car
GROUP BY person_id
) c ON c.person_id = p.id
ORDER BY c.cnt;
No need to join to person twice. This should be fastest if you count most or all rows.
For a small selection, correlated subqueries are faster:
SELECT p.*
FROM person p
ORDER BY (SELECT count(*) FROM car c WHERE c.person_id = p.id)
WHERE p.id BETWEEN 10 AND 20; -- some very selective predicate
As for your original: IN takes a set on the right hand, order of elements is ignored, so ORDER BY is pointless in the subuery.

Return 1 result per left join

Currently I am performing a left join on two tables. The first table has an id and a persons name, the second table has an id, the id of a person from table 1, and then a timestamp (of a flight).
People Flights
id | name id | person_id | time
------------ ---------------------------
1 Dave 1 1 1284762115
2 Becky 2 1 1284787352
3 2 1284772629
4 2 1286432934
5 1 1283239480
When I perform my left join, I get a list of people and their flight times, but what I would like is just the list of people with the flight time with the highest ID
I have been using
SELECT p.id, p.name max(f.time)
FROM People p
LEFT JOIN Flights f ON p.id = f.person_id
GROUP BY p.id, p.name
However, this just gives me the LAST flight time, rather than the last flight time uploaded into the system (ie, highest ID).
1 Dave 1284787352
2 Becky 1286432934
So to reiterate, I would like to see the name of the person, along with the flight time of their last UPLOADED (highest ID) flight time.
1 Dave 1283239480
2 Becky 1286432934
Use:
SELECT p.id,
p.name,
f.time
FROM PEOPLE p
JOIN FLIGHTS f ON f.person_id = p.id
JOIN (SELECT f.person_id,
MAX(f.id) AS max_id
FROM FLIGHTS f
GROUP BY f.person_id) x ON x.person_id = f.person_id
AND x.max_id = f.id
If you are using a database that supports analytics:
SELECT p.id,
p.name,
x.time
FROM PEOPLE p
JOIN (SELECT f.person_id,
f.time,
ROW_NUMBER() OVER(PARTITION BY f.person_id
ORDER BY f.id DESC) AS rk
FROM FLIGHTS f) x ON x.person_id = p.id
AND x.rk = 1
If you want people, including those without flights:
SELECT p.id,
p.name,
f.time
FROM PEOPLE p
LEFT JOIN FLIGHTS f ON f.person_id = p.id
JOIN (SELECT f.person_id,
MAX(f.id) AS max_id
FROM FLIGHTS f
GROUP BY f.person_id) x ON x.person_id = f.person_id
AND x.max_id = f.id
...and the analytic version:
SELECT p.id,
p.name,
x.time
FROM PEOPLE p
LEFT JOIN (SELECT f.person_id,
f.time,
ROW_NUMBER() OVER(PARTITION BY f.person_id
ORDER BY f.id DESC) AS rk
FROM FLIGHTS f) x ON x.person_id = p.id
AND x.rk = 1
I think you are looking for something like the below. group by the person_id and select the max id then use that list to select from the flights. This is my first thought there may be a more efficent way.
EDITED:
SELECT p.id, p.name MAX(f.time)
FROM People p
LEFT JOIN Flights f ON p.id = f.person_id
WHERE f.id in(SELECT MAX(id) FROM flights GROUP BY person_id)

SQL Query to Count() multiple tables

I have a table which has several one to many relationships with other tables. Let's say the main table is a person, and the other tables represent pets, cars and children. I would like a query that returns details of the person,the number of pets, cars and children they have e.g.
Person.Name Count(cars) Count(children) Count(pets)
John Smith 3 2 4
Bob Brown 1 3 0
What is the best way to do this?
Subquery Factoring (9i+):
WITH count_cars AS (
SELECT t.person_id
COUNT(*) num_cars
FROM CARS c
GROUP BY t.person_id),
count_children AS (
SELECT t.person_id
COUNT(*) num_children
FROM CHILDREN c
GROUP BY t.person_id),
count_pets AS (
SELECT p.person_id
COUNT(*) num_pets
FROM PETS p
GROUP BY p.person_id)
SELECT t.name,
NVL(cars.num_cars, 0) 'Count(cars)',
NVL(children.num_children, 0) 'Count(children)',
NVL(pets.num_pets, 0) 'Count(pets)'
FROM PERSONS t
LEFT JOIN count_cars cars ON cars.person_id = t.person_id
LEFT JOIN count_children children ON children.person_id = t.person_id
LEFT JOIN count_pets pets ON pets.person_id = t.person_id
Using inline views:
SELECT t.name,
NVL(cars.num_cars, 0) 'Count(cars)',
NVL(children.num_children, 0) 'Count(children)',
NVL(pets.num_pets, 0) 'Count(pets)'
FROM PERSONS t
LEFT JOIN (SELECT t.person_id
COUNT(*) num_cars
FROM CARS c
GROUP BY t.person_id) cars ON cars.person_id = t.person_id
LEFT JOIN (SELECT t.person_id
COUNT(*) num_children
FROM CHILDREN c
GROUP BY t.person_id) children ON children.person_id = t.person_id
LEFT JOIN (SELECT p.person_id
COUNT(*) num_pets
FROM PETS p
GROUP BY p.person_id) pets ON pets.person_id = t.person_id
you could use the COUNT(distinct x.id) synthax:
SELECT person.name,
COUNT(DISTINCT car.id) cars,
COUNT(DISTINCT child.id) children,
COUNT(DISTINCT pet.id) pets
FROM person
LEFT JOIN car ON (person.id = car.person_id)
LEFT JOIN child ON (person.id = child.person_id)
LEFT JOIN pet ON (person.id = pet.person_id)
GROUP BY person.name
I would probably do it like this:
SELECT Name, PersonCars.num, PersonChildren.num, PersonPets.num
FROM Person p
LEFT JOIN (
SELECT PersonID, COUNT(*) as num
FROM Person INNER JOIN Cars ON Cars.PersonID = Person.PersonID
GROUP BY Person.PersonID
) PersonCars ON PersonCars.PersonID = p.PersonID
LEFT JOIN (
SELECT PersonID, COUNT(*) as num
FROM Person INNER JOIN Children ON Children.PersonID = Person.PersonID
GROUP BY Person.PersonID
) PersonChildren ON PersonChildren.PersonID = p.PersonID
LEFT JOIN (
SELECT PersonID, COUNT(*) as num
FROM Person INNER JOIN Pets ON Pets.PersonID = Person.PersonID
GROUP BY Person.PersonID
) PersonPets ON PersonPets.PersonID = p.PersonID
Note, that it depends on your flavour of RDBMS, whether it supports nested selects like the following:
SELECT p.name AS name
, (SELECT COUNT(*) FROM pets e WHERE e.owner_id = p.id) AS pet_count
, (SELECT COUNT(*) FROM cars c WHERE c.owner_id = p.id) AS world_pollution_increment_device_count
, (SELECT COUNT(*) FROM child h WHERE h.parent_id = p.id) AS world_population_increment
FROM person p
ORDER BY p.name
IIRC, this works at least with PostgreSQL and MSSQL. Not tested, so your mileage may vary.
Using subselects not very good practice, but may be here it will be good
select p.name, (select count(0) from cars c where c.idperson = p.idperson),
(select count(0) from children ch where ch.idperson = p.idperson),
(select count(0) from pets pt where pt.idperson = p.idperson)
from person p
You could do this with three outer joins:
SELECT
Person.Name,
sum(case when cars.id is not null then 1 else 0 end) car_count,
sum(case when children.id is not null then 1 else 0 end) child_count,
sum(case when pets.id is not null then 1 else 0 end) pet_count
FROM
Person
LEFT OUTER JOIN
cars on
Person.id = cars.person_id
LEFT OUTER JOIN
children on
Person.id = children.person_id
LEFT OUTER JOIN
pets on
Person.id = pets.person_id
GROUP BY
Person.Name
I belive that Oracle now supports the case when syntax, but if not you could use a decode.
You'd need to include multiple count statements in the query. Off the top of my head,
SELECT p.Name, COUNT(DISTINCT t.Cars), COUNT(DISTINCT o.Children), Count(DISTINCT p.Pets)
FROM Person p
INNER JOIN Transport t ON p.ID = t.PersonID
LEFT JOIN Offspring o ON p.ID = o.PersonID
LEFT JOIN Pets p ON p.ID = o.OwnerID
GROUP BY p.Name
ORDER BY p.Name