2 Outer Joins from one table - mysql

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

Related

SQL create table with interconnected IDs between tables

I have two tables in a database that I would like to combine in a specific way.
Here are the tables:
table: watchhistory
customerid | titleid | rating | date
------------+-----------+--------+------------
1488844 | tt0389605 | 3 | 2005-09-06
1181550 | tt0389605 | 3 | 2004-02-01
1227322 | tt0389605 | 4 | 2004-02-06
786312 | tt0389605 | 3 | 2004-11-16
525356 | tt0389605 | 2 | 2004-07-11
1009622 | tt0389605 | 1 | 2005-01-19
table: media
mediaid | directorid | title | genre | runtime | releasedate
-----------+------------+----------------+----------------------+---------+-------------
tt0090557 | nm0851724 | Round Midnight | [Drama, Music] | 133 | 1986
tt0312296 | nm0146385 | 1 Giant Leap | [Documentary, Music] | 155 | 2002
tt0078721 | nm0001175 | 10 | [Comedy, Romance] | 122 | 1979
tt2170245 | nm3593080 | 10 | [Thriller] | 76 | 2012
tt5282238 | nm6207118 | 10 | [Thriller] | 90 | 2015
tt0312297 | nm0302572 | 10 Attitudes | [Comedy, Drama] | 87 | 2001
I would like to make a table with the following columns:
title (from media) | Views#
I created this query to get the top 10 titleids, meaning the top 10 titles from watchhistory that appear in watchhistory the most times:
SELECT titleid, count(*) as Views FROM watchhistory GROUP BY titleid ORDER BY Views DESC limit 10;
titleid | views
------------+-------
tt7631348 | 1307
tt14627576 | 1065
tt8372506 | 1063
tt5793632 | 1056
tt1403008 | 1053
tt7825602 | 1051
tt6840954 | 1046
tt12780424 | 1042
tt7266106 | 1036
tt6539274 | 1035
The goal is to essentially replace this titleid column (from watchhistory) with the title (from media). I tried using joins between the watchhistory.titleid and media.mediaid with no luck.
What SQL query do I need to get this desired table?
Thanks in advance.
You need to INNER JOIN to your media table on mediaid:
SELECT m.title, count(wh.*) as Views
FROM watchhistory wh
INNER JOIN media m on m.mediaid = wh.titleid
GROUP BY m.mediaid
ORDER BY Views DESC LIMIT 10;
To see what the select and join are doing, you can simplify it:
SELECT m.*, wh.*
FROM watchhistory wh
INNER JOIN media m on m.mediaid = wh.titleid
The result will be a joined 'table' that has the two tables combined on the mediaid/titleid.

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?

How do I get multiple COUNT with multiple JOINS and multiple conditions?

I have SQL (MySQL) that I've can't figure out. The application is using uploaded photos where there are many tagged participants in a photo and there is the possibility to give photos a vote between 1 to 5.
The original query gets all the votes for a photo and orders them by amount of votes and the average of those votes.
Now I need to limit the returned photos by the ones with more than 1 participant. So photos with only 1 participant should not be accounted for.
Simplified schema looks like this.
PHOTOS
----------------------
| id | title |
----------------------
| 1 | Fun stuff |
| 2 | Crazy girls |
| 3 | Single boy |
PHOTO_VOTES
-------------------------------------------
| photo_id | grade | date | user_id |
-------------------------------------------
| 1 | 3 | … | 12 |
| 1 | 3 | … | 12 |
| 2 | 5 | … | 14 |
| 2 | 4 | … | 14 |
| 3 | 4 | … | 15 |
| 3 | 4 | … | 18 |
PHOTO_PARTICIPANTS
-------------------------
| photo_id | user_id |
-------------------------
| 1 | 12 |
| 1 | 21 |
| 1 | 33 |
| 2 | 14 |
| 2 | 33 |
| 3 | 12 |
This is how far I got:
SELECT vote.photo_id,
COUNT(vote.photo_id) AS vote_count,
AVG(vote.grade) AS vote_average,
COUNT(pp.photo_id) AS participant_count
FROM photo_votes vote
LEFT JOIN photos p ON (vote.photo_id = p.id)
LEFT JOIN photo_participants pp ON (pp.photo_id = p.id)
GROUP BY vote.post_id,
HAVING vote_count >= 2
AND vote_average >= 3
AND participant_count > 1
ORDER BY count DESC, average DESC;
Basically what I'm looking for to end up with, excluding the photo with only one participant:
VOTES
-----------------------------------------------------------
| photo_id | vote_count | average | participant_count
-----------------------------------------------------------
| 1 | 2 | 3 | 3
| 2 | 2 | 4.5 | 2
Update
It turned out this is a very inefficient way of trying to do what I want. Gordons answer below did solve the problem, but as soon as I wanted to join fields from the photos table as well, the "cartesian product"-issue became a real problem - it became a very heavy and slow query.
The solution I finally ended up with is adding a cache-field into the photos table keeping track of how many participants are in the photo. In other words I added a 'participant_count' field to 'photos' that is being updated every time a change is made to the participants table. I also run a cron-job regularly to make sure all photos 'participant_count' are properly up-to-date.
First, you don't need left joins for this. But that shouldn't affect the results. The problem is that you have a cartesian product, because you have two 1-n relationships to photos: votes and participants.
The proper way to fix this is by using subqueries:
SELECT pv.photo_id, pv.vote_count, pv.vote_average, pp.participant_count
FROM (SELECT pv.photo_id, count(*) AS vote_count, avg(grade) AS vote_average
FROM photo_votes pv
GROUP BY pv.photo_id
) pv
JOIN
(SELECT pp.photo_id, count(*) AS participant_count
FROM photo_participants p;
GROUP bY pv.photo_id
) pp
ON pv.photo_id = pp.photo_id
WHERE pv.vote_count >= 2 AND
pv.vote_average >= 3 AND
pp.participant_count > 1
ORDER BY pv.vote_count DESC, pv.vote_average DESC;
Note that you don't even need the photos table, because you are not using any fields in it.

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.

