I have three tables which I need to join: User, Notification and UserNotification. The latter is just a cross reference table between User and Notificaion with columns UserID (fk to User) and LastReadNotificationID (fk to Notification). Table UserNotification should contain a reference to the last read Notification in a user-notification feature in a web app. So if we have two records in Notification with ID 1 and 2, and one record in UserNotification with a fk to Notification = 1, it means that the user has NOT read the last created notification which I want to display at the next login.
Now, I need to select all columns from the User table at login and add another column (Notify) to the resultset. Notify should be a boolean that should be false if:
Notification is empty
or Notification contains a record with eg ID = 10 AND UserNotification does have the corresponding foreign key.
Notify should be true if:
Notification contains a record AND UserNotification is empty.
or Notification contains a record with eg ID = 10 BUT UserNotification does NOT have the corresponding foreign key.
The problem is that I can't write a query that meets all the above requirements. The query I have at the moment works except when Notification is empty (and thus is UserNotification). In this case my query returns Notify = true;
If have tried many different ways to solve (left joins, right joins, if, case when, ifnull etc) this but I'm stuck. Please help.
The query I use now:
SELECT ID, FirstName, LastName, Email, Password, Roles, LastLoginTime, LoginCount, Active,
(SELECT IFNULL((SELECT 0 FROM UserNotification UN, User U
WHERE UN.UserId = U.ID AND U.Email = :email
AND UN.LastReadNotificationID <=> (SELECT MAX(ID) FROM Notification WHERE Display = 1)), 1)) AS Notify
FROM User WHERE Email = :email;
The 3 tables:
CREATE TABLE `User` (
`ID` bigint(20) NOT NULL AUTO_INCREMENT,
`FirstName` varchar(50) CHARACTER SET utf8 DEFAULT NULL,
`LastName` varchar(50) CHARACTER SET utf8 DEFAULT NULL,
`Email` varchar(50) CHARACTER SET utf8 DEFAULT NULL,
`Password` varchar(200) CHARACTER SET utf8 DEFAULT NULL,
`Roles` varchar(200) CHARACTER SET utf8 DEFAULT NULL,
`LastLoginTime` varchar(50) CHARACTER SET utf8 DEFAULT NULL,
`LoginCount` int(11) DEFAULT NULL,
`Active` bit(1) NOT NULL DEFAULT b'1',
PRIMARY KEY (`ID`),
UNIQUE KEY `Email` (`Email`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=latin1;
CREATE TABLE `UserNotification` (
`UserID` bigint(20) NOT NULL,
`LastReadNotificationID` int(11) NOT NULL,
UNIQUE KEY `UserID_UNIQUE` (`UserID`),
KEY `fk_UserNotification_Notification_ID` (`LastReadNotificationID`),
CONSTRAINT `fk_UserNotification_Notification_ID` FOREIGN KEY (`LastReadNotificationID`) REFERENCES `Notification` (`ID`) ON DELETE CASCADE,
CONSTRAINT `fk_UserNotification_User_ID` FOREIGN KEY (`UserID`) REFERENCES `User` (`ID`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `Notification` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`Text` text NOT NULL,
`Created` timestamp NOT NULL,
`UserID` bigint(20) NOT NULL,
`Display` bit(1) DEFAULT NULL,
PRIMARY KEY (`ID`),
KEY `fk_Notification_User_ID` (`UserID`),
CONSTRAINT `fk_Notification_User_ID` FOREIGN KEY (`UserID`) REFERENCES `User` (`ID`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1;
The problem with your attempts probably boils down to one basic issue: you want to chain the three tables together with left joins (A -> B -> C) and yet you also want to be able to infer whether there are any other rows in the table that fall outside of the join logic and the pairs of IDs.
B and C can both be empty which is why it's natural to approach this with left joins. (B's "emptiness" is per user, of course.) The problem is that B sits in the middle and it can be empty even if C is not. But when B is empty you can't determine anything conclusive about C just from the results of the join.
select
u.ID as UserID, FirstName, LastName, Email, Password,
Roles, LastLoginTime, LoginCount, Active,
case
when max_n.IsEmpty = 1 or un.LastReadNotification = max_n.ID then 0
-- the following is equivalent and eliminates the IsEmpty flag entirely
-- when coalesce(max_n.ID, 0) = coalesce(un.LastReadNotification, 0) then 1
else 1 -- isn't it sufficient to just return 1 at this point?
-- when un.LastReadNotification is null then 1 -- notification wasn't empty btw
-- when un.LastReadNotification < agg_n.MaxID then 1 -- can't be greater, right?
end as Notify
from
User u
left outer join UserNotification un
on un.UserID = u.ID
cross join (
select
case when max(ID) is not null then 0 else 1 end as IsEmpty,
max(ID) as ID
from Notification
) max_n
You may prefer a subquery over the cross join:
select
u.ID as UserID, FirstName, LastName, Email, Password,
Roles, LastLoginTime, LoginCount, Active,
case
when coalesce((select max(ID) from Notification), 0)
= coalesce(LastReadNotification, 0)
else 1
end as Notify
from User u left outer join UserNotification un on un.UserID = u.ID
Ultimately all you really needed to know is whether there is a notification with ID greater than the ID last seen by each user. Grabbing just the highest notification ID is enough information to let you make that determination across the board.
Here's another thought though: would it possibly be easier to just have LastReadNotification in the user table with an initial value of 0 along with a dummy row with ID 0 in the "empty" notification table? Essentially you remove the need to know anything about emptiness at all. If you implemented the 0 row you'd just end up with something like this:
select
ID as UserID, FirstName, LastName, Email, Password,
Roles, LastLoginTime, LoginCount, Active,
case when exists (
select 1
from Notification n
where n.ID > u.LastReadNotification
) then 1 else 0 end as NotifyA,
(
select sign(count(*))
from Notification n
where n.ID > u.LastReadNotification
) as NotifyB
from User
Please expand on your desired results. Do you want the actual last entered notification that the user has not read only? Or do you just want to know if you should notify the user?
SELECT
u.Id
,u.FirstName
,u.LastName
,u.Email
,u.Password
,u.Roles
,u.LastLoginTime
,u.LoginCount
,u.Active
,CASE WHEN un.UserId IS NULL THEN 1 Notify
FROM
User u
CROSS JOIN Notification n
LEFT JOIN UserNotification un
ON u.ID = un.UserId
AND n.Id = un.LastReadNotificationId
WHERE
U.Email = :email
AND n.Display = 1
;
This query will give you every combination possible for Users to Notifications and then join the UserNotifications table if a record exists. It will specify to notify but for all Notifications that the user has not read. so user will be repeated by all Notifications.
You can then limit to notify and specify the latest Notification the user has not read by using a case statement and group by.
SELECT
u.Id
,u.FirstName
,u.LastName
,u.Email
,u.Password
,u.Roles
,u.LastLoginTime
,u.LoginCount
,u.Active
,CASE
WHEN
SUM(CASE
WHEN un.UserId IS NULL THEN 1
ELSE 0
END) > 0 THEN 1
ELSE 0
END AS Notify
,MAX(CASE WHEN un.UserId IS NULL THEN n.ID END) AS NextNotification
FROM
User u
CROSS JOIN Notification n
LEFT JOIN UserNotification un
ON u.ID = un.UserId
AND n.Id = un.LastReadNotificationId
WHERE
U.Email = :email
AND n.Display = 1
GROUP BY
u.Id
,u.FirstName
,u.LastName
,u.Email
,u.Password
,u.Roles
,u.LastLoginTime
,u.LoginCount
,u.Active
If the user has read all notification the NextNotifcation will be NULL as MAX ignores null values and the case statement doesn't specify and else therefore the value would be null.
If you just want to know if you should notify the user just ignore/remove the NextNotifcation column.
Another way to do this which would be similar to the way you were proceeding would be:
SELECT
u.Id
,u.FirstName
,u.LastName
,u.Email
,u.Password
,u.Roles
,u.LastLoginTime
,u.LoginCount
,u.Active
,CASE
WHEN EXISTS
(SELECT
*
FROM
Notifications n
LEFT JOIN UserNotifications un
ON n.Id = un.LastReadNotificationId
AND un.UserId = u.Id
WHERE
n.Display = 1
)
THEN 1
ELSE 0
END AS Notify
FROM
Users u
WHERE
u.Email = :email
;
Normally I don't like putting a sub select in a column definition but looking at your datatype of BIGINT for users it may actually be better performance to use EXISTS and not the cross join.
Related
I am implementing a simple follow/followers system in MySQL. So far I have three tables that look like:
CREATE TABLE IF NOT EXISTS `User` (
`user_id` INT AUTO_INCREMENT PRIMARY KEY,
`username` varchar(40) NOT NULL ,
`pswd` varchar(255) NOT NULL,,
`email` varchar(255) NOT NULL ,
`first_name` varchar(40) NOT NULL ,
`last_name` varchar(40) NOT NULL,
CONSTRAINT uc_username_email UNIQUE (username , email)
);
-- Using a middle table for users to follow others on a many-to-many base
CREATE TABLE Following (
follower_id INT(6) NOT NULL,
following_id INT(6) NOT NULL,
KEY (`follower_id`),
KEY (`following_id`)
)
CREATE TABLE IF NOT EXISTS `Tweet` (
`tweet_id` INT AUTO_INCREMENT PRIMARY KEY,
`text` varchar(280) NOT NULL ,
-- I chose varchar vs TEXT as the latter is not stored in the database server’s memory.
-- By querying text data MySQL has to read from it from the disk, much slower in comparison with VARCHAR.
`publication_date` DATETIME NOT NULL,,
`username` varchar(40),
FOREIGN KEY (`username`) REFERENCES `user`(`username`)
ON DELETE CASCADE
);
Lets say I want to write a query that returns the 10 latest tweets by users followed by the user with username "Tom". What is the best way to writhe that query and return results with username, first name, last name, text and publication date.
Also if one minute later I want to query again 10 latest tweets and assuming someone Tom follows tweets during that minute, how do I query the database to not select tweets that have already shown in the first query?
To answer your first question:
SELECT u1.username, u1.first_name, u1.last_name, t.text, t.publication_date
FROM Tweet t
JOIN User u1 ON t.username = u1.username
JOIN Following f ON f.following_id = u1.user_id
JOIN User u2 ON u2.user_id = f.follower_id
WHERE u2.username = 'Tom'
ORDER BY t.publication_date DESC
LIMIT 10
For the second part, simply take the tweet_id from the first row of the first query (so the latest tweet_id value) and use it in the WHERE clause for the next query i.e.
WHERE u2.username = 'Tom'
AND t.tweet_id > <value from previous query>
To get latest 10 tweets for Tom:
select flg.username, flg.first_name, flg.last_name, t.tweet_id, t.text, t.publication_date
from user flr
inner join following f on f.follower_id = flr.user_id
inner join user flg on flg.user_id = f.following_id
inner join tweet t on t.username = flg.username
where flr.username = 'Tom'
order by tweet_id desc
limit 10
To get the next 10 tweets, pass in the max tweet_id, and apply an additional condition in the where clause:
where flr.username = 'Tom'
and t.tweet_id > <previous_max_tweet_id>
I am trying to list all competitions in a table, whether a user has entered each competition, and the total number of entries for each competition.
Here are the tables:
CREATE TABLE `competition` (
`competitionID` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` char(255) NOT NULL DEFAULT '',
`description` varchar(750) NOT NULL DEFAULT '',
`startDate` date DEFAULT NULL,
`endDate` date DEFAULT NULL,
`isLive` tinyint(1) NOT NULL,
PRIMARY KEY (`competitionID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `competition` (`competitionID`, `name`, `description`,
`startDate`, `endDate`, `isLive`)
VALUES
(1,'Win a car','Win a really cool car!','2018-04-01 09:30:27','2019-04-01 09:30:27',1),
(2,'Another competition','Win something even better!','2018-04-01 09:30:27','2019-04-01 09:30:27',1);
CREATE TABLE `competition_entrant` (
`competitionEntrantID` int(11) NOT NULL AUTO_INCREMENT,
`userID` int(11) NOT NULL,
`competitionID` int(11) NOT NULL,
PRIMARY KEY (`competitionEntrantID`),
UNIQUE KEY `userID` (`userID`,`competitionID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `competition_entrant` (`competitionEntrantID`, `userID`,
`competitionID`)
VALUES
(1,1,1),
(2,1,2),
(3,2,1);
So in this example user with id 1 has entered both competitions and user with id 2 has entered competition with id 1.
Here is my query.
SELECT
`c`.`name`,
COUNT(`ce1`.`userID`) AS 'hasEnteredCompetition',
COUNT(`ce2`.`userID`) AS 'totalEntries'
FROM competition c
LEFT JOIN `competition_entrant` `ce1` ON `c`.`competitionID` =
`ce1`.`competitionID`
AND `ce1`.`userID` = 2
LEFT JOIN `competition_entrant` `ce2` ON `c`.`competitionID` =
`ce2`.`competitionID`
GROUP BY (c.competitionID);
The problem is that hasEnteredCompetition is showing the total number of entries rather than just 1 for the user entered i.e. the count for that user.
Can anyone tell me what I'm doing wrong here?
You are joining to the competition_entrant table twice, so the user "2" entry is being pulled twice. You can see it this way:
SELECT C.COMPETITIONID,C.NAME,CE1.USERID,CE1.COMPETITIONID
FROM COMPETITION C
LEFT JOIN COMPETITION_ENTRANT CE1 ON C.COMPETITIONID = CE1.COMPETITIONID AND CE1.USERID = 2
LEFT JOIN COMPETITION_ENTRANT CE2 ON C.COMPETITIONID = CE2.COMPETITIONID
1 Win a car 2 1
2 Another competition null null
1 Win a car 2 1
You could add a count distinct to your query like this:
select C.NAME,C.COMPETITIONID,
COUNT(DISTINCT CE1.USERID) as "hasEnteredCompetition",
COUNT(CE2.USERID) as "totalEntries"
from COMPETITION C
left join COMPETITION_ENTRANT CE1 on C.COMPETITIONID = CE1.COMPETITIONIDand CE1.USERID = 2
left join COMPETITION_ENTRANT CE2 on C.COMPETITIONID = CE2.COMPETITIONID
group by (C.NAME,C.COMPETITIONID)
If I understand you correctly (a "expected result" would be nice) you only need to list all competitions, the number of users that entered and if anyone entered at all, right? Then you do not need the second left join, you could go with something like this:
select
competition.competitionID,
competition.name,
case when count(competition.competitionID) > 0 THEN 'yes' ELSE 'no' END AS hasEnteredCompetition,
count(competition.competitionID) AS 'totalEntries'
from competition
left join competition_entrant ON competition.competitionID = competition_entrant.competitionID
group by competitionId, name
I have a user-settings setup with a so called 'property bag' I guess.
I want to store settings for my users. Most users won't change the default setting, so I thought I should make a 'default value' for each setting. This way I don't have store a user_setting record for each setting for each user.
This is my (simplified) mysql database:
CREATE TABLE `user` (
`user_id` INT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(45) NOT NULL,
PRIMARY KEY (`user_id`)
);
CREATE TABLE `setting` (
`setting_id` INT NOT NULL AUTO_INCREMENT,
`key` VARCHAR(100) NOT NULL,
`default_value` VARCHAR(100) NOT NULL,
PRIMARY KEY (`setting_id`)
);
CREATE TABLE `user_setting` (
`user_setting_id` INT NOT NULL AUTO_INCREMENT,
`user_id` INT NOT NULL,
`setting_id` INT NOT NULL,
`value` VARCHAR(100) NOT NULL,
PRIMARY KEY (`user_setting_id`),
CONSTRAINT `fk_user_setting_1`
FOREIGN KEY (`user_id`)
REFERENCES `user` (`user_id`)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT `fk_user_setting_2`
FOREIGN KEY (`setting_id`)
REFERENCES `setting` (`setting_id`)
ON DELETE RESTRICT
ON UPDATE CASCADE
);
INSERT INTO `user` VALUES (1, 'username1'),(2, 'username2');
INSERT INTO `setting` VALUES (1, 'key1', 'somevalue'),(2, 'key2', 'someothervalue');
In my code I can easy do a lookup for each setting for each user. By checking if there is a row in the user_setting table, I know that this is other then the default value.
But is there a way to get an overview for all the settings for each user? Normaly I would left-join the user -> user_setting -> setting tables for each user, but now I don't have a user_setting record for each user/setting. Is this possible with a single query?
If you just had a cartesian join of user against setting, you'll get one row for every user/setting combination. Then simply left join the user_setting table and you can pick up the overridden value when it exists.
So something like this:
SELECT u.user_id, s.key, s.default_value, us.value
FROM user u, setting s
LEFT JOIN user_setting us
ON(us.user_id=u.user_id AND us.setting_id=s.setting_id)
ORDER BY u.user_id, s.key
You could refine this further using IFNULL so that you get the value of the setting regardless of whether it's overridden or default:
SELECT u.user_id, s.key, IFNULL(us.value , s.default_value) AS value
FROM user u, setting s
LEFT JOIN user_setting us
ON(us.user_id=u.user_id AND us.setting_id=s.setting_id)
ORDER BY u.user_id, s.key
(Answering my own question isn't the way I normaly work, but I'm not sure if this is the correct answer and it's based on Paul Dixon's answer)
As mentioned, a cartesian join is needed between user and setting. The correct query would be:
SELECT u.user_id, s.key, IFNULL(us.value , s.default_value) AS value
FROM user u
CROSS JOIN setting s
LEFT JOIN user_setting us ON
(us.user_id=u.user_id AND us.setting_id=s.setting_id)
ORDER BY u.user_id, s.key;
I'm setting up a chat-service.
The table for my messages is created as followed:
CREATE TABLE message (
fromId int NOT NULL,
toId int NOT NULL,
message text NOT NULL,
timestamp timestamp NOT NULL,
toRead int NOT NULL,
FOREIGN KEY (fromId) REFERENCES users(id) ON DELETE SET NULL,
FOREIGN KEY (toId) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
As a user I should get all other users I already chatted with. Therefore I create an sql-statement:
SELECT u.name, u.surname, u.id, m.toRead, m.timestamp, m.fromId, m.toId
FROM message m, user_informations u
WHERE (m.toId = '2' AND m.fromId = u.id) OR (m.fromId = '2' AND m.toId = u.id)
GROUP BY concat(m.fromId, m.toId OR m.toId, m.fromId)
ORDER BY m.timestamp LIMIT 10;
It works as long as one users only writes the other. If both have written each other I get two returned columns
'Peter','Tester','1','0','2015-07-27 16:10:11','1','2'
'Peter','Tester','1','0','2015-07-27 17:14:22','2','1'
because fromId in first result is other than in second and same with toId.
How can I get the statement to return only one row?
What I am trying to do is create a comments section for a website,
The comments consist of a user's name, email and comment. I store this data in the 'comments' table
CREATE TABLE `comments` (
`commentid` int(5) NOT NULL auto_increment,
`user` varchar(40) NOT NULL default '',
`email` varchar(100) NOT NULL default '',
`comment` text NOT NULL,
PRIMARY KEY (`commentid`)
)
What i want to do is execute a query that grabs all this data but also checks the email address in the 'users' table to see if it exists. If it does, grab the avatar from the 'misc' table. If the email doesn't exist in the 'users' table, it's just left blank.
At the moment with the query i tried, it only grabs the data from the 3 tables if the email exists in the 'users' table. I have another comment which as anonymous user left but that's not getting grabbed by the query.
CREATE TABLE `users` (
`userid` int(25) NOT NULL auto_increment,
`email` varchar(255) NOT NULL default '',
`username` varchar(25) NOT NULL default '',
PRIMARY KEY (`userid`)
)
CREATE TABLE `misc` (
`miscid` int(4) NOT NULL auto_increment,
`userid` varchar(3) NOT NULL default '',
`avatar` varchar(100) NOT NULL default '',
PRIMARY KEY (`miscid`)
)
I am pretty sure i need a nested select as a column name so that if there is an email it displays there...if not it's left blank.
EDIT:
Made the table structures how it should be.
This is a query I have just tried but it only displays a row which has an email address. there should be another without email address
SELECT c.comment, c.user, av.avatar
FROM comments c
INNER JOIN users u ON c.email = u.email
LEFT OUTER JOIN (
SELECT userid, avatar
FROM misc
) AS av ON av.userid = u.userid
If I correctly understood your issue, the problem is that you are using an INNER JOIN between comments and users, which means that it will only return matching rows on email. Thus the reason why it does not return comments that are without email addresses or non-matching email addresses.
Replace your INNER JOIN with a LEFT JOIN. Try out this query:
SELECT `c`.`comment`, `c`.`user`, `m`.`avatar`
FROM `comments` `c`
LEFT JOIN `users` `u` ON `c`.`email` = `u`.`email`
LEFT JOIN `misc` `m` ON `m`.`userid` = `u`.`userid`;
Hope that should help you get all comments.
Not really sure what your desired output, how you get the right misc for a given user, but here is the general idea
SELECT userid, email, username, IF(email<>'',(SELECT avatar from misc where miscid = users.userid),null) avater FROM users;
this is a more readable version
SELECT
userid,
email,
username,
IF(email<>''
,/*then*/(SELECT avatar from misc where miscid = users.userid)
,/*else*/null)
as avater
FROM users;
Please provide a clear list of your tables, and an example desired output, and we can better assist.
The final desired example output is very helpful when designing MySQL statements.
SELECT * FROM comments LEFT JOIN users
ON users.email=comments.email
Not really sure if this is what you mean, but I guess you just want some extra columns in your query result with the email address (empty if not available) and avatar (empty if not available), if that's right you can work with a LEFT JOIN.
SELECT
c.*,
u.email,
m.avatar
FROM
comments as c LEFT JOIN
users as u ON (u.userid = c.user) LEFT JOIN
misc as m ON (m.miscid = u.userid)
Please not that the column names you are using are quite weird and inconsistent; use just the name id for the id of the column, and reference only to those id's in other models.
No, what you actually need is LEFT OUTER JOIN.
Its purpose is exactly what you need - when joining two (or three) tables on some key and the left table has no correspondent key it's columnsare filled with NULL in the result set for that key.