Mysql join match multiple rows from each table - mysql

Here is a simplified version of the pertinent part of my DB:
Person:
id
PersonSkills:
person_id
skillname
ability
Position:
id
PositionSkills:
position_id
skillname
ability
A person can have any number of PersonSkills, let's say in this instance, our user has 6.
A Position can require any number of PositionSkills, let's say in this instance the position requires 3.
I need a query that will determine whether ALL of the PositionSkills associated with this Position are present in the PersonSkills associated with this person. (I also need to ensure that the PersonSkill's ability is greater than the PositionSkill's ability, but I think that will be simple once I figure out the part that is giving me trouble here.)
Thanks in advance,
Jason
EDIT
Here is more detail on what I'm looking for:
PersonSkills
+-------------+---------+---------+
| person_id | skill | ability |
+-------------+---------+---------+
| 1 | A | 5 |
| 1 | B | 4 |
| 1 | C | 5 |
| 1 | D | 4 |
| 1 | E | 5 |
+-------------+---------+---------+
PositionSkills
+-------------+---------+---------+
| position_id | skill | ability |
+-------------+---------+---------+
| 5 | A | 3 |
| 5 | B | 3 |
| 5 | C | 3 |
| 6 | A | 3 |
| 6 | B | 3 |
| 6 | Z | 3 |
+-------------+---------+---------+
From the fact that I'm user 1, I want a query to tell me that I am qualified for Position 5, because I have skills A, B, and C, that it requires, but that I am not qualified for Position 6, because I lack skill Z
Thanks again,
Jason

Try this solution:
SELECT
a.id,
(COUNT(c.position_id) = (SELECT COUNT(*) FROM positionskills WHERE position_id = <position_id here>)) AS isQualified
FROM
person a
LEFT JOIN
personskills b ON a.id = b.person_id
LEFT JOIN
positionskills c ON
b.skillname = c.skillname AND
b.ability >= c.ability AND
c.position_id = <position_id here>
GROUP BY
a.id
WHERE
a.id = <person_id here>
If the person is qualified, isQualified will be 1 else it will be 0
EDIT: As per clarification in the question, to get all the positions for which the person is qualified for, use this solution:
SELECT
a.position_id
FROM
(
SELECT bb.position_id, COUNT(*) AS skillshave
FROM personskills aa
INNER JOIN positionskills bb ON aa.skillname = bb.skillname AND aa.ability >= bb.ability
WHERE aa.person_id = <person_id here>
GROUP BY bb.position_id
) a
INNER JOIN
(
SELECT position_id, COUNT(*) AS skillsrequired
FROM positionskills
GROUP BY position_id
) b ON a.position_id = b.position_id AND a.skillshave = b.skillsrequired

Related

Do the people in the table satisfy the following condition

I have the following tables:
Student Table
| id | name | gender|
|----|----------|-------|
| 1 | April | F |
| 2 | Jane | F |
| 3 | Joe | M |
| 4 | Mike | M |
Project Table
| project_id | student_id | project_name|
|------------|------------|-------------|
| 101 | 1 | Alpha |
| 101 | 2 | Alpha |
| 101 | 3 | Alpha |
| 102 | 2 | M |
| 102 | 4 | M |
| 103 | 1 | Beta |
| 103 | 3 | Beta |
Assume there are much more students and project ids.
Multiple students can work in the same project.
My question is, having the tables above, how can I check how many students who worked together on 2 or more projects? So in the example above, students with id 1 and 3 worked together in project Alpha and Beta.
My code so far is
SELECT * FROM student s
JOIN project s ON student.id = project.project_id
I know I want to join both tables by the column they share (which is the student id) but I have no idea what to do after. I am new to SQL barely a week in learning and would appreciate the most help.
Use a self join and aggregation:
select p1.student_id, p2.student_id, count(*) as num_projects
from projects p1 join
projects p2
on p1.project_id = p2.project_id and
p1.student_id < p2.student_id
group by p1.student_id, p2.student_id
having count(*) > 1
order by count(*) desc;
Consider:
select count(*)
from (
select 1
from projects p1
inner join projects p2
on p2.project_id = p1.project_id and p2.student_id < p1.student_id
group by p1.student_id, p2.student_id
having count(*) > 1
) t
The inner query self-joins the project table and generates unique tuples of students that worked on the same project; the having clause filters on tuples that worked together on more than one project.
The outer query just counts the number of tuples.

