Return unrated rows from a MySQL database - mysql

I have a system where people rate items, and I have two tables, I want to only show the user items they have not rated.
item (i and simplified for this example)
----
item_id, name
1, widget1
2, widget2
I have a rating table, which stores three columns
rating
------
item_id
user_id
rating
So I want to only return results that that user has not yet rated, now I did try this;
psuedo-query
SELECT * FROM item LEFT JOIN rating r ON r.item_id = i.item_id WHERE r.user_id != USER_ID_OF_THE_USER;
However that still returned items that they had rated, as other people had rated the item...
So if I have 100 items in the database, user a has rated 30 and user b has rated 70... then user a should get the 70 items they have to rate, and user b should get the 30 items they havent rated.
My rating table has a compound unique key, so if they rate item_id = 1 once, and rate it again, it just updates the rating value, it doesnt make a new row. One row is inserted for every item that is rated by a user.
This feels like it should be easy and it probably is, but I am stuck.

I'm assuming every user has to rate every item. If so, then you can do this with not exists:
select *
from item i
where not exists (
select 1
from rating r
where i.item_id = r.item_id and r.user_id = ?)
SQL Fiddle Demo
Give the fact you're using mysql, a left join / null check would probably be faster:
select i.*
from item i
left join rating r on i.item_id = r.item_id and r.user_id = ?
where r.user_id is null
More Fiddle
http://explainextended.com/2009/09/18/not-in-vs-not-exists-vs-left-join-is-null-mysql/

Just use an anti-join pattern, like this:
SELECT i.*
FROM item i
LEFT
JOIN rating r
ON r.item_id = i.item_id
AND r.user_id = USER_ID_OF_THE_USER
WHERE r.user_id IS NULL
The outer join returns all rows from item, along with rating by the user. If there is no related row from the rating table, then the values of the columns from rating will be NULL. So all we need is to add a WHERE clause to filter out the rows that had a match.

You can use this query:
SELECT * FROM item i
WHERE NOT EXISTS (
SELECT * FROM rating r
WHERE r.item_id = i.item_id
AND r.user_id = USER_ID_OF_THE_USER);
This will return you those items for which there is no rating of user USER_ID_OF_THE_USER.

Related

List of all products with user's saved products

I'm having an issue trying to figure out a query that will allow me to show a list of all of my product as well as showing whether or not a user has saved any given product.
I have 3 tables involved in the query (users, product_user, product).
I am determing whether or not a user has saved a product by joining the three tables and checking if user_id is null or not with the following query:
SELECT products.*, users.id as 'user_id' FROM products
LEFT JOIN product_user ON products.id = product_user.product_id
LEFT JOIN users ON product_user.user_id = users.id AND users.id =1;
However this returns duplicate rows when the user has saved a product (user_id null version and user_id = 1 version). A distinct statement won't work because the rows aren't distinct in this case. What is best practices to ensure that I only get back distinct products? I need to get back the entire list of products, whether or not the user has saved it.
This is being queried in mysql.
I think this does what you want:
select p.*,
(select pu.user_id
from product_user pu
where pu.product_id = p.id and pu.user_id = 1
limit 1
) as user_id
from products p;
This will return only one row per product. The row will have the user_id -- only once and it has to match whatever you pass in.

MySQL - How to select records that match all IN values but in 1 or more tables

Good day, I can't seem to figure out how to do this. I'll first explain my database model:
User (user_id, name)
Job (job_id, name)
UserTopJob (user_id, job_id)
UserOtherJob(user_id, job_id)
A user can setup his top jobs which he likes best. Those values will be saved into UserTopJob by the user_id and the job_id. The user can set some other jobs he likes into UserOtherJob as well.
Now, what I want to do is query out users that match my job search input.
For example, the search input is job_id 1 and 2.
Now I want to query out the users that match BOTH job_id 1 and job_id 2, but it doesn't matter whether they are in the users top or other jobs, or divided between those two tables.
So a user must be returned if:
Both job_id 1 & 2 are in top jobs
Both job_id 1 & 2 are in the other jobs
They have both job_id 1 and 2 but in different tables
The number of input ids can grow and does not have a limit. It must always match ALL input values.
Edit: So, for example if I'm putting job_ids 1 and 2 and 3 into the query, the ids 1 AND 2 AND 3 need to be in the top or other table for that user.
Can anybody please help me create a MySQL-query that can do this and doesn't put too much pressure on db-performance?
Thanks in advance for helping me out here!
You can use UNION for this type of work.
SELECT user_id AS user FROM UserTopJob where job_id in {job_ids}
UNION
SELECT user_id AS user FROM UserOtherJob where job_id in {job_ids};
Try this query:
SELECT u.*
FROM User u
WHERE NOT EXISTS (
SELECT 1
FROM User u0
JOIN Job j ON j.job_id IN (1,2) -- or other list of job ids
LEFT JOIN UserTopJob utj ON utj.user_id = u0.user_id AND utj.job_id = j.job_id
LEFT JOIN UserOtherJob uoj ON uoj.user_id = u0.user_id AND uoj.job_id = j.job_id
WHERE u0.user_id = u.user_id
AND utj.job_id IS NULL
AND uoj.job_id IS NULL
)
Test in on SQL Fiddle
You can do a JOIN between the tables to get the required result like
select u.name as user_name,
j.name as job_name
from `user` u
INNER join usertopjob utj on u.user_id = utj.user_id
inner join userotherjob uoj on u.user_id = uoj.user_id
inner join job j on j.job_id = utj.job_id or j.job_id = uoj.job_id
where j.job_id in (1,2);
Alright, this was a brain buster this evening. Toying around with this for some time I came up with this and it seems to work.
SELECT user_id, SUM(matched) AS totalMatched FROM
(
SELECT uoj.user_id, COUNT(uoj.job_id) AS matched FROM userOtherJob AS uoj
INNER JOIN user AS u ON u.user_id = uoj.user_id
WHERE uoj.job_id IN (1,2)
GROUP BY u.user_id
UNION ALL
SELECT utj.user_id, COUNT(utj.job_id) AS matched FROM userTopJob AS utj
INNER JOIN user AS u ON u.user_id = utj.user_id
WHERE utj.job_id IN (1,2)
GROUP BY u.user_id
) AS t
GROUP BY user_id
HAVING totalMatched = 2
This query counts the matches in the 'other' table, after that the matches in the 'top' table, and sums the totals of both tables. So, the total number of matches (combined from top and other) must be the same value as the number of jobs we're looking for.

