Return 1 result per left join - sql

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 the last flight they had).
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 1289239480
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 their last flight times.
So SELECT p.id, p.name, f.time FROM People p LEFT JOIN Flights f ON p.id = f.person_id
Returns
1 Dave 1284762115
1 Dave 1284787352
1 Dave 1289239480
2 Becky 1284772629
2 Becky 1286432934
I would like to see just:
1 Dave 1289239480
2 Becky 1286432934
So I need to return only the match with the highest f.id or the highest f.time

SELECT
p.id, p.name, MAX(f.time) AS LastFlight
FROM
People p
LEFT JOIN Flights f ON p.id = f.person_id
GROUP BY
p.id, p.name

SELECT p.id, p.name, f.time FROM People p LEFT JOIN
(select person_id, max(time) time from flights group by person_id) f
ON p.id = f.person_id

Try this:
;with LastFlightTimes as
(
select person_id, max(id) maxid
from Flights f
group by person_id
)
SELECT p.id, p.name, f.time FROM People p
LEFT JOIN LastFlightTimes lft ON p.id = lft.person_id
left join Flights f on f.id = lft.maxid

Related

Join 2 tables where one table may or may not have an entry

i have 2 tables (person + activities)
Person
id
name
1
John
2
Axel
3
William
Activities
activity_id
person_id
activity_type
1
1
Login
2
1
Visited Website
3
1
Logout
4
3
Login
5
3
Logout
As you can see John and William have both several activities. But Axel has no activities at all.
The result i try to achieve is as follows.
I want to select id and name from every entry of the person table and the activity_id and activity_type from the activity table.
If the person has no activities yet, the id and name of the person should still be shown.
And if the person has more then one activity, only the one with the highest id should be shown.
The result i aim for:
id
name
activity_id
activity_type
1
John
3
Logout
2
Axel
null
null
3
William
5
Logout
When i try a left join:
select p.id, p.name, a.activity_id, a.activity_type
from person p left join activity a on p.id = a.person_id
order by p.id
i get this result:
id
name
activity_id
activity_type
1
John
1
Login
1
John
2
Visited Website
1
John
3
Logout
2
Axel
null
null
3
William
4
Login
3
William
5
Logout
But as i only one want one entry per person i added the following where-clause:
select p.id, p.name, a.activity_id, a.activity_type
from person p left join activity a on p.id = a.person_id
where a.id = (select max(id) from activity a2 where a2.person_id = p.id)
order by p.id
This is the result:
id
name
activity_id
activity_type
1
John
3
Logout
3
William
5
Logout
The entry for John and William are as i wanted. Only the 'last' activity is shown.
The problem is Axel is not shown anymore.
Any help appriciated.
Many thanks in advance!
In Postgres the simplest and fastest method is usually to use a Postgres extension to SQL, distinct on:
select distinct on (p.id) p.id, p.name, a.activity_id, a.activity_type
from person p left join
activity a
on p.id = a.person_id
order by p.id, a.activity_id desc;
distinct on returns one row for keys specified in the parentheses. The specific row is determining by the order by.
For performance, I would suggest writing the query like this:
select p.id, p.name, a.activity_id, a.activity_type
from person p left join
(select distinct on (a.person_id) a.*
from activity a
order by a.person_id, a.activity_id desc
) a
on p.id = a.person_id;
And then creating an index on activity(person_id, activity_id desc).
Just another option you could use, which might be more performant with the correct indexes in place, to use a window function to identify the activity per person
select p.id, p.name, a.activity_id, a.activity_type
from person p
left join (
select *, Row_Number() over(partition by person_id order by activity_id desc) rn
from activities
)a on a.person_id=p.id and a.rn=1

Inner join on the same row twice