how to perform an outer join in mysql

I have a table A that contains tree columns, id, users ids and vehicle id. And a table B that contains vehicleid, and vehicle name.
Table A
---------------------------
| Id | User_id |Vehicle_id|
---------------------------
| 1 | 1 | 2 |
| 2 | 1 | 3 |
| 3 | 1 | 4 |
| 4 | 2 | 2 |
| 5 | 2 | 3 |
| 6 | 4 | 5 |
---------------------------
Table B
-------------------
| Id |Vehicle_name|
-------------------
| 1 | Car |
| 2 | Bike |
| 3 | Plane |
| 4 | Boat |
| 5 | Rocket |
-------------------
Given a user id, I need to get all vehicle names, that doesn't match with table A. I've tried Outer joins, but I can't manage to do get the info that i need.
For example: Given user id 1, the query should return Car and Rocket.
thanks in advance
This is simple enough using not in or not exists:
select b.*
from b
where not exists (select 1
from a
where a.vehicle_id = b.id and a.user_id = #a_user_id
);
I also thought of using a cross join and was able to get the output in case you are more comfortable with join logic.
SELECT CJOIN.USER_ID, CJOIN.VEHICLE_ID, CJOIN.VEHICLE_NAME
FROM
(SELECT DISTINCT A.USER_ID, B.ID AS VEHICLE_ID, B.VEHICLE_NAME FROM TABLE_A A CROSS JOIN TABLE_B B) CJOIN
LEFT JOIN
TABLE_A D
ON CJOIN.USER_ID = D.USER_ID AND CJOIN.VEHICLE_ID = D.VEHICLE_ID
WHERE D.USER_ID IS NULL AND D.VEHICLE_ID IS NULL;
First, I got all possible combinations of USER_ID x VEHICLE_ID by a cross join and used this table in a left join to pull records for which there is no match.

Limit this JOIN query so one result for each left-side table is returned?

I have these two tables:
Person:
+----+-------+
| ID | name |
+----+-------+
| 1 | John |
| 2 | Frank |
+----+-------+
Position:
+----+---------+----------+------------+
| ID | name | personID | startDate |
+----+---------+----------+------------+
| 1 | Cashier | 1 | 2013-01-01 |
| 2 | Manager | 1 | 2013-04-23 |
| 3 | Cashier | 2 | 2014-02-01 |
+----+---------+----------+------------+
The Position table tracks various positions that a person has held.
How can I create a listing that shows each person and their current position (which would be whatever position has the latest start date)? Essentially I need to limit the JOIN of the Position table to only return one result.
I tried the following code.
SELECT p.id, h.positionID FROM person p JOIN position h ON p.id = h.personID
Try the following :-
select p.ID, pp.name PersonName, p.name PositionName ,p.startDate
from Position p
inner join
(select personID, max(startDate) sdate from Position group by personID) as a
on p.personID = a.personID and p.startDate = a.sdate
left join Person pp
on pp.ID = p.personID
Yet, it is highly advised that you post the code that you tried.
SQL Fiddle

Fastest way to select min row with join

