Convert subquery to join - mysql

Is it possible to convert the following subquery to a JOIN, preferably without a derived table?
SELECT * FROM users u
LEFT JOIN user_groups ug ON u.usergroupid=ug.groupid
WHERE
u.userstatus=1 AND
ug.groupstatus=1 AND
ug.grouprank>=(SELECT grouprank FROM user_groups WHERE groupkey='users')
The user_groups table looks like:
+-------------+---------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+---------------------+------+-----+---------+----------------+
| groupid | tinyint(3) unsigned | NO | PRI | NULL | auto_increment |
| groupkey | varchar(8) | NO | | NULL | |
| grouprank | smallint(6) | NO | | NULL | |
| groupstatus | tinyint(1) unsigned | NO | | NULL | |
+-------------+---------------------+------+-----+---------+----------------+
The users table looks like:
+--------------+---------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+---------------------+------+-----+---------+----------------+
| userid | int(10) unsigned | NO | PRI | NULL | auto_increment |
| username | varchar(32) | NO | | NULL | |
| userpassword | varchar(32) | NO | | NULL | |
| usergroupid | tinyint(3) unsigned | NO | | NULL | |
| userstatus | tinyint(1) unsigned | NO | | NULL | |
+--------------+---------------------+------+-----+---------+----------------+

Assuming that exactly one user_groups record has groupkey = 'users' (since otherwise your query is invalid), your query is equivalent to this:
SELECT u.*,
ug1.*
FROM users u
LEFT
JOIN user_groups ug1
ON u.usergroupid = ug1.groupid
LEFT
JOIN user_groups ug2
ON ug2.groupkey = 'users' -- not a real join condition
WHERE u.userstatus = 1
AND ug1.groupstatus = 1
AND ug1.grouprank >= ug2.grouprank
;
But note that the LEFT JOINs actually end up working as INNER JOINs, since your WHERE clause depends on the joins having succeeded. So you probably really want something like this:
SELECT u.*,
ug1.*
FROM users u
LEFT
JOIN user_groups ug1
ON u.usergroupid = ug1.groupid
AND ug1.groupstatus = 1
LEFT
JOIN user_groups ug2
ON ug2.groupkey = 'users'
AND ug1.grouprank >= ug2.grouprank
WHERE u.userstatus = 1
;

I think this is what you're looking for -- give it a test.
SELECT * FROM users u
LEFT JOIN user_groups ug ON u.usergroupid=ug.groupid
JOIN user_groups ug2 ON ug.grouprank >= ug2.grouprank AND ug2.groupkey = 'users'
WHERE
u.userstatus=1 AND
ug.groupstatus=1

Related

Cannot update table by counting fields in related table

I'd like to update followers in profile table by counting the followed_id on follow table.
mysql> explain follow;
+-------------+---------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------------+---------+------+-----+---------+-------+
| id | int(11) | NO | | NULL | |
| followed_id | int(11) | NO | | NULL | |
| follower_id | int(11) | NO | | NULL | |
+-------------+---------+------+-----+---------+-------+
And
mysql> explain profile;
+----------------+---------------+------+-----+-------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------------+---------------+------+-----+-------------------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| user_id | int(10) | NO | MUL | 0 | |
| followers | int(7) | NO | | 0 | |
| following | int(7) | NO | | 0 | |
+----------------+---------------+------+-----+-------------------+----------------+
Here is the query that I came up with:
UPDATE profile A
INNER JOIN (SELECT id,COUNT(*) idcount FROM follow GROUP BY id) as B
ON B.id = A.user_id
SET A.followers = B.idcount
But the query does not work as it should. It adds only 1 when profile has followers.
How can I fix this?
You are currently counting the number of rows for each id value in follow, which is always going to be 1. What you need to do is count the number of follower_id values for each followed_id. Also, as #juergend pointed out, you should use a LEFT JOIN so that you can get 0 values for users with no followers. Change your query to this:
UPDATE profile A
LEFT JOIN (SELECT followed_id, COUNT(DISTINCT follower_id) AS idcount
FROM follow
GROUP BY followed_id) as B ON B.followed_id = A.user_id
SET A.followers = COALESCE(B.idcount, 0)
You can use a similar query to update following:
UPDATE profile A
LEFT JOIN (SELECT follower_id, COUNT(DISTINCT followed_id) AS idcount
FROM follow
GROUP BY follower_id) as B ON B.follower_id = A.user_id
SET A.following = COALESCE(B.idcount, 0)

