Fastest way to select min row with join - mysql

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.

Related

i want to show an one particular blog have how much likes and how much comment using sql query

Blog table:
| bid | btitle |
| 29 | ...... |
| 38 | ...... |
likes table:
| lid | bid |
| 1 | 29 |
| 2 | 29 |
| 3 | 29 |
| 4 | 38 |
| 5 | 38 |
comment table
| commid | bid |
| 1 | 29 |
| 2 | 29 |
| 3 | 38 |
I had tried the following query but that will not work for me:
SELECT blog.bid,blog.btitle,COUNT(likes.lid) AS likecnt,COUNT(comment.comid) AS commentcnt FROM blog,likes,comment WHERE blog.bid=likes.bid AND blog.bid=comment.bid GROUP BY blog.bid
i want output like:
| bid | btitle | likecnt | commentcnt |
| 29 | ...... | 3 | 2 |
| 38 | ...... | 2 | 1 |
You can do left join with separate aggregation :
select b.bid, b.btitle,
coalesce(l.likecnt, 0) as likecnt,
coalesce(c.commentcnt, 0) as commentcnt
from blog b left join
(select l.bid, count(*) as likecnt
from likes l
group by l.bid
) l
on l.bid = b.bid left join
(select c.bid, count(*) as commentcnt
from comment c
group by c.bid
) c
on c.bid = l.bid;
If you want only matching bids the use INNER JOIN instead of LEFT JOIN & remove COALESCE().
Under many circumstances, correlated subqueries may be the fastest solution:
select b.bid, b.btitle,
(select count(*) from likes l where l.bid = b.bid) as num_likes,
(select count(*) from comment c where c.bid = b.bid) as num_comments
from blog b;
When is this a win performance wise. First, you want indexes on likes(bid) and comments(bid). With those indexes, it might be the fastest approach for your query.
It is particularly better if you have a where clause filtering the blogs in the outer query. It only has to do the counts for the blogs in the result set.
Use proper joins and count DISTINCT values because multiple joins increase the number of returned rows:
SELECT b.bid, b.btitle,
COUNT(DISTINCT l.lid) AS likecnt,
COUNT(DISTINCT c.comid) AS commentcnt
FROM blog b
LEFT JOIN likes l ON b.bid = l.bid
LEFT JOIN comment c ON b.bid = c.bid
GROUP BY b.bid, b.btitle
See the demo.
I use LEFT joins just in case there are no comments or likes for a post.
Results:
| bid | btitle | likecnt | commentcnt |
| --- | ------ | ------- | ---------- |
| 29 | ...... | 3 | 2 |
| 38 | ...... | 2 | 1 |

Group 3 tables by max date

A customer can make many inquiries, and an inquiry can have many updates. I'm trying to view each inquiry and the latest update. I'm able to pull the latest date but not the update that's relevant to it. I've seen answers similar to mine but they only seem to deal with one join and I don't understand how I can use those to find a solution.
Here's my sql
select c.name, i.inquirycontent, u.updatecontent, max(u.date) from inquiries i
inner join customers c on c.customerid = i.customerid
left join updates u on u.inquiryid = i.inquiryid
group by i.inquiryid
Even if I omit the customer table, I am still unable to match the latest update to its content. How can I do this?
edit - as requested, here is some sample data
+------------+------+
| customerid | name |
+------------+------+
| 1 | jeff |
+------------+------+
| 2 | anne |
+------------+------+
+-----------+-----------------+------------+
| inquiryid | inquirycontent | customerid |
+-----------+-----------------+------------+
| 1 | inquiry1content | 1 |
+-----------+-----------------+------------+
| 2 | inquiry2content | 1 |
+-----------+-----------------+------------+
| 3 | inquiry3content | 2 |
+-----------+-----------------+------------+
+----------+-----------------+-----------+----------+
| updateid | updatecontent | inquiryid | date |
+----------+-----------------+-----------+----------+
| 1 | update1content | 1 | 01-01-17 |
+----------+-----------------+-----------+----------+
| 2 | update2content | 1 | 03-01-17 |
+----------+-----------------+-----------+----------+
| 3 | update3content | 3 | 04-01-17 |
+----------+-----------------+-----------+----------+
And here's what I want the query to output -
+------+-----------------+----------------+----------+
| name | inquirycontent | latestupdate | date |
+------+-----------------+----------------+----------+
| jeff | inquiry1content | update2content | 03-01-17 |
+------+-----------------+----------------+----------+
| jeff | inquiry2content | NULL | NULL |
+------+-----------------+----------------+----------+
| anne | inquiry3content | update3content | 04-01-17 |
+------+-----------------+----------------+----------+
This is a crude solution using UNION, but it works for what I need. The first select finds all unique inquiries by their latest update, and the second select finds all rows that haven't yet been updated.
(select c.name, i.inquirycontent, u.updatecontent, t2.mxdate
from updates u
inner join inquiries i on i.inquiryid = u.inquiryid
inner join customers c on c.customerid = i.customerid
inner join
(
select max(u.date) mxdate, i.inquiryid
from updates u
left join inquiries i on i.inquiryid = u.inquiryid
group by i.inquiryid
) t2
on u.inquiryid = t2.inquiryid
and u.date = t2.mxdate)
UNION
(select c.name, i.inquirycontent, u.updatecontent, u.date
from inquiries i
left join updates u on u.inquiryid = i.inquiryid
inner join customers c on c.customerid = i.customerid
where u.updatecontent is NULL)