Only get users with a specific joined table count

My SqlFiddle
Seems pretty simple but for the life of me I cannot seem to get the results I want. Keep in mind this is happening on a server that has over 6 million records so it has to be efficient.
I want to get the users who are joined to item table that have status 1 of 3 count or higher only not the users who have status of 1 on an item 2 times has to be >= 3.
SELECT
user.id,
user.name,
item.id as item_id,
itemstatus.item_status,
COUNT(item.status) as status
FROM user
JOIN item ON (user.id = item.user_id)
JOIN itemstatus ON (item.status = itemstatus.id)
WHERE item.status = 1
GROUP BY user.id
My current query above get all with a count. How do I get only the ones that have the 3 or above count. Thanks in advance. I hope I made this clear.
If I understand you correctly, you only want records with COUNT(item.status) greater than or equal to 3, in which case you need to use the HAVING clause after your GROUP BY clause. The HAVING clause is sort of like a WHERE clause for aggregate values.
SELECT
user.id,
user.name,
item.id as item_id,
itemstatus.item_status,
COUNT(item.status) as status
FROM user
JOIN item ON (user.id = item.user_id)
JOIN itemstatus ON (item.status = itemstatus.id)
WHERE item.status = 1
GROUP BY user.id
HAVING COUNT(item.status) >= 3

Correct SELECT/JOIN for "has login user liked"

We're having the following tables:
User (id, name)
Item (id, title, text)
Like (id, itemId, userId)
The Like table stores a has-and-belongs-to-many relationship between Item and User.
What is the most efficient way to select a list from Item and see if the "logged in" user has "liked" that particular Item?
SELECT * FROM Item ORDER BY published DESC LIMIT 10
(+ check if each Item has been liked by known user id, e.g. '123')
Is this done best with a sub-select, join or two individual queries?
The most straight forward way ( if i have understood the question right ) is
select * from Like where itemId = ? and userId = ?;
Assuming you have the itemId you want to check and userId from the session.
It will be good to have a composite index on table Like and columns itemId,userId to have fast returning queries.
Joining is more effective than subselect, database engines are well optimized for joining tables.
The query (I have changed the name of table 'like', because it is a keyword in SQL)
SELECT COUNT(*)
FROM User u
JOIN Likes l ON u.id = l.userID
JOIN Item i ON l.itemid = i.id
WHERE i.title = 'item' AND u.name = 'user'
If the result is greater than 0, the user liked the item.
EDIT:
If you want to display the 10 newest items, and see if the user liked them or no, you can use this query:
SELECT i.id, i.title, IF(l.userID IS NULL, 0, 1) AS Liked
FROM item i
LEFT JOIN Likes l ON i.id = l.itemID AND l.userID = ?
ORDER BY i.published DESC LIMIT 10
It is important to use LEFT JOIN, otherwise you will not get the rows the user has not liked.

Attempting to Join 3 tables in MySQL

I have three tables that are joined. I almost have the solution but there seems to be one small problem going on here. Here is statement:
SELECT items.item,
COUNT(ratings.item_id) AS total,
COUNT(comments.item_id) AS comments,
AVG(ratings.rating) AS rate
FROM `items`
LEFT JOIN ratings ON (ratings.item_id = items.items_id)
LEFT JOIN comments ON (comments.item_id = items.items_id)
WHERE items.cat_id = '{$cat_id}' AND items.spam < 5
GROUP BY items_id ORDER BY TRIM(LEADING 'The ' FROM items.item) ASC;");
I have a table called items, each item has an id called items_id (notice it's plural). I have a table of individual user comments for each item, and one for ratings for each item. (The last two have a corresponding column called 'item_id').
I simply want to count comments and ratings total (per item) separately. With the way my SQL statement is above, they are a total.
note, total is the total of ratings. It's a bad naming scheme I need to fix!
UPDATE: 'total' seems to count ok, but when I add a comment to 'comments' table, the COUNT function affects both 'comments' and 'total' and seems to equal the combined output.
Problem is you're counting results of all 3 tables joined. Try:
SELECT i.item,
r.ratetotal AS total,
c.commtotal AS comments,
r.rateav AS rate
FROM items AS i
LEFT JOIN
(SELECT item_id,
COUNT(item_id) AS ratetotal,
AVG(rating) AS rateav
FROM ratings GROUP BY item_id) AS r
ON r.item_id = i.items_id
LEFT JOIN
(SELECT item_id,
COUNT(item_id) AS commtotal
FROM comments GROUP BY item_id) AS c
ON c.item_id = i.items_id
WHERE i.cat_id = '{$cat_id}' AND i.spam < 5
ORDER BY TRIM(LEADING 'The ' FROM i.item) ASC;");
In this query, we make the subqueries do the counting properly, then send that value to the main query and filter the results.
I'm guessing this is a cardinality issue. Try COUNT(distinct comments.item_id)