select query to calculate number of occurrence as well as total cost

I have one report page which displays summarized data of other report.I have used php and mysqli. Let me explain you in deep.
I have a web application of store, where you can add product details. Using these product details you can generate packaging list report of products. And based on the generated packaging list report I need to generate one other report which contains summarized data of the packaging list.
below are my tables:
product table:
id | name | desc_id | purity | style_no | type | duty
1 | ABC | 1 | 18 | TEST123 | R | 100
2 | XYZ | 2 | 14 | TEST456 | B | 80
3 | DEF | 1 | 14 | TEST122 | R | 80
4 | PQR | 1 | 18 | TEST124 | R | 120
5 | HJK | 3 | 18 | TEST134 | B | 300
Description table:
id | descrip
1 | Gold Diamond Ring
2 | Gold Diamond Pendant
3 | Gold Diamond Earring
packaging_master table
id | name
1 | pkg_1
2 | pkg_2
packging_details table
id | pkg_id | prod_id
1 | 1 | 1
2 | 1 | 2
3 | 1 | 3
4 | 1 | 4
5 | 1 | 5
I have used below query to generate the packaging list report for specific id, which works correctly.
SELECT id, (SELECT descrip FROM description WHERE id = desc_id ) AS descrip,
style_no, type , purity, duty FROM product WHERE id IN ( SELECT prod_id FROM packaging_list_details WHERE pkg_id =1 ) ORDER BY descrip ASC , purity ASC
which displays below result:
id | descrip | style_no | type | purity | duty
1 |Gold Diamond Ring | TEST123 | R | 18 | 100
4 |Gold Diamond Ring | TEST124 | R | 18 | 120
3 |Gold Diamond Ring | TEST122 | R | 14 | 80
2 |Gold Diamond Pendant| TEST456 | B | 14 | 80
5 |Gold Diamond Earring| TEST134 | B | 18 | 300
Now I want summarized data of above result using query.
Like:
id | descrip | purity | qty | duty
1 |Gold Diamond Ring | 18 | 2 | 220
2 |Gold Diamond Ring | 14 | 1 | 80
3 |Gold Diamond Pendant| 14 | 1 | 80
4 |Gold Diamond Earring| 18 | 1 | 300
How can I achieve this?
You need to use the GROUP_BY statement - See MySql docs for more info.
This will translate the query to such
SELECT d.descrip, p.purity, count(p.purity) as qty, sum(p.duty)
FROM product p
INNER JOIN Description d ON p.desc_id = d.id
LEFT OUTER JOIN packaging_details pg on pg.prod_id = p.id
GROUP BY d.descrip, p.purity
ORDER BY d.descrip desc, p.purity desc
You can also use the sub select methodology you were using, but I prefer using joins. INNER JOIN will link both tables so that all their records are returned. OUTER JOIN will return all rows from the tables on the LEFT of the statement and matches them to values from the tables on the RIGHT.
See a full SQL Fiddle sample.
NOTE: I am not sure where you are getting the values for Id in your sample - Are they simply row numbers?
I think you should rewrite your query using JOINs:
SELECT
P.id
,D.descrip
,P.style_no
,P.type
,P.purity
,P.duty
FROM
packaging_list_details PLD
JOIN
product P ON
(P.id = PLD.prod_id)
LEFT JOIN
description D on
(D.desc_id = P.id)
WHERE
(PLID.pkg_id = 1)
That should give you the same result you already have. To get the totals, you can write a new query, similar to the above:
SELECT
P.id
,D.descrip
,P.type
,P.purity
,COUNT(p.id) as total_products
,SUM(P.duty) as total_duty
FROM
packaging_list_details PLD
JOIN
product P ON
(P.id = PLD.prod_id)
LEFT JOIN
description D on
(D.desc_id = P.id)
WHERE
(PLID.pkg_id = 1)
GROUP BY
P.id
,D.descrip
,P.type
,P.purity
The second query gives you the totals you are looking for.