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.
Related
I have a table which contains contacts (simplified here):
create table contacts
(
id_c char(36) not null primary key,
first_name varchar(255) not null,
last_name varchar(255) not null,
user_id1_c char(36) not null,
user_id2_c char(36) not null,
user_id3_c char(36),
user_id4_c char(36),
user_id5_c char(36),
user_id6_c char(36),
user_id7_c char(36) null,
user_id8_c char(36) null,
user_id9_c char(36) null,
user_id10_c char(36) null,
user_id11_c char(36) null,
user_id12_c char(36) null,
user_id13_c char(36) null,
user_id14_c char(36),
user_id15_c char(36) null,
user_id16_c char(36) null
)
engine = MyISAM
charset = utf8;
and a related table containing user information:
create table users (
id char(36) not null primary key,
first_name varchar(255),
last_name varchar(255)
)
For a report, I would want to be able to get the names of the associated users, and in MySQL this is a JOIN statement.
SELECT contacts.*,
CONCAT_WS(' ', u1.first_name, u1.last_name) AS `some_user`,
CONCAT_WS(' ', u2.first_name, u2.last_name) AS `some_other_user`,
FROM contacts
LEFT JOIN users u1 ON u1.id = user_id1_c
LEFT JOIN users u2 ON u2.id = user_id2_c
...
In the query, I would be able to get the names no problem (and before you go rushing to the comments with criticism for the design, this is the structure I got, so building a more abstract middleman user table isn't really within my pay grade). I recognize that appending 16 joins onto a query is slow even if it is well indexed. So my question becomes the following:
Is there a more direct way to address this problem? I have considered doing a LEFT JOIN users WHERE users.id IN (user_id1_c,...), but I cannot organize a GROUP BY or a distinct such that I retrieve the names matched to the column, just that a user maps to some column in a contact record.
Am I doomed to repeated LEFT JOINs, or is there a better way. If it matters, I am currently in version MySQL5.7, though I think we might be upgrading our company to version 8 sometime soon.
SELECT
c.*,
MAX(CASE WHEN u.id = c.user_id1 THEN u.whole_name END) AS user_name_1,
MAX(CASE WHEN u.id = c.user_id2 THEN u.whole_name END) AS user_name_2,
MAX(CASE WHEN u.id = c.user_id3 THEN u.whole_name END) AS user_name_3,
...
FROM
contacts
LEFT JOIN
(
SELECT
id,
CONCAT_WS(' ', first_name, last_name) AS whole_name
FROM
users
)
AS u
ON u.id IN (c.user_id1, c.user_id2, c.user_id3, ...)
GROUP BY
c.id
Really you should group by everything that you select from the contacts table, but MySQL 5.7 is lax and allows you to group by only the unique identififer.
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>
Table comment contains comment and its replies
CREATE TABLE `comment` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`comment` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
)
Table reply_to which has response_id and comment_id
both of which are the IDs of the records in the comment table.
CREATE TABLE `reply_to` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`response_id` int(11) NOT NULL,
`comment_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
)
If table comment has data like the following:
And table reply_to contains
And expected result like :
Question: How can I display all comments and their replies by using a left outer join?
This question came out from this page
Database design for comments and replies
How come the sql below not working here ?
SELECT c.comment, r.comment as reply
FROM Comment c
LEFT OUTER JOIN Comment r on c.id = r.id
LEFT OUTER JOIN reply_to rt on rt.response_id = r.id
It's still not totally clear what you want the results to look like, but the original query (the one with ANSI-89 joins) from the post you linked to gives the results I expected:
So I rewrote that query with proper joins, and it looks like this:
SELECT
oc.comment,
rc.comment
FROM
comment oc
INNER JOIN
reply_to r
ON r.comment_id = oc.id
INNER JOIN
comment rc
ON r.response_id = rc.id
If you want to see comments with no reply:
then you could use left outer joins like this:
SELECT
oc.comment,
rc.comment
FROM
comment oc
LEFT OUTER JOIN
reply_to r
ON r.comment_id = oc.id
LEFT OUTER JOIN
comment rc
ON r.response_id = rc.id
I have the following 3 tables:
CREATE TABLE Pins (
email varchar(100) NOT NULL,
boardID int NOT NULL,
));
CREATE TABLE Boarders (
email varchar(100) NOT NULL,
boardID int NOT NULL,
FOREIGN KEY (categoryName) REFERENCES Category (name));
CREATE TABLE User (
email varchar(100) NOT NULL,
name varchar(50),
PRIMARY KEY (email));
I am wanting to make the query search results this: for each Pin, show the description
This obviously is not working, but any suggestions on how to get the above to display?
select p.description ,c.title, u.name from PushPin p,CorkBoard c, User u
where c.email=u.email and c.email =p.email and c.boardID = p.boardID
order by p.description
Yes, this will require you to join the three tables.
Something like the below code should work.
Just defining the Foreign key relations does not mean you would not need to join while querying.
SELECT PP.description, CB.title, U.name
FROM PushPin PP JOIN CorkBoard CB ON PP.boardID = CB.boardID
JOIN USSER U ON PP.email = U.email
WHERE description like '$search'
ORDER BY description;
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.