I have the following tables:
games:
id | tournament | player1 | player 2
1 | 1 | 1 | 2
2 | 1 | 3 | 4
players
id | name
1 | Johnson
2 | Smith
tournaments
id | name
1 | Tournament 1
Now I want to extract all information in the table 'games'.
I've used:
SELECT t.name, g.player1, g.player2
FROM tournaments AS t
INNER JOIN games AS g ON g.tournament = t.id
This works for extracting the information for the tournament - but I'm looking for the same for the players as well. The only solution I could find was after my loop to do another SQL to extract the information from the player:
SELECT * FROM players where id = player1variable
Is this the best solution? Is it possible to include this information in the first SQL?
You can do this by JOINing to the players table twice, once for each player:
SELECT t.name, p1.*, p2.*
FROM tournaments AS t
INNER JOIN games AS g ON g.tournament = t.id
INNER JOIN players AS p1 ON p1.id = g.player1
INNER JOIN players AS p2 ON p2.id = g.player2
Output:
name id name id name
Tournament 1 1 Johnson 2 Smith
Demo on dbfiddle
Note that this will give you columns with the same name, which may cause issues in your application. You can work around this by using column aliases:
SELECT t.name, p1.name AS player1, p2.name AS player2
FROM tournaments AS t
INNER JOIN games AS g ON g.tournament = t.id
INNER JOIN players AS p1 ON p1.id = g.player1
INNER JOIN players AS p2 ON p2.id = g.player2
Output:
name player1 player2
Tournament 1 Johnson Smith
Demo on dbfiddle
Use SubQuery.
SELECT t.name
, g.player1
, (select m.name from players m where m.id = g.player1) as player1_name
, g.player2
, (select m.name from players m where m.id = g.player2) as player2_name
FROM tournaments AS t
INNER JOIN games AS g ON g.tournament = t.id

How to get the count of a particular category for each year?