In this example, I have a listing of users (main_data), a pass list (pass_list) and a corresponding priority to each pass code type (pass_code). The query I am constructing is looking for a list of users and the corresponding pass code type with the lowest priority. The query below works but it just seems like there may be a faster way to construct it I am missing. SQL Fiddle: http://sqlfiddle.com/#!2/2ec8d/2/0 or see below for table details.
SELECT md.first_name, md.last_name, pl.*
FROM main_data md
JOIN pass_list pl on pl.main_data_id = md.id
AND
pl.id =
(
SELECT pl2.id
FROM pass_list pl2
JOIN pass_code pc2 on pl2.pass_code_type = pc2.type
WHERE pl2.main_data_id = md.id
ORDER BY pc2.priority
LIMIT 1
)
Results:
+------------+-----------+----+--------------+----------------+
| first_name | last_name | id | main_data_id | pass_code_type |
+------------+-----------+----+--------------+----------------+
| Bob | Smith | 1 | 1 | S |
| Mary | Vance | 8 | 2 | M |
| Margret | Cough | 5 | 3 | H |
| Mark | Johnson | 9 | 4 | H |
| Tim | Allen | 13 | 5 | M |
+------------+-----------+----+--------------+----------------+
users (main_data)
+----+------------+-----------+
| id | first_name | last_name |
+----+------------+-----------+
| 1 | Bob | Smith |
| 2 | Mary | Vance |
| 3 | Margret | Cough |
| 4 | Mark | Johnson |
| 5 | Tim | Allen |
+----+------------+-----------+
pass list (pass_list)
+----+--------------+----------------+
| id | main_data_id | pass_code_type |
+----+--------------+----------------+
| 1 | 1 | S |
| 3 | 2 | E |
| 4 | 2 | H |
| 5 | 3 | H |
| 7 | 4 | E |
| 8 | 2 | M |
| 9 | 4 | H |
| 10 | 4 | H |
| 11 | 5 | S |
| 12 | 3 | S |
| 13 | 5 | M |
| 14 | 1 | E |
+----+--------------+----------------+
Table which specifies priority (pass_code)
+----+------+----------+
| id | type | priority |
+----+------+----------+
| 1 | M | 1 |
| 2 | H | 2 |
| 3 | S | 3 |
| 4 | E | 4 |
+----+------+----------+
Due to mysql's unique extension to its GROUP BY, it's simple:
SELECT * FROM
(SELECT md.first_name, md.last_name, pl.*
FROM main_data md
JOIN pass_list pl on pl.main_data_id = md.id
ORDER BY pc2.priority) x
GROUP BY md.id
This returns only the first row encountered for each unique value of md.id, so by using an inner query to order the rows before applying the group by you get only the rows you want.
A version that will get the details as required, and should also work across different flavours of SQL
SELECT md.first_name, md.last_name, MinId, pl.main_data_id, pl.pass_code_type
FROM main_data md
INNER JOIN pass_list pl
ON md.id = pl.main_data_id
INNER JOIN pass_code pc
ON pl.pass_code_type = pc.type
INNER JOIN
(
SELECT pl.main_data_id, pl.pass_code_type, Sub0.MinPriority, MIN(pl.id) AS MinId
FROM pass_list pl
INNER JOIN pass_code pc
ON pl.pass_code_type = pc.type
INNER JOIN
(
SELECT main_data_id, MIN(priority) AS MinPriority
FROM pass_list a
INNER JOIN pass_code b
ON a.pass_code_type = b.type
GROUP BY main_data_id
) Sub0
ON pl.main_data_id = Sub0.main_data_id
AND pc.priority = Sub0.MinPriority
GROUP BY pl.main_data_id, pl.pass_code_type, Sub0.MinPriority
) Sub1
ON pl.main_data_id = Sub1.main_data_id
AND pl.id = Sub1.MinId
AND pc.priority = Sub1.MinPriority
ORDER BY pl.main_data_id
This does not rely on the flexibility of MySQLs GROUP BY functionality.
I'm not familiar with the special behavior of MySQL's group by, but my solution for these types of problems is to simply express as where there doesn't exist a row with a lower priority. This is standard SQL so should work on any DB.
select distinct u.id, u.first_name, u.last_name, pl.pass_code_type, pc.id, pc.priority
from main_data u
inner join pass_list pl on pl.main_data_id = u.id
inner join pass_code pc on pc.type = pl.pass_code_type
where not exists (select 1
from pass_list pl2
inner join pass_code pc2 on pc2.type = pl2.pass_code_type
where pl2.main_data_id = u.id and pc2.priority < pc.priority);
How well this performs is going to depend on having the proper indexes (assuming that main_data and pass_list are somewhat large). In this case indexes on the primary (should be automatically created) and foreign keys should be sufficient. There may be other queries that are faster, I would start by comparing this to your query.
Also, I had to add distinct because you have duplicate rows in pass_list (id 9 & 10), but if you ensure that duplicates can't exist (unique index on main_data_id, pass_code_type) then you will save some time by removing the distinct which forces a final sort of the result set. This savings would be more noticeable the larger the result set is.

