Select highest value of subquery in oracle - sql

I have the following query:
SELECT P.ID,
(
SELECT F.VALUE_ID FROM FORM F
INNER JOIN F.PERSON_ID = P.ID
) AS LATEST_FORM
FROM FORM F1 INNER JOIN PERSON P
ON P.ID = F1.PERSON_ID
WHERE F1.NO_PCP_IND IS NOT NULL
ORDER BY P.ID
Each person can have multiple forms and I'm just trying to get the latest form they have submitted. I tried using ROWNUM inside the subquery and I understand why it doesn't work but not sure how to make it generate the right result set with Oracle.

This is the query:
SELECT P.ID,
(SELECT F.VALUE_ID
FROM FORM F INNER JOIN
F.PERSON_ID = P.ID
) AS LATEST_FORM
FROM FORM F1 INNER JOIN
PERSON P
ON P.ID = F1.PERSON_ID
WHERE F1.NO_PCP_IND IS NOT NULL
ORDER BY P.ID;
In addition to being syntactically incorrect, this would return one row per person and user if it worked. I think your original query should be more like:
SELECT P.ID,
(SELECT F.VALUE_ID
FROM FORM F
WHERE F1.NO_PCP_IND IS NOT NULL AND F.PERSON_ID = P.ID AND
ROWNUM = 1
) AS LATEST_FORM
FROM PERSON P
ORDER BY P.ID;
This doesn't fix your problem, because you want to keep the last value. For that, use the keep functionality:
SELECT P.ID,
(SELECT MAX(F.VALUE_ID) KEEP (DENSE_RANK FIRST ORDER BY XXX DESC)
FROM FORM F
WHERE F1.NO_PCP_IND IS NOT NULL AND F.PERSON_ID = P.ID
) AS LATEST_FORM
FROM PERSON P
ORDER BY P.ID;
In this case XXX is the name of the column that defines the ordering of the forms for each person.

I believe its the ValueId which tells about the latest form
So wouldn't this work??
SELECT max(F.VALUEID), P.ID
FROM FORM F, PERSON P
where F.PID = P.ID

Related

Get max/min value of the column independet from where clause

I am having the following query and running it on postgress
select
p.id as p_id,
p.name as p_name,
p.tags,
p.creator,
p.value
p.creation_date,
cp.id as c_part_id,
fr.distance
count(*) OVER() AS total_item
from t_p p
left join t_c_part cp on p.id = cp.p_id
left join t_fl fr on p.id = fr.p_id
where p.name = 'test'
ORDER BY p.id ASC, p.name ASC
OFFSET 0 FETCH NEXT 25 ROWS only
What is missing here is that I also need to get max(p.value) and min(p.value) not affected by the "where" clause - so calculated from total (all) values.
I am dreaming that I can do it within one query and reduce the number of transactions.
Honestly not sure if it is possible!
What I tried is something like this ->
SELECT
(SELECT COUNT(*) from t_p) as count,
(SELECT json_agg(t.*) FROM (
SELECT * FROM t_p
where ***
) AS t) AS rows
But this one did not look really nice as it require additional JSON manipulation at the backend.
I discovered that I might try to use the "with" statement to create a temporary view so the where condition is only evaluated once, but did not succeed to make it works...
You can add the extra columns as scalar subqueries in the form (select min(value) from t_p). Their values are not related to the main query so they should be totally independent.
Your original query has some minor syntax issues (missing commas). I fixed those and the result is:
select
p.id as p_id,
p.name as p_name,
p.tags,
p.creator,
p.value,
p.creation_date,
cp.p_id as c_part_id,
fr.distance,
count(*) OVER() AS total_item,
(select min(value) from t_p) as min_value,
(select max(value) from t_p) as max_value
from t_p p
left join t_c_part cp on p.id = cp.p_id
left join t_fl fr on p.id = fr.p_id
where p.name = 'test'
ORDER BY p.id ASC, p.name ASC
OFFSET 0 FETCH NEXT 25 ROWS only
See running query (without any data) at DB Fiddle.
You can join to a sub-query that calculates both MIN & MAX.
...
from t_p p
left join t_c_part cp on p.id = cp.p_id
left join t_fl fr on p.id = fr.p_id
cross join (
select
min(value) as min_value
, max(value) as max_value
, avg(value) as avg_value
from t_p
) as v
...
Then use v.min_value and v.max_value in the select.
Doesn't even have to be a LATERAL.
You could get the minimum and maximum "on the side" like this:
select
p.id as p_id,
p.name as p_name,
p.tags,
p.creator,
p.value
p.creation_date,
cp.id as c_part_id,
fr.distance,
count(*) OVER() AS total_item,
p.min_value,
p.max_value
from (SELECT id,
name,
tags,
creator,
value,
creation_date,
min(value) OVER () AS min_value,
max(value) OVER () AS max_value,
FROM t_p) AS p
left join t_c_part cp on p.id = cp.p_id
left join t_fl fr on p.id = fr.p_id
where p.name = 'test'
ORDER BY p.id ASC, p.name ASC
OFFSET 0 FETCH NEXT 25 ROWS only;

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

How to add condition on the left table to include “zero” / “0” results in COUNT aggregate?

I have an SQL-select:
SELECT
p.id,
COUNT(a.id)
FROM Person p
LEFT JOIN Account a
ON a.person_id = p.id
WHERE p.id = 1
GROUP BY p.id;
and it works fine. But if I add a condition on left table this query will return no rows instead of zero count:
SELECT
p.id,
COUNT(a.id)
FROM Person p
LEFT JOIN Account a
ON a.person_id = p.id
WHERE p.id = 1 AND a.state = '0'
GROUP BY p.id;
How can add the condition on the left table that returns 0 count in case there are no results?
In a LEFT JOIN, conditions on the second table need to be in the ON clause:
SELECT p.id, COUNT(a.id)
FROM Person p LEFT JOIN
Account a
ON a.person_id = p.id AND a.state = '0'
WHERE p.id = 1
GROUP BY p.id;
The rule is pretty simple to follow. A LEFT JOIN keeps all rows in the first table, even when there is no match in the second table. The values in the second table become NULL. The NULL value will fail the condition a.state = '0'.

How to make LEFT JOIN with row having max date?

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
;

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.