I am trying out a problem which states me to find " For each year, count the number of movies in that year that had only female actors".
Table schema is as follows:
-------------------- ----------------------- ----------------------
| Movie | | Person | | Cast |
-------------------- ------------------------ ----------------------
| MovieID | year | | PersonID | Gender | | MovieID | PersonID |
-------------------- ------------------------ ----------------------
Running the following query:
SELECT M.YEAR, COUNT(M.MID) NUMBER_OF_FEMALE_ONLY_MOVIES FROM MOVIE M
WHERE M.MID IN (SELECT X.MID FROM (SELECT AX.MID, COUNT(AX.PID) TOTAL_CAST
FROM M_CAST AX GROUP BY AX.MID) X
WHERE
X.TOTAL_CAST = (SELECT COUNT(A.PID) FROM M_CAST A, PERSON B WHERE A.MID =
X.MID AND
TRIM(B.PID) = TRIM(A.PID) AND B.GENDER = 'Female')) GROUP BY M.YEAR
My results are :
---------------------------------------
| year | NUMBER_OF_FEMALE_ONLY_MOVIES |
---------------------------------------
| 1999 | 1 |
| 2005 | 1 |
| 2009 | 1 |
| 2012 | 1 |
| 2018 | 1 |
----------------------------------------
But I need to return 0 as count for the years which do not have any such movies.
Eg.
2013 0
WITH
PERSON_CAST_MERGE AS
(
SELECT P.PID,C.MID,GENDER
FROM PERSON P
INNER JOIN M_CAST C ON C.PID = P.PID
),
MALE_COUNT AS
(
SELECT F.MID FROM PERSON_CAST_MERGE F
WHERE TRIM(F.GENDER) NOT LIKE "%FEMALE%"
),
FEMALE_COUNT AS
(
SELECT F.MID FROM PERSON_CAST_MERGE F
WHERE TRIM(F.GENDER) LIKE "%FEMALE%"
),
ONLY_FEMALE AS
(
SELECT F.MID FROM FEMALE_COUNT F
WHERE F.MID NOT IN (SELECT M.MID FROM MALE_COUNT M)
),
TEST AS
(
SELECT M.YEAR,COUNT(M.MID) AS NO_OF_MOVIES
FROM ONLY_FEMALE F
INNER JOIN MOVIE M ON M.MID = F.MID
GROUP BY M.YEAR
)
SELECT M.YEAR,
CASE
WHEN M.YEAR IN (SELECT F.YEAR FROM TEST F) THEN
(SELECT F.NO_OF_MOVIES FROM TEST F WHERE F.YEAR = M.YEAR)
WHEN M.YEAR <> (SELECT F.YEAR FROM TEST F) THEN
0
END
AS NO_OF_MOVIES
FROM MOVIE M
GROUP BY M.YEAR
I'd suggest exploring the data within the CTE to get a better understanding.
First CTE (all_cast): Return the entire movie cast
Second CTE (male_present): Return movie id's from all_cast where there exists male actors.
Result: Return movies from all_cast where movie id is not present in male_present
WITH all_cast AS (
SELECT SUBSTR(m."year",-4) as 'Year', m.title, trim(m.MID) as MID, p.Name, trim(p.Gender) as Gender
FROM Movie m
JOIN M_Cast mc
ON m.MID = mc.MID
JOIN Person p
ON trim(mc.PID) = p.PID
),
male_present AS (
SELECT year, mid, name
FROM all_cast
WHERE Gender = 'Male'
)
SELECT year, COUNT(DISTINCT mid) as 'All Female Cast'
FROM all_cast a
WHERE NOT EXISTS (SELECT * FROM male_present WHERE a.mid = mid)
GROUP BY year
You need only the group by with subquery as you require reference to the movieids of personids with gender as female in person
SELECT YEAR, COUNT(*) FROM
MOVIE
Where MovieId IN (SELECT MOVIEId
from CAST WHERE PERSONID IN
(Select PersonId from Person Where
Gender ='FEMALE'))
Group by Year
Try this- A DISTINCT MovieID is required as there may have multiple Female casting for a single movie. Distinct will provide the actual count of movies.
SELECT
M.Year,
COUNT(DISTINCT MovieID)
FROM Movie M
INNER JOIN Cast C ON M.MovieID = C.MovieID
INNER JOIN Person P ON C.PersonID = P.PersonID
WHERE P.Gender = 'Female'
GROUP BY M.Year;
I think the problem can be solved by joining all tables and filtering on WHERE clause for female actors. In this case joining tables will also give better performance rather than sub-querying.
Please try the following code:
Select year, count(*)
from movie
join Cast on movie.movieid = cast.movieid
join person on person.personid = cast.personid
where person.gender = 'Female'
group by year
Please let me know if that works fine for you.
By merging your query with the Movie table using the outer left join, you can get the desired results. The time taken will be very low compared to the answer posted by #Lucky
WITH FEMALE_ONLY AS
(SELECT M.YEAR,
COUNT(M.MID) COUNT_ALL_FEMALE
FROM MOVIE M
WHERE M.MID IN
(SELECT Q.MID
FROM
(SELECT MC.MID,
COUNT(MC.PID) total
FROM M_CAST MC
GROUP BY MC.MID) Q
WHERE Q.total =
(SELECT COUNT(A.PID)
FROM M_CAST A,
PERSON B
WHERE A.MID = Q.MID
AND TRIM(B.PID) = TRIM(A.PID)
AND B.Gender = 'Female'))
GROUP BY M.YEAR)
SELECT DISTINCT M.year,
coalesce(FO.COUNT_ALL_FEMALE, 0) FEMALE_ONLY_MOVIES
FROM Movie M
LEFT OUTER JOIN FEMALE_ONLY FO ON M.year = FO.year
ORDER BY M.year;
You can do like this
select z.year, count(*)
from Movie z
where not exists (select *
from Person x, M_Cast xy
where x.PID = xy.PID and xy.MID = z.MID and x.gender!='Female')
group by z.year;

PostgreSQL - Selecting count of unique values in one and two columns