2 Outer Joins from one table

I wanted to learn about web development so I made website with where users can vote on movies, and have issues with making a query for what I need. My tables are as follows:
--rtable--
+-----------+------------+------------+
| movieid | rating | userid |
+-----------+------------+------------+
| 1 | 9 | 27 |
| 2 | 8 | 27 |
| 1 | 10 | 31 |
| 1 | 7 | 42 |
| 2 | 8 | 31 |
+-----------+------------+------------+
--mtable--
+-----------+------------+------------+------------+
| movieid | moviename | movielink | director |
+-----------+------------+------------+------------+
| 1 | Foo | foo.com | bob |
| 2 | Bar | bar.com | steve |
+-----------+------------+------------+------------+
I wanted to make a query to for movie name, movie link, avg(rating), and the users rating (if exists), descending by avg(rating)
--desiredtable (if userid == 42)--
+-----------+------------+------------+------------+
| moviename | movielink | avgrating | yourrating |
+-----------+------------+------------+------------+
| Foo | foo.com | 8.66 | 7 |
| Bar | bar.com | 8 | NULL |
+-----------+------------+------------+------------+
I've managed to get moviename + movielink + avgrating working with OUTER LEFT JOIN but I'm scratching my head as to how to add yourrating. I've tried doubling up on OUTER JOIN and using sub-queries but can't seem to get it to work.
This is what I have so far that works
SELECT mtable.moviename, mtable.movielink, ROUND(AVG(rtable.rating), 2) AS avgrating,
FROM mtable LEFT OUTER JOIN rtable ON rtable.movieid = mtable.movieid GROUP BY mtable.charid ORDER BY AVG(rtable.rating) DESC
You need to join the rtable twice on the mtable, once to get all ratings for the average, once to get the user's rating. You also need to supply the userid for which r2 is filtered within the on clause. That filter criterion in the on clause will apply to r2 only, not the entire dataset.
SELECT mtable.moviename, mtable.movielink, ROUND(AVG(r1.rating), 2) AS avgrating, max(r2.rating) as yourrating
FROM mtable m LEFT OUTER JOIN rtable r1 ON r1.movieid = m.movieid
LEFT JOIN rtable r2 on r2.movieid=m.movieid and r2.userid=...
GROUP BY m.movieid, m.moviename, m.movielink
ORDER BY AVG(r1.rating) DESC

How do I select all the dealers that did not have an order?

I am trying to join two tables and only select the dealers that did not have their promo code used on any order.
How can I do this?
I'm trying this below, but it's not working right. In the example I want to get just Bob, since his promo_code hasn't been used in any orders.
SELECT d.`name`
FROM z_dealer d
LEFT OUTER JOIN z_order o ON (d.promo_code = o.promo_code)
AND o.promo_code IS NULL
Here are my tables...
mysql> select * from z_dealer;
+----+------+------------+
| id | name | promo_code |
+----+------+------------+
| 1 | John | holiday |
| 2 | Suzy | special |
| 3 | Bob | laborday |
+----+------+------------+
mysql> Select * from z_order;
+----+-------+------------+
| id | total | promo_code |
+----+-------+------------+
| 1 | 10 | holiday |
| 2 | 20 | special |
| 3 | 15 | holiday |
| 4 | 45 | special |
+----+-------+------------+
SELECT d.`name` FROM z_dealer d LEFT JOIN z_order o ON (d.promo_code = o.promo_code) WHERE o.promo_code IS NULL
Have you tried INNER JOIN? or You can try IN like this :
SELECT d.name
FROM z_dealer d
WHERE d.promo_code not in( SELECT promo_code FROM z_order)
I'm not entirely sure why it's not working in your example code. I've created the same tables locally and when I run the script you provided I get the single 'Bob' answer.
SELECT d.name
FROM z_dealer d
LEFT OUTER JOIN z_order o ON (d.promo_code = o.promo_code)
AND o.promo_code IS NULL
What results are you seeing exactly?

Mysql join match multiple rows from each table

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