Complex Query with related tables - Optimal Solution - mysql

The Schema:
I have 3 Tables:
User
Feature
User_has_Feature:
initially all users has no features
Example data:
User:
| id | name |
| 1 | Rex |
| 2 | Job |
Feature:
| id | name |
| 1 | Eat |
| 2 | Walk |
User_has_Feature:
| id | user_id | feature_id | have_feature |
| 1 | 1 | 1 | true |
| 2 | 1 | 1 | true |
| 3 | 2 | 2 | true |
| 4 | 2 | 2 | false |
The questions are:
¿How to get only the records that have all features? (explicitly)
Example:
| user_name | feature_name | feature_status |
| Rex | Eat | true |
| Rex | Walk | true |
How to get records that do not have all the features? (again explicitly)
Example:
| user_name | feature_name | feature_status |
| Job | Eat | true |
| Job | Walk | false |
Some conditions have to be attended
I need the Users list with all features (true or false) in both queries like examples
User have 650k records (for now)
Feature have 45 records (for now)
Is one time query.
The idea is to export the result to a CSV file
Early Solution
thanks to the answers of (#RolandoMySQLDBA, #Tom Ingram, #DRapp) I found a solution:
SELECT u.name, f.name, IF(uhf.status=1,'YES','NO') as status
FROM user u
JOIN user_has_feature uhf ON u.id = uhf.user_id
JOIN feature f ON f.id = uhf.feature_id
JOIN
(
SELECT u.id as id
FROM user u
JOIN user_has_feature uhf ON uhf.user_id = u.id
WHERE uhf.status = 1
GROUP BY u.id
HAVING count(u.id) <= (SELECT COUNT(1) FROM feature)
) as `condition` ON `condition`.id = u.id
ORDER BY u.name, f.id, uhf.status
For get records that do not have all the features and for get all record that have all features change:
WHERE uhf.status = 1 by WHERE uhf.status = 2
HAVING count(u.id) <= (SELECT COUNT(1) FROM feature) by HAVING count(u.id) = (SELECT COUNT(1) FROM feature)
but I want to know if this is an optimal solution?

SELECT
UNF.*,
IF(
(LENGTH(UNF.FeatureList) - LENGTH(REPLACE(UNF.FeatureList,',','')))
= (FC.FeatureCount - 1),'Has All Features','Does Not Have All Features'
) HasAllFeatures
FROM
(SELECT
U.name user_name
GROUP_CONCAT(F.name) Features
FROM
(SELECT user_id,feature_id FROM User_has_Feature
WHERE feature_status = true) UHF
INNER JOIN User U ON UHF.user_id = U.id
INNER JOIN Feature F ON UHF.feature_id = F.id
GROUP BY
U.name
) UNF,
(SELECT COUNT(1) FeatureCount FROM Feature) FC
;
The UNF subquery returns with all users listed in User_has_Feature and a comma-separated list of the features. The column HasAllFeatures is determined by the number of columns in UNF.FeatureList. In your case, there are two features. If the number of commas in UNF.FeatureList is FeatureCount - 1, then the user has all features. Otherwise, user does not have all features.
Here is a better version that shows all users and whether or not they have all, some or no features
SELECT
U.name user_name,
IFNULL(UsersAndFeatures.HasAllFeatures,
'Does Not Have Any Features')
WhatFeaturesDoesThisUserHave
FROM
User U LEFT JOIN
(
SELECT
UHF.user_id id,
IF(
(LENGTH(UHF.FeatureList) - LENGTH(REPLACE(UHF.FeatureList,',','')))
= (FC.FeatureCount - 1),
'Has All Features',
'Does Not Have All Features'
) HasAllFeatures
FROM
(
SELECT user_id,GROUP_CONCAT(Feature.name) FeatureList
FROM User_has_Feature INNER JOIN Feature
ON User_has_Feature.feature_id = Feature.id
GROUP BY user_id
) UHF,
(SELECT COUNT(1) FeatureCount FROM Feature) FC
) UsersAndFeatures
USING (id);

select
u.id,
u.name as User_Name,
f.name as Feature_Name,
uhf.feature_Status
from
( select uhf.user_id,
sum( if( uhf.feature_status, 1, 0 ) ) as UserFeatureCount
from user_has_feature uhf
group by uhf.user_id ) AllUsersWithCounts
join
( select count(*) as AllFeaturesCount
from Feature ) AllFeatures
on AllUsersWithCounts.UserFeatureCount = AllFeatures.AllFeaturesCount
join user u
on AllUsersWithCounts.user_id = u.ID
join user_has_feature uhf
on AllUsersWithCounts.User_id = uhf.user_id
join feature f
on uhf.feature_id = f.id
The above query should get all people that explicitly have ALL features. In order to get those that do NOT have all features, just change the one join from = to <
on AllUsersWithCounts.UserFeatureCount < AllFeatures.AllFeaturesCount

Here's my bash at it
create a view of the general information
CREATE VIEW v_users_have_features AS
SELECT usr.id, usr.name, feature.name, has_feature.status
FROM usr
JOIN has_feature ON usr.id = has_feature.user_id
JOIN feature ON has_feature.feature_id = feature.id;
use the view for other queries
SELECT v_users_have_features.id, v_users_have_features.u_name, v_users_have_features.f_name
FROM v_users_have_features
GROUP BY v_users_have_features.id
HAVING COUNT( v_users_have_features.id ) = (SELECT COUNT( feature.id )
FROM feature
WHERE feature.name = v_users_have_features.f_name )
p.s. you may need to adapt (particularly the latter) to your exact requirements you could also omit creating the view and nest it in the FROM clause like in another answer it just seemed handier to create the view

Count the number of features. Write a query over users that uses a correlated subquery to find all the features a user has and count them. Make the restriction criterion in the top query the equality of that count and the global number of features.
Can MySQL do correlated subqueries? If not, you might need to use a better database.

Related

Mysql join table and select when record doesn't exit in left join

I have a table which list available items, when user click on any item it will insert in added_items table. Now my problem is I want to use join to select all items that any user has not added. My current query doesn't show items if one user has added it.
items
name | type | id | user
-------|------|----|------------------
JAVA | A | 1 | SYSTEM
PHP | A | 2 | SYSTEM
HTML | B | 3 | USER1
added_items
item_id | user
----------|--------------
1 | peter
My query
SELECT it.*
FROM items it
LEFT JOIN added_items ait
ON ait.user = it.user
#on ait.item_id = it.id
WHERE it.type = "A"
AND ait.user IS NULL
The second query I tried
SELECT it.*
FROM items it
LEFT JOIN added_items ait
ON ait.item_id = it.id
WHERE it.type = "a"
AND ait.user != "peter"
Expected result
when current user is peter I want to retrieve only PHP as peter has added JAVA.
But if current user isn't on added_items the retrieve all record.
Add fiddle http://sqlfiddle.com/#!9/3761e40
SELECT *
FROM items
WHERE NOT EXISTS ( SELECT NULL
FROM added_items
WHERE items.id = added_items.item_id
-- AND added_items.user = "peter"
)
-- AND items.type = 'A'
fiddle
You can try:
SELECT it.*
FROM items it
LEFT JOIN added_items ait
ON ait.item_id = it.id
WHERE ait.item_id IS NULL

MYSQL Join Left Condition does not work

I have this example of my problem:
http://sqlfiddle.com/#!9/0cc41/1/0
Description:
There are three tables (events, persons and user). The table persons connects events and user. persons.type_id is the events.id and persons.user_id is user.id. I created two events (id 1 and 2). There are one entry in person for each event.
My Sql:
SELECT events.*,
coalesce(part_person.Accept_Participants_LJ, 0) AS Accept_Participants
FROM events
LEFT JOIN (
SELECT GROUP_CONCAT(CONCAT(part_user.forname, ' ', part_user.surname) SEPARATOR ', ') AS Accept_Participants_LJ,
part_person.type_id
FROM persons AS part_person
LEFT JOIN user AS part_user ON part_user.id = part_person.user_id
WHERE part_person.type = 'event_participant'
) part_person ON events.id = part_person.type_id
GROUP BY events.id
My expectation was:
------------------------
|id|Accept_Participants|
------------------------
|1 | Carl Habicht |
------------------------
|2 | Peter Zwegert |
------------------------
As you can see, the result is:
----------------------------------
|id| Accept_Participants |
----------------------------------
|1 | Carl Habicht, Peter Zwegert|
----------------------------------
|2 | 0 |
----------------------------------
It seems, that he ignores the ON-Condition of the Left Join.
But, where is my mistake?
Group_concat should (almost) always have a group by and group by without any aggregation is not useful
Maybe this is what you need
SELECT events.*,
coalesce(part_person.Accept_Participants_LJ, 0) AS Accept_Participants
FROM events
LEFT JOIN (
SELECT GROUP_CONCAT(CONCAT(part_user.forname, ' ', part_user.surname) SEPARATOR ', ') AS Accept_Participants_LJ,
part_person.type_id
FROM persons AS part_person
LEFT JOIN user AS part_user ON part_user.id = part_person.user_id
WHERE part_person.type = 'event_participant'
group by part_person.type_id
) part_person ON events.id = part_person.type_id
order BY events.id

How can this be done in a single select statement?

I've got these three tables in the DB & I want to select the event_name for a specific userID from t1event given that I know the value of ID from t1user. How can I do this in a single select statement. (I am using mysql).
**t1user**
+----+
| ID |
+----+
**t2userEvent**
+---------+----------+
| userID | eventID |
+---------+----------+
**t1event**
+----------+--------------+
| eventID | event_name |
+----------+--------------+
Use join:
SELECT t1user.ID, t1event.event_name
FROM t1user
JOIN t2userEvent ON t1user.ID = t2userEvent.userID
JOIN t1event ON t1event.eventID = t2userEvent.eventID
WHERE t1user.ID = :user_id
If you want the users who doesn't have events be listed too, then use LEFT JOIN instead.
You could try this:
SELECT A.event_name FROM t1event A INNER JOIN t2userEvent B
ON A.eventID = B.eventID WHERE b.userID = ?
If I understand it correctly you have the userID as parameter?

How would I execute this complex conditional multi-table MySQL join (queries provided)?

Ok I have a few tables tables. I am only showing relevant fields:
items:
----------------------------------------------------------------
name | owner_id | location_id | cab_id | description |
----------------------------------------------------------------
itm_A | 11 | 23 | 100 | Blah |
----------------------------------------------------------------
.
.
.
users:
-------------------------
id | name |
-------------------------
11 | John |
-------------------------
.
.
.
locations
-------------------------
id | name |
-------------------------
23 | Seattle |
-------------------------
.
.
.
cabs
id | location_id | name
-----------------------------------
100 | 23 | Cool |
-----------------------------------
101 | 24 | Cool |
-----------------------------------
102 | 24 |thecab |
-----------------------------------
I am trying to SELECT all items (and their owner info) that are from Seattle OR Denver, but if they are in Seattle they can only be in the cab NAMED Cool and if they are in Denver they can only be in the cab named 'thecab' (not Denver AND cool).
This query doesn't work but I hope it explains what I am trying to accomplish:
SELECT DISTINCT
`item`.`name`,
`item`.`owner_id`,
`item`.`description`,
`user`.`name`,
IF(`loc`.`name` = 'Seattle' AND `cab`.`name` = 'Cool',1,0) AS `cab_test_1`,
IF(`loc`.`name` = 'Denver' AND `cab`.`name` = 'thecab',1,0) AS `cab_test_2`,
FROM `items` AS `item`
LEFT JOIN `users` AS `user` ON `item`.`owner_id` = `user`.`id`
LEFT JOIN `locations` AS `loc` ON `item`.`location_id` = `loc`.`location_id`
LEFT JOIN `cabs` AS `cab` ON `item`.`cab_id` = `cabs`.`id`
WHERE (`loc`.`name` IN ("Seattle","Denver")) AND `cab_test_1` = 1 AND `cab_test_2` = 1
I'd rather get rid of the IFs is possible. It seems inefficent, looks clunky, and is not scalable if I have a lot of location\name pairs
Try this:
SELECT DISTINCT
item.name,
item.owner_id,
item.description,
user.name
FROM items AS item
LEFT JOIN users AS user ON item.owner_id = user.id
LEFT JOIN locations AS loc ON item.location_id = loc.id
LEFT JOIN cabs AS cab ON item.cab_id = cabs.id
WHERE ((loc.name = 'Seattle' AND cab.name = 'Cool')
OR (loc.name = 'Denver' AND cab.name = 'thecab'))
My first thought is to store the pairs of locations and cab names in a separate table. Well not quite a table, but a derived table generated by a subquery.
You still have the problem of pivoting the test results into separate columns. The code can be simplified by making use of mysql boolean expressions, which get rid of the need for a case or if.
So, the approach is to use the same joins you have (although left join is not needed because the comparison on cab.name turns them in to inner joins). Then add a table of the pairs you are looking for, along with the "test name" for the pair. The final step is an explicit group by and a check whether conditions are met for each test:
SELECT i.`name`, i.`owner_id`, i.`description`, u.`name`,
max(pairs.test_name = 'test_1') as cab_test_1,
max(pairs.test_name = 'test_2') as cab_test_2
FROM `items` i LEFT JOIN
`users` u
ON i.`owner_id` = u.`id` LEFT JOIN
`locations` l`
ON i.`location_id` = l.`location_id` left join
`cabs` c
ON i.`cab_id` = c.`id` join
(select 'test_1' as testname, 'Seattle' as loc, 'cool' as cabname union all
select 'test_2', 'Denver', 'thecab'
) pairs
on l.name = pairs.name and
l.cabname = c.name
group by i.`name`, i.`owner_id`, i.`description`, u.`name`;
To add in additional pairs, add them into the pairs table along, and add an appropriate line in the select for the test flag.

MySQL: Creating Multiple Variables with COALESCE

By using COALESCE, I can create a temporary variable called comment_votes like so:
SELECT comments.*, COALESCE(rs_reputations.value, 0) AS comment_votes FROM `comments`
LEFT JOIN rs_reputations ON comments.id = rs_reputations.target_id AND
rs_reputations.target_type = 'Comment' AND rs_reputations.reputation_name =
'comment_votes' AND rs_reputations.active = 1 WHERE (impression_id = 1)
I want to create a second variable called impression_votes in the came query. I attempted to do this with:
SELECT comments.*, COALESCE(rs_reputations.value, 0) AS comment_votes
FROM 'comments'
LEFT JOIN rs_reputations ON
comments.id = rs_reputations.target_id AND
rs_reputations.target_type = 'Comment' AND
rs_reputations.reputation_name = 'comment_votes' AND
rs_reputations.active = 1
SELECT comments.*, COALESCE(rs_reputations.value, 0) AS impression_votes
FROM 'comments'
LEFT JOIN rs_reputations ON
comments.id = rs_reputations.target_id AND
rs_reputations.target_type = 'Comment' AND
rs_reputations.reputation_name = 'impression_votes' AND
rs_reputations.active = 1
WHERE
This leads to the error:
You have an error in your SQL syntax
Is what I'm attempting even possible? If so, I seem to be bridging the two SELECT/COALESCE statements improperly. How should I write this?
The MySQL COALESCE function is actually an inbuilt function that returns the first non-null value - it's not a variable, it's a function that is actually supported across a wide variety of database systems.
For example, with the following table:
| Id | Name | Counter |
| 1 | lolcat | NULL |
| 2 | codez | 1 |
The sql statement:
SELECT Id, Name, COALESCE(counter, 0) AS NonNullCounter FROM table
will return the results:
| Id | Name | NonNullCounter |
| 1 | lolcat | 0 |
| 2 | codez | 1 |
In this instance, the NULL value has been replaced by 0.
This is useful for you as, if you don't yet have any matching rows in rs_reputations for the row in comments, the LEFT JOIN will return NULL for the column rs_repuations.value, which is then replaced by 0 by COALESCE.
If you are new to JOINs then there is a great visual guide by Jeff Atwood.
Your first query can is actually:
SELECT comments.*,
COALESCE(rs_reputations.value, 0) AS comment_votes
FROM comments
LEFT JOIN rs_reputations ON comments.id = rs_reputations.target_id
AND rs_reputations.reputation_name = 'comment_votes'
WHERE impression_id = 1;
CHOICE 1 - UNION
You have a couple of choices - you can either UNION your results together like this:
SELECT comments.*,
COALESCE(rs_reputations.value, 0) AS votes,
'comment_votes' AS vote_type
FROM comments
LEFT JOIN rs_reputations ON comments.id = rs_reputations.target_id
AND rs_reputations.reputation_name = 'comment_votes'
WHERE impression_id = 1
UNION
SELECT comments.*,
COALESCE(rs_reputations.value, 0) AS votes,
'impression_votes' as vote_type
FROM comments
LEFT JOIN rs_reputations ON comments.id = rs_reputations.target_id
AND rs_reputations.reputation_name = 'impression_votes'
WHERE impression_id = 1;
In this instance your results will look like this:
|comments_columns|votes|vote_type |
| * |12 |comment_vote |
| * |2 |impression_vote |
CHOICE 2 - JOIN ON TO THE SAME TABLE TWICE
Or you can self join onto the same table twice by using the same table name but a different alias:
SELECT comments.*,
COALESCE(CommentRep.value, 0) AS comment_votes,
COALESCE(ImpressionRep.value, 0) AS impression_votes,
FROM comments
LEFT JOIN rs_reputations AS CommentRep ON comments.id = CommentRep.target_id
AND CommentRep.reputation_name = 'comment_votes'
LEFT JOIN rs_reputations AS ImpressionRep ON comments.id = ImpressionRep.target_id
AND ImpressionRep.reputation_name = 'impression_votes'
WHERE CommentRep.impression_id = 1
AND ImpressionRep.impression_id = 1
In this instance your results will look like this:
|comments_columns|comment_votes|impression_votes|
| * |12 |0 |
| * |2 |6 |
Finally (phew) the reason you have an error in your original SQL is that you are chaining two SELECT statements together without actually relating them - the SQL doesn't really make sense in this instance as you need to logically relate them (either via a UNION or a repeated join as per above.