Firstly, I'd like to apologise for the ambiguous title (I promise to revise it once I'm actually aware of the problem I'm trying to solve!)
I have two tables, player and match, which look like the following:
player:
id name
-- ----
1 John
2 James
3 April
4 Jane
5 Katherine
match:
id winner loser
-- ------ -----
1 1 2
2 3 4
Records in the match table represent a match between two players, where the id column is generated by the database, and the values in the winner and loser columns reference the id column in the player table.
I want to run a query which spits out the following:
player.id player.name total_wins total_matches
--------- ----------- ---------- -------------
1 John 1 1
2 James 0 1
3 April 1 1
4 Jane 0 1
5 Katherine 0 0
I currently have a query which retrieves total_wins, but I'm not sure how to get the total_matches count on top of that.
select p.id, p.name, count(m.winner)
from player p left join match m on p.id = m.winner
group by p.id, p.name;
Thanks for your help!
Try
select p.id, p.name,
sum(case when m.winner = p.id then 1 end ) as total_wins,
count(m.id) as total_matches
from player p
left join match m on p.id in ( m.winner, m.loser )
group by p.id, p.name;
One method splits the match match table, so you have a single row for each win and loss. The rest is just a left join and aggregation:
select p.id, p.name, coalesce(sum(win), 0) as win, count(m.id) as total_matches
from player p left join
(select match, winner as id, 1 as win, 0 as loss from match
union all
select match, loser as id, 0 as win, 1 as loss from match
) m
on p.id = m.id
group by p.id, p.name;

How to use UNION with COUNT