Writing a SQL query that gets information from 3 different tables

I cant seem to figure out how to write these 2 queries on the tables that I have created. The two queries that I am trying to write are
Find users that have reviewed both shops and restaurants.
Find users that reviewed businesses, but not shops or restaurants.
The tables that I am using are
reviews;
+-------------+---------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------------+---------+------+-----+---------+-------+
| business_id | int(11) | NO | PRI | NULL | |
| user_id | int(11) | NO | PRI | NULL | |
| review_id | int(11) | NO | PRI | NULL | |
| review_date | date | YES | | NULL | |
| star_rating | int(1) | YES | | 1 |
businesses
+--------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+-------+
| business_id | int(11) | NO | PRI | NULL | |
| name | varchar(50) | YES | | NULL | |
| city | varchar(40) | YES | | NULL | |
| state | varchar(20) | YES | | NULL | |
| full_address | varchar(120) | YES | | NULL | |
users;
+------------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+-------------+------+-----+---------+-------+
| user_id | int(11) | NO | PRI | NULL | |
| name | varchar(50) | YES | | NULL | |
| user_since | date | YES | | NULL
explain is_a_restaurant;
+--------------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------------+-------------+------+-----+---------+-------+
| business_id | int(11) | NO | PRI | NULL | |
| cuisine_type | varchar(20) | YES | | NULL | |
| total_seats | int(11) | YES | | 1 | |
+--------------+-------------+------+-----+---------+-------+
explain is_a_shop;
+-------------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------------+-------------+------+-----+---------+-------+
| business_id | int(11) | NO | PRI | NULL | |
| shop_type | varchar(50) | YES | | NULL | |
I honestly dont know where to even start. I know I am going to join the businesses with the business ID but I dont know how I am going to find people who reviewed both shops and restaurants. can anyone help?
EDIT: WHat I have tried
For the first query: Find users that have reviewed both shops and restaurants.
SELECT b.business_id
FROM is_a_shop b
JOIN reviews r
ON r.business_id = b.business_id
JOIN is_a_restaurant k
ON r.business_id = k.business_id;
Here's what the first one should look like: you can change the select statement based on what info you want to see.
SELECT u.name
FROM users u
INNER JOIN reviews rev ON rev.user_id = u.user_ID
WHERE EXISTS
(SELECT *
FROM is_a_shop s
WHERE s.business_id = rev.business_id)
OR EXISTS
(SELECT *
FROM is_a_restaurant r
WHERE r.business_id = rev.business_id)
This basically pulls all the users who wrote reviews where the business_id is found in the is_a_shop table or is_a_restaurant. you should be able to figure out the second query easily from this.
Like most things SQL these queries can be solved in a number of different ways. These solutions should be pretty easy to understand:
Find users that have reviewed both shops and restaurants.
-- solution 1 - using joins:
select u.name
from users u
join (
select r.user_id
from reviews r
join is_a_restaurant i on i.business_id = r.business_id
) resturant_reviews on u.user_id = resturant_reviews.user_id
join (
select r.user_id
from reviews r
join is_a_shop i on i.business_id = r.business_id
) shop_reviews on u.user_id = shop_reviews.user_id
-- solution 2: using exists - probably faster than solution 1
select u.name
from users u
join reviews r on u.user_id = r.user_id
where exists (
select 1 from is_a_restaurant i
where i.business_id = r.business_id
) and exists (
select 1 from is_a_shop i
where i.business_id = r.business_id
)
Find users that reviewed businesses, but not shops or restaurants.
-- solution 1: using not in - probably faster than solution 2
select u.name from users u
join reviews r on u.user_id = r.user_id
where r.business_id not in
(
select business_id from is_a_restaurant
union all
select business_id from is_a_shop
)
-- solution 2: using exists
select u.name from users u
join reviews r on u.user_id = r.user_id
where not exists (
select business_id from is_a_restaurant
where r.business_id = business_id
)
and not exists (
select business_id from is_a_shop
where r.business_id = business_id
)