Mysql join on 3 tables output

I am learning joins and have the following tables.
Student
| ID | NAME |
-------------
| 1 | A |
| 2 | B |
| 3 | C |
| 4 | D |
Pass
| ID | MARKS |
--------------
| 2 | 80 |
| 3 | 75 |
Fail
| ID | MARKS |
--------------
| 1 | 25 |
| 4 | 20 |
The output I want is this:
| NAME | MARKS |
----------------
| B | 80 |
| C | 75 |
| A | 25 |
| D | 20 |
I wrote a query like this:
select s.id,s.name,p.marks from student s
left join pass p on s.id=p.id
left join (select f.marks,f.id from fail f ) as nn on s.id=nn.id
order by marks desc;
The output I got is this:
| id | name | Marks|
--------------------
| 1 | B | 80 |
| 2 | C | 75 |
| 3 | A | Null |
| 4 | D | NUll |
Cant figure out why Null is coming. Any pointers?
You can use CASE statement for that:
SELECT Name,
CASE WHEN P.Marks IS NULL THEN f.Marks ELSE P.Marks END AS Marks
FROM Student s
LEFT JOIN Pass p ON s.ID = p.ID
LEFT JOIN Fail f ON s.ID = f.ID
ORDER BY Marks DESC;
Or you can also use IF statement:
SELECT Name,
IF(P.Marks IS NULL, F.Marks, P.Marks) AS Marks
FROM Student s
LEFT JOIN Pass p ON s.ID = p.ID
LEFT JOIN Fail f ON s.ID = f.ID
ORDER BY Marks DESC;
Output
| NAME | MARKS |
----------------
| B | 80 |
| C | 75 |
| A | 25 |
| D | 20 |
See this SQLFiddle
To learn more about JOINs see: A Visual Explanation of SQL Joins
You select only the passed marks, this is the reason of null-s appears near falied results.
If you want to select the failed marks you can use IF condition
select s.id,s.name,IF(p.marks = null, nn.marks, p.marks) as markss
from student s
left join pass p on s.id=p.id
left join fail nn on s.id=nn.id
order by markss desc;
Or you can use union of the passed and failed results.
select s.id,s.name, u.marks
from student s
left join ( (SELECT * FROM pass) UNION (SELECT * FROM fail) ) as n ON n.id = s.id
order by marks desc;
You need to understand how the different joins work to understand why you receive NULL for the marks column.
Take a look here:A Visual Explanation of SQL Joins
The relevant example for you is:
LEFT OUTER JOIN:
SELECT * FROM TableA
LEFT OUTER JOIN TableB
ON TableA.name = TableB.name
id name id name
-- ---- -- ----
1 Pirate 2 Pirate
2 Monkey null null
3 Ninja 4 Ninja
4 Spaghetti null null
The Null values you received for the marks column are rows that have no match in the left joined tables.
(the left part of the Venn diagram) the values that does have a value are the cross section between the tow groups of the Venn Diagram.
specifics for your example:
select s.id,s.name,p.marks
from student s
left join pass p on s.id=p.id
left join (select f.marks,f.id from fail f ) as nn on s.id=nn.id
order by marks desc;
The output i got is this:
id | name | Marks
-------------------
1 | B | 80
2 | C | 75
3 | A | Null
4 | D | NUll
This will return all student rows when.
students that have a passing gtade will display the grade and thous who don't will display null.
Try the below Query, use COALESCE
select s.id,s.name,COALESCE(p.marks , nn.marks) as marks
from student s
left join pass p on s.id=p.id
left join fail nn on s.id=nn.id
order by marks desc;
SQL Fiddle