How can I unite these two related queries? - sql

I have this query to check if a person is a customer or have been:
SELECT DISTINCT ON (person_id) person_id, person.name,
(CASE WHEN status = 'inactive' then 'Ex-Customer'
WHEN status = 'active' then 'Customer'
END) AS account_status
FROM person_subscription
INNER JOIN person ON person_subscription.person_id = person.id
ORDER BY person_id, status ASC
And I have this other query to get the locations:
SELECT person_id, string_agg(name, ';' ORDER BY person_id)
FROM person_location WHERE person_id IN
(SELECT person_id FROM person_subscription WHERE status IS NOT NULL)
GROUP BY person_id;
How can I unite them and show person location as a single row on the first query?

If I follow this correctly, you can use lateral joins:
select p.id as person_id, p.name, pl.*, ps.*
from person p
cross join lateral (
select string_agg(pl.name, ';' order by pl.name) as as person_locations
from person_location pl
where pl.person_id = p.id
) pl
cross join lateral (
select
case status
when 'inactive' then 'ex-customer'
when 'active' then 'customer'
end as account_status
from person_subscription ps
where ps.person_id = p.id
order by ps.??
limit 1
) ps
As commented already, your original first query is missing an order by clause, which makes it undefined which subscription status will be chosen where there are several matches. This translates as order by ps.?? in the second subquery, which you would need to replace with the relevant column name.
Another flaw, that time in the second query in your question, is that the order by clause of string_agg() is not deterministic (all rows in the group have the same person_id). I ordered by location name instead, you can change that to some other column if you like.

You would join it in:
SELECT DISTINCT ON (ps.person_id) ps.person_id, ps.person.name,
(CASE WHEN ps.status = 'inactive' then 'Ex-Customer'
WHEN ps.status = 'active' then 'Customer'
END) AS account_status
FROM person_subscription ps INNER JOIN
person p
ON ps.person_id = p.id LEFT JOIN
(SELECT pl.person_id, STRING_AGG(pl.name, ';') as names
FROM person_location pl
GROUP BY pl.person_id
) pl
ON pl.person_id = p.id
ORDER BY ps.person_id, p.status ASC;
I'm not sure what the significance of the WHERE clause is for getting locations, but you can include that in the subquery as well.

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;

Joining two tables with special case on the right table?

I have two tables: Candidate: {Id, Name} and Candidate_Education: {Id, CandidateId, Education, GraduationDate}
I want to show the candidate name and his last education , I made that query:
SELECT c.Name, ce.Education AS 'Last Education'
FROM Candidate c
LEFT JOIN Candidate_Education ce
ON c.Id = (SELECT TOP 1 CandidateID FROM Candidate_Education
ORDER BY GraduationDate DESC)
But the results is not correct, there are Candidates who assigned Educations they don't have relation with Candidate_Education
More typical ways to do what you want use ROW_NUMBER() or OUTER APPLY:
SELECT c.Name, ce.Education
FROM Candidate c OUTER APPLY
(SELECT TOP 1 ce.*
FROM Candidate_Education ce
WHERE ce.CandidateID = c.CandidateID
ORDER BY ce.GraduationDate DESC
) ce;
Your query is missing a join condition between the two tables in the FROM clause. However, there are alternatives that are more appropriate for SQL Server.
other solution
with OneCandidate as (
select * from (
select ce.*, ROW_NUMBER() over(partition by ce.CandidateID order by ce.CandidateID desc) rang
from Candidate_Education ce
) tmp
where tmp.rang=1
)
SELECT c.Name, ce.Education
FROM Candidate c left outer OneCandidate ce on ce.CandidateID = c.CandidateID
WITH maxGraduationCTE AS
(SELECT Id, MAX(GraduationDate) AS 'Last Education' FROM Candidate_Education
GROUP BY Id),
lastEducationCTE AS
(SELECT [Last Education], ce.Id, ce.CandidateId, ce.Education FROM maxGraduationCTE
INNER JOIN Candidate_Education ce ON maxGraduationCTE.Id = ce.Id);
SELECT c.Name,
CASE WHEN [Last Education] IS NULL THEN 'No Education'
ELSE CAST([Last Education] As Varchar)
END As [Last Education]
FROM Candidate c
LEFT JOIN lastEducationCTE ce
ON c.Id = ce.CandidateId
You could use two CTE expressions and a left join with a CASE statement to handle null values. I used a case because I wasn't sure about the data type for the GraduationDate...may have to convert it because if it is a date, you can't mix varchar and date data types

Count and group the number of times each town is listed in the table

SELECT PEOPLE.TOWNKEY, TOWN_LOOKUP.TOWN FROM PEOPLE
INNER JOIN TOWN_LOOKUP
ON PEOPLE.TOWNKEY = TOWN_LOOKUP.PK
ORDER BY TOWN
Current Table Output:
You are missing the group by clause entirely:
SELECT tl.town, COUNT(*)
FROM people p
INNER JOIN town_lookup ON p.townkey = tl.pk
GROUP BY tl.town
ORDER BY tl.town

select count from table upon other table

i have two tables (main_table) and (sub_table) related by person_id ..now i want to select person_id from main table and count of records in sub_table that related to main_table where records in sub_table not equal 'eco' .. now the problem is when i make this .. the query get only person_id that not equal 'eco' ... but i want from query to select every person_id and select count 0 if person_id in sub_table equal 'eco' :
SELECT m.person_id, COUNT(*) AS eco FROM (SELECT person_id FROM Main_table
WHERE (person_id ='c')AS m INNER JOIN
(SELECT person_id
FROM sub_table
WHERE person_status != 'eco'
GROUP BY person_id) AS eco ON eco.person_id = m.person_id GROUP BY m.person_id
The problem is that you are INNER joining to your sub_table, so you are by definition limiting the results to only people with an entry in this table that does not equal eco.
I think you can do this by simply left joining to your sub table, with the person_status in the join criteria:
SELECT m.Person_ID,
COUNT(s.Person_ID) AS NonEcoCount
FROM Main_Table m
LEFT JOIN sub_table s
ON s.Person_ID = m.Person_ID
AND s.person_status != 'eco'
GROUP BY m.Person_ID;
SELECT m.person_id,
countNonEco = (SELECT COUNT(*) FROM sub_table s
WHERE m.person_id = s.person_id
AND (s.person_status IS NULL OR s.person_status <> 'eco'))
FROM Main_table m

Need your help in SQL query

There are two tables:
Clients (id, name)
Order (id, id_client, name), where id_client - foreign key.
Write a query that selects the identifier and name of the first table and the number of records in the second table, associated with them. The result should be sorted by surname in descending order.
I've tried
SELECT
Clients.id, Clients.name, count(id)
FROM clients
INNER JOIN Order ON Clients.id = Order.id_client
GROUP BY
Clients.id, Clients.name
ORDER BY
Clients.name DESC
But it doesn't work. What is wrong?
SELECT
c.ID,
c.Name,
COUNT(o.ID)
FROM
Clients c
LEFT JOIN [Order] o
ON
o.id_client = c.id
GROUP BY
c.ID,
c.Name
ORDER BY
c.Name DESC
SELECT Clients.id, Clients.name, count(client.id) FROM clients INNER JOIN Order on Clients.id=Order.id_client GROUP BY Clients.id, Clients.name ORDER BY Clients.name DESC
Change count(id) to
count(Clients.id) or count(Order.id)
I don't know which table you need count(id) from. I hope you understand where the issue is.
SELECT
c.ID,
c.Name,
COUNT(o.ID)
FROM
Clients c,
Order o
WHERE o.id_client = c.id
GROUP BY
c.ID
c.Name