SQL Left Inner Join (mysql)

I have been using the following query:
I am using two tables: (there are some others mentioned but not needed for this question)
assessment_criteria
+-------------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------------+--------------+------+-----+---------+----------------+
| id | mediumint(9) | NO | PRI | NULL | auto_increment |
| scheme_of_work_id | mediumint(9) | NO | | NULL | |
| level | char(255) | YES | | NULL | |
| criteria | char(255) | NO | | NULL | |
+-------------------+--------------+------+-----+---------+----------------+
criteria_completed
+------------------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------------------+--------------+------+-----+---------+----------------+
| id | mediumint(9) | NO | PRI | NULL | auto_increment |
| student_ID | mediumint(9) | NO | | NULL | |
| assessment_criteria_id | mediumint(9) | NO | | NULL | |
| date_marked | date | NO | | NULL | |
| notes | varchar(255) | YES | | NULL | |
| attainment | varchar(15) | YES | | NULL | |
| effort | varchar(15) | YES | | NULL | |
| marked_by | varchar(20) | NO | | NULL | |
+------------------------+--------------+------+-----+---------+----------------+
I was using a query like this to display a list of assessment criteria that a student HAS NOT completed:
SELECT DISTINCT assessment_criteria.id, assessment_criteria.level, assessment_criteria.criteria FROM assessment_criteria, criteria_completed
WHERE (assessment_criteria.scheme_of_work_id = '17')
AND (assessment_criteria.id NOT IN (SELECT criteria_completed.assessment_criteria_id FROM criteria_completed WHERE (student_ID = '403')))
ORDER BY level;
This query has become incredibly slow to run, I have been trying to make it faster using LEFT JOIN.
SELECT DISTINCT a.id, a.level, a.criteria
FROM assessment_criteria a
LEFT JOIN criteria_completed b
ON a.id = b.assessment_criteria_id
WHERE b.assessment_criteria_id IS NULL
But I am having no success when I try to add in clauses for project and student; ie.
SELECT DISTINCT a.id, a.level, a.criteria
FROM assessment_criteria a
LEFT JOIN criteria_completed b
ON a.id = b.assessment_criteria_id
WHERE b.assessment_criteria_id IS NULL
AND (b.student_ID = '403')
AND (a.scheme_of_work_id = '17');
mysql reports "empty set". I suspect I am referencing these foreign keys incorrectly?
(Just to confirm, you are using b.assessment_criteria_id IS NULL to detect failed joins)
Applying the filters on table b to the WHERE clause will filter out any records where the join has failed, which I believe is the cause of the problem.
You can try moving the b filters into the JOIN condition:
SELECT DISTINCT a.id, a.level, a.criteria
FROM assessment_criteria a
LEFT JOIN criteria_completed b
ON a.id = b.assessment_criteria_id
AND (b.student_ID = 403)
WHERE b.assessment_criteria_id IS NULL
AND (a.scheme_of_work_id = 17);
Although personally, I dislike filtering like this in a JOIN. The alternative would be:
SELECT DISTINCT a.id, a.level, a.criteria
FROM assessment_criteria a
LEFT JOIN criteria_completed b
ON a.id = b.assessment_criteria_id
WHERE (a.scheme_of_work_id = 17)
AND (b.assessment_criteria_id IS NULL OR b.student_ID = 403);

How can I create a MySQL query within a query and concatenate?