I have this table structure:
TABLE: PERSON TABLE: CAR
PersonID PersonID | CarID
------ ---------|---------
1 1 | 51
1 | 52
TABLE: PET TABLE: AGE
PersonID | PetID Person | AgeID
---------|---- -------|----
1 | 81 1 | 20
1 | 82
1 | 81
One person can have many cars and pets, but only one age.
I want to count the number of cars someone has, count the number of pets someone has, and list their age.
This is what I have so far:
select
car.personid as person,
count(car.carid) as cars,
null as pets
from car
where car.personid = 1
group by car.personid
union all
select
pet.personid as person,
null as cars,
count(pet.petid) as pets
from pet
where pet.personid = 1
group by pet.personid
This produces:
Person | Cars | Pets
-------|------|-----
1 | 2 | null
1 | null | 3
But I'd like the results to look like this:
Person | Cars | Pets | Age
-------|------|------|----
1 | 2 | 3 | 20
There's a fiddle here: http://sqlfiddle.com/#!3/f584a/1/0
I'm completely stuck on how to bring the records into one row and add the age column.
SQL Fiddle
Query 1:
SELECT p.PersonID,
( SELECT COUNT(1) FROM CAR c WHERE c.PersonID = p.PersonID ) AS Cars,
( SELECT COUNT(1) FROM PET t WHERE t.PersonID = p.PersonID ) AS Pets,
a.AgeID AS Age
FROM PERSON p
LEFT OUTER JOIN
AGE a
ON ( p.PersonID = a.PersonID )
Results:
| PersonID | Cars | Pets | Age |
|----------|------|------|-----|
| 1 | 2 | 3 | 20 |
Query 2:
WITH numberOfPets AS (
SELECT PersonID,
COUNT(1) AS numberOfPets
FROM PET
GROUP BY PersonID
),
numberOfCars AS (
SELECT PersonID,
COUNT(1) AS numberOfCars
FROM CAR
GROUP BY PersonID
)
SELECT p.PersonID,
COALESCE( numberOfCars, 0 ) AS Cars,
COALESCE( numberOfPets, 0 ) AS Pets,
AgeID AS Age
FROM PERSON p
LEFT OUTER JOIN AGE a ON ( p.PersonID = a.PersonID )
LEFT OUTER JOIN numberOfPets t ON ( p.PersonID = t.PersonID )
LEFT OUTER JOIN numberOfCars c ON ( p.PersonID = c.PersonID )
Results:
| PersonID | Cars | Pets | Age |
|----------|------|------|-----|
| 1 | 2 | 3 | 20 |
Should work with duplicate Petid or duplicate carid
SqlFiddle Demo
WITH person_cte
AS (SELECT *
FROM person),
car_count
AS (SELECT Count(1) AS car,
p.personid
FROM person_cte p
LEFT OUTER JOIN car c
ON p.personid = c.personid
GROUP BY p.personid),
pet_count
AS (SELECT Count(1) AS Pet,
p.personid
FROM person_cte p
LEFT OUTER JOIN pet c
ON p.personid = c.personid
GROUP BY p.personid)
SELECT c.personid,
c.car,
p.pet,
a.ageid
FROM car_count c
INNER JOIN age a
ON c.personid = a.personid
INNER JOIN pet_count p
ON p.personid = c.personid;
If there wont be any duplicates in Carid or Petid then use this
SqlFiddle Demo
SELECT p.personid,
a.ageid,
Count(DISTINCT carid) as carid,
Count(DISTINCT petid) as petid
FROM person p
INNER JOIN age a
ON p.personid = a.personid
LEFT OUTER JOIN car c
ON p.personid = c.personid
LEFT OUTER JOIN pet pe
ON p.personid = pe.personid
GROUP BY p.personid,
a.ageid
One issue I see with most of these responses is that they will only include people who own a car. What if the person doesn't have a vehicle, but has pets? What if they haven't entered their age, yet? You'd lose that metric.
Tie the person table into this as the main requirement. To get the rest of the numbers you could take various approcahes, such as a simple series of left outer joins on the other tables and count their result.
Also note that tagging "ID" at the end of values is a misnomer and considered bad design practice. If it's an age, just call it "age" or "age_value", but not "AgeID". I would also suggest denormalizing your AGE and PERSON tables and make Age (not AgeID) a nullable field.
E.G.
SELECT
PERSON.PersonID,
AgeID AS Age,
CarCount,
PetCount
FROM
#PERSON AS PERSON
LEFT OUTER JOIN AGE AS AGE
ON AGE.PersonID = PERSON.PersonID
LEFT OUTER JOIN
( SELECT PersonID, COUNT( 1 ) AS CarCount FROM CAR GROUP BY PersonID ) AS CAR
ON CAR.PersonID = PERSON.PersonID
LEFT OUTER JOIN
( SELECT PersonID, COUNT( 1 ) AS PetCount FROM PET GROUP BY PersonID ) AS PET
ON PET.PersonID = PERSON.PersonID
You need to be joining on single values, so do your counts within subqueries
select c.PersonID,a.CarID,b.PetID,c.AgeID from (
select person.PersonID, COUNT(car.CarID) as CarID
from Person INNER JOIN Car on Person.PersonID = Car.PersonID
group by Person.PersonID) a
inner join (
select person.PersonID, COUNT(Pet.PetID) as PetID
from Person INNER JOIN Pet on Person.PersonID = Pet.PersonID
group by Person.PersonID) b
on a.PersonID = b.PersonID
inner join (select PersonID,AgeID from Age) c
on a.PersonID = c.PersonID
Another method is
select person,
sum(cars) as cars,
sum(pets) as pets
from
(
select
car.personid as person,
count(car.carid) as cars,
null as pets
from car
where car.personid = 1
group by car.personid
union all
select
pet.personid as person,
null as cars,
count(pet.petid) as pets
from pet
where pet.personid = 1
group by pet.personid
) as t
group by person
Do you want to count distinct number of cars/pets? If so, add a distinct within the count.
select
person.personid as person,
count(car.carid) as cars,
count(pet.petid) as pets
age.ageID
from person
left outer join pet on pet.personid = person.personid
left outer join car on car.personid = person.personid
left outer join age on age.personid = person.personid
where car.personid = 1
group by car.personid, age.ageID;