So I have the following table, users:
+-----------+------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------+------------------+------+-----+---------+-------+
| uid | int(10) unsigned | NO | PRI | 0 | |
| name | varchar(60) | NO | UNI | | |
---------------------------------------------------------------
And the table, roles:
+-------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+------------------+------+-----+---------+----------------+
| rid | int(10) unsigned | NO | PRI | NULL | auto_increment |
| name | varchar(64) | NO | UNI | | |
+-------+------------------+------+-----+---------+----------------+
I have a third table, users_roles, which in a one-to-many relationship maps users to one or more roles:
+-------+------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+------------------+------+-----+---------+-------+
| uid | int(10) unsigned | NO | PRI | 0 | |
| rid | int(10) unsigned | NO | PRI | 0 | |
+-------+------------------+------+-----+---------+-------+
Finally, I have a fourth table, memberships, that maps one-to-one with users:
+-------------------+---------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------------------+---------------+------+-----+---------+-------+
| membership_no | varchar(32) | NO | PRI | NULL | |
| uid | int(11) | YES | | NULL | |
--------------------------------------------------------------------
Currently I have a nice and simple query that provides a list of users with their membership number:
SELECT u.uid, u.name, m.membership_no FROM users AS u
LEFT JOIN memberships AS m ON u.uid = m.uid;
But now I would like to add an additional column to my query that looks up all the users_roles entries once per user, and then concatenates each roles.name in one column, so I can see all the roles for that user. An example output might be:
uid name membership no roles
--- ---- ----- ------
1 foo 123432 admin, normal user, student
2 bar baz 235235 admin
3 bak 2352352 normal user, student
So my task is how I would integrate this additional query on the users_roles table, link user_roles.rid to roles.rid and concatenate the data in a column. Any pointers?
You're looking for GROUP_CONCAT. First JOIN the other tables, so you can get the role names, then GROUP them together by user id.
SELECT u.uid, u.name, m.membership_no, GROUP_CONCAT(r.name ORDER BY r.rid) AS roles
FROM users AS u
LEFT JOIN memberships AS m ON u.uid = m.uid
JOIN users_roles AS ur ON u.uid = ur.uid
JOIN roles AS r ON ur.rid = r.rid
GROUP BY u.uid
DEMO: http://sqlfiddle.com/#!2/ffa31/1
Use MySQL GROUP_CONCAT function.
SELECT
u.uid,
u.name,
m.membership_no,
(SELECT GROUP_CONCAT(name) FROM users_roles ur INNER JOIN roles r ON r.rid = ur.rid WHERE ur.uid = u.uid) as roles
FROM users AS u
LEFT JOIN memberships AS m ON u.uid = m.uid;

Counting many to many association records

I have many to many association between words and definitions.
words:
+-----------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
+-----------------+--------------+------+-----+---------+----------------+
definitions:
+-------------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| language_id | int(11) | YES | MUL | NULL | |
+-------------------+--------------+------+-----+---------+----------------+
definitions_words:
+---------------+---------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------+---------+------+-----+---------+-------+
| definition_id | int(11) | NO | PRI | NULL | |
| word_id | int(11) | NO | PRI | NULL | |
+---------------+---------+------+-----+---------+-------+
I would like to get all word records which have exactly one definition with language_id = 1.
I think the simplest way to express this in SQL is using in:
select *
from words
where id in (select word_id
from word_definitions
where language_id = 1
having count(*) = 1
)
However, in with a subquery does not always work efficiently in MySQL. It can be replaced with an exists clause:
select *
from words w
where exists (select 1
from word_definitions wd
where language_id = 1
having count(*) = 1 and wd.word_id = w.id
)
SELECT a.ID, COUNT(*) totalRecordCount
FROM words a
INNER JOIN definition_words b
ON a.ID = b.word_ID
INNER JOIN definitions c
ON b.definition_id = c.ID
INNER JOIN
(
SELECT id,
SUM(language = 1) totalCount
FROM definitions
GROUP BY id
) d ON c.ID = d.ID AND
d.TotalCount = 1
GROUP BY a.ID