Mysql - PHP - Checking a users posts and comments likes - mysql

Would anyone be able to recommend the best way to check if a user has liked a post or comment?
I am currently building a website that has similair features to Facebooks wall.
My website will show a 'wall' of posts from people you follow that you can like or comment on.
For example, comments I have:
Comments table containing: id, user_id, text (plus other columns)
Comments Likes table: comment_id, user_id, created
This is the current query I use to get the comments and checks if user has liked it using an inner join on the likes table. It uses an IF() to return liked as either 1 or empty, which works fine:
SELECT comments.id, comments.post_id, comments.user_id, comments.reply_id, comments.created, comments.text, comments.likes, comments.replies, comments.flags, user.name, user.tagline, user.photo_id, user.photo_file, user.public_key,
**IF(likes.created IS NULL, '', '1') as 'liked'**
FROM events_feed_comments AS comments
INNER JOIN user AS user ON comments.user_id = user.id
**LEFT JOIN events_feed_comments_likes AS likes ON comments.id = likes.comment_id AND likes.user_id = :user**
WHERE comments.post_id = :post_id AND comments.reply_id IS NULL
ORDER BY comments.created DESC
LIMIT :limit OFFSET :offset
However, I realise that this will not be cacheable for anyone else as it contains the logged in users likes. There may end up being a lot of posts and so will need to introduce caching.
I am wondering what the best way to check the likes will be?
At the moment these are the solutions i can think of:
I could either select all the comments limited to say 30 at a time (cacheable)
Then loop over each result doing a fetch/count query in the likes table to see if a user has liked it.
I could do a fetch from the likes table doing a where in clause using the returned 30 id results.
Then do some sort of looping to see if the likes value matches the returned results.
Fetch all of the comments (cacheable), fetch all of a users likes (could be cacheable?), then do some looping / comparing to see if the values match.
I am just not sure what would be the best solution, or if there is some other recommended way to achieve this?
I am thinking the second approach may be best but i'm interested to see what you think?
Updates to show the table Create statements
CREATE TABLE `events_feed_comments` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`post_id` int(11) NOT NULL,
`reply_id` int(11) DEFAULT NULL,
`text` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
`likes` int(11) NOT NULL,
`replies` int(11) NOT NULL,
`flags` smallint(6) NOT NULL,
`created` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
CREATE TABLE `events_feed_comments_likes` (
`comment_id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
`created` datetime NOT NULL,
PRIMARY KEY (`comment_id`,`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`photo_id` int(11) DEFAULT NULL,
`email` varchar(180) COLLATE utf8mb4_unicode_ci NOT NULL,
`roles` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL CHECK (json_valid(`roles`)),
`password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(80) COLLATE utf8mb4_unicode_ci NOT NULL,
`tagline` varchar(120) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`biography` varchar(2000) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`social` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`social`)),
`specialties` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`available` smallint(6) NOT NULL DEFAULT 0,
`theme` varchar(7) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`photo_file` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`following` int(11) NOT NULL,
`followers` int(11) NOT NULL,
`is_private` smallint(6) NOT NULL,
`public_key` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL,
`show_groups` smallint(6) NOT NULL,
`show_feed` smallint(6) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UNIQ_8D93D649E7927C74` (`email`),
UNIQUE KEY `UNIQ_8D93D64966F9D463` (`public_key`),
UNIQUE KEY `UNIQ_8D93D6497E9E4C8C` (`photo_id`),
CONSTRAINT `FK_8D93D6497E9E4C8C` FOREIGN KEY (`photo_id`) REFERENCES `photos` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci

Instead of using If in your SQL query, consider using SQL Case to simplify the query.
CASE --
WHEN ---- THEN '----'
ELSE '---'
END

For performance:
comments: INDEX(post_id, reply_id, created)
likes: INDEX(comment_id, user_id, created)
Those improvements may eliminate the need for "caching".
Please put "filtering" in WHERE, such as AND likes.user_id = :user** and put "relations" in ON. It can matter when using LEFT, and does help a human reading the query.
If events_feed_comments_likes is a many-to-many mapping table, you may want INDEX(user_id) also.
I assume this is the query in question:
SELECT comments.id, comments.post_id, comments.user_id, comments.reply_id,
comments.created, comments.text, comments.likes, comments.replies,
comments.flags, user.name, user.tagline,
user.photo_id, user.photo_file, user.public_key,
IF(likes.created IS NULL, '', '1') as 'liked'
FROM events_feed_comments AS comments
INNER JOIN user AS user ON comments.user_id = user.id
LEFT JOIN events_feed_comments_likes AS likes ON comments.id = likes.comment_id
AND likes.user_id = :user
WHERE comments.post_id = :post_id
AND comments.reply_id IS NULL
ORDER BY comments.created DESC
LIMIT :limit OFFSET :offset

Related

Choose the best way to get the number of comments for posts from Mysql

I have a question about the preference of choosing the table design in the database.
First of all, I have three tables for Users, Posts, Comments
this schema showing the tables:
this user schema :
CREATE TABLE `Users` (
`id` int(11) NOT NULL,
`fullname` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`profileImg` varchar(255) DEFAULT NULL,
`appPackage` varchar(50) DEFAULT NULL,
`email` varchar(255) NOT NULL,
`token` text NOT NULL,
`gender` enum('male','female') DEFAULT NULL,
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
ALTER TABLE `Users`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `email` (`email`);
this Posts schema :
CREATE TABLE `Posts` (
`id` int(11) NOT NULL,
`userId` int(11) NOT NULL,
`text` varchar(255) COLLATE utf8mb4_bin NOT NULL,
`background` varchar(10) COLLATE utf8mb4_bin DEFAULT NULL,
`img` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
`country` varchar(25) COLLATE utf8mb4_bin DEFAULT NULL,
`countryCode` varchar(2) COLLATE utf8mb4_bin DEFAULT NULL,
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`CommentsCount` int(11) NOT NULL,
`likes` int(11) NOT NULL DEFAULT '0',
`views` int(11) NOT NULL DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
ALTER TABLE `Posts`
ADD PRIMARY KEY (`id`),
ADD KEY `users_post_userId_fk` (`userId`);
this comments schema :
CREATE TABLE `Comments` (
`id` int(11) NOT NULL,
`postId` int(11) NOT NULL,
`userId` int(11) NOT NULL,
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`commentText` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
ALTER TABLE `Comments`
ADD PRIMARY KEY (`id`),
ADD KEY `users_comments_userId_fk` (`userId`),
ADD KEY `users_comments_postId_fk` (`postId`);
I want to query the Posts table, and each query brings 10 rows with comments count for every post.
Which is the best choice?
Querying the Posts table with joining to get count of comments like this :
SELECT * ,
(SELECT COUNT(*)
FROM Comments
WHERE Comments.postId = Posts.id) AS commentsCount
FROM Posts
ORDER BY Posts.id DESC
LIMIT 0,10
Or create a column commentsCount in the posts table that contains the number of comments for each post .
and Create a trigger linked to the comments table where, when inserting new comment, one number in the commentsCount is increased and when delete comment one number is decreased in commentsCount .
The query becomes only a posts table to get posts and comment count :
SELECT * FROM Posts ORDER BY Posts.id DESC LIMIT 0,10
Thanks in advance!
I would recommend against storing derived information in the post table. This would go against normalization best practices (and will be painful to maintain).
Information about comments belong to their own table. Whenever you need to count the number of comments per post, you can either use a correlated subquery as suggested by you:
SELECT
p.* ,
(SELECT COUNT(*) FROM Comments c WHERE Comments.postId = Posts.id) AS commentsCount
FROM Posts p
ORDER BY p.id DESC
LIMIT 0,10
But if you are dealing with a large number of posts this might become inefficient, so you can also join the Posts table with an aggregation query on Comments like so:
SELECT p.*, COALESCE(c.commentsCount, 0) commentsCount
FROM Posts p
LEFT JOIN (
SELECT postId, COUNT(*) commentsCount
FROM Comments
GROUP BY postId
) c ON c.postId = p.id
ORDER BY p.id DESC
LIMIT 0,10
There are pros and cons for both variants:
Adding another column breaks normal form. This is a problem not only in theory: Your write load on the posts table will increase drastically.
Calculating the post count each time does create quite a load, but is a very clean option.
Both will work fine on a moderate number of posts and comments, but I recommend you explore a different approcach, if you scale to a higher post/comment frequency:
Use an ethereal but fast cache (such as memcached) to store the post counts.
If a post comes in, ignore the cache
If a comment comes in (or is deleted), just delete the cache item for that post
If a comment count is requested, look into the cache - if it isn't there calculate it and put it into the cache.
You can use the built-in expiry mechanisms to keep the comment count cache at a manageable size while still having a very high hit rate.

MYSQL Join three tables with exceptions

I am working with three SQL tables and I am using INNER JOIN to join these tables. Here is an overview of what the tables look like.
CREATE TABLE `User` (
`id` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`firstName` varchar(64) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`lastName` varchar(64) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`email` varchar(64) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
CREATE TABLE `Ownership` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user` varchar(32) COLLATE utf8_unicode_ci NOT NULL,
`certificate` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unicity` (`user`,`certificate`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
CREATE TABLE `Certificate` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`domain` varchar(64) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`creationDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`expirationDate` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
`type` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8
The request to join the tables is the following :
select user, lastName, domain
from Ownership
INNER JOIN User ON Ownership.user = User.id
INNER JOIN Certificate ON Ownership.certificate = Certificate.id;`
I have this type of result for example :
What I would like is to retrieve only one user for one domain name and create an exception to do the join only if the certificate.domain value does not already exists.
Is this something possible with MYSQL ?
Thank you
You need a pre-query to at least qualify some uniqueness component associated with a given domain. To get that, lets consider this.
select
C.domain,
min( O.user ) JustOneUser
from
Certificate C
JOIN Ownership O
on C.ID = O.Certificate
group by
C.Domain
So this will get one user for a given domain. Now, use THAT result to get the user associated to the domain.
select
PQ.Domain,
U.LastName,
U.FirstName
from
(select
C.domain,
min( O.user ) JustOneUser
from
Certificate C
JOIN Ownership O
on C.ID = O.Certificate
group by
C.Domain ) PQ
JOIN User U
on PQ.JustOneUser = U.ID

Mysql case when getting another table

I have got two tables one is log details and another is user name list.
I can get with "INNER JOIN user_tbl ON log_user_id=user_id" but also I have got a super user id is exception and this exception user isn't in the user table. I am using this user at the back ground the relation with database is level of this user like as 9999.
How can I show the super user as a name like as "Supervisory"?
Example sql:
Table structures are:
CREATE TABLE `user_list_tb` (
`user_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_name` varchar(40) COLLATE utf8_turkish_ci NOT NULL,
`user_pwd` varchar(40) COLLATE utf8_turkish_ci NOT NULL,
`user_level` int(11) NOT NULL DEFAULT '0',
`user_owner_id` int(10) unsigned NOT NULL,
`user_datetime` datetime NOT NULL,
`user_change_pwd` tinyint(1) DEFAULT NULL,
`user_pwd_try` int(3) unsigned DEFAULT NULL,
PRIMARY KEY (`user_id`),
UNIQUE KEY `user_name_UNIQUE` (`user_name`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8 COLLATE=utf8_turkish_ci;
CREATE TABLE `log_system_tb` (
`log_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`log_act_name` varchar(10) COLLATE utf8_turkish_ci NOT NULL,
`log_user_id` int(10) unsigned NOT NULL,
`log_datetime` datetime NOT NULL,
`log_message` varchar(256) COLLATE utf8_turkish_ci DEFAULT NULL,
PRIMARY KEY (`log_id`)
) ENGINE=InnoDB AUTO_INCREMENT=49720 DEFAULT CHARSET=utf8 COLLATE=utf8_turkish_ci;
SELECT
log_id,
log_act_name,
log_user_id,
user_name,
log_datetime,
log_message
from log_system_tb
JOIN user_list_tb
ON (log_user_id=user_id)
where log_datetime>="2016-04-01 00:00:00"
and
log_datetime<="2016-04-14 00:00:00"
order by log_id desc limit 1000;
Best regards
Mehmet
You need to use a left join because there will not be a row in the user_list that corresponds to the supervisor's logged events. Also please see the case statement below.
SELECT
log_id,
log_act_name,
log_user_id,
(case when user_id=9999 then 'Supervisory' ELSE user_name END),
log_datetime,
log_message
from log_system_tb
LEFT JOIN user_list_tb
ON (log_user_id=user_id)
where log_datetime>="2016-04-01 00:00:00"
and
log_datetime<="2016-04-14 00:00:00"
order by log_id desc limit 1000;
You can do it like this:
(case when user_id=9999 then 'Supervisory' END)

Problem with sorting comments in date order

I am novice in MySQL and I have a problem with sorting two tables.
This SQL is about sorting newest comments on books, but I am getting these books sorted by first comments on them, not on latest.
SELECT b.*, c.date_added as date FROM books b
LEFT JOIN comments c ON (b.id = c.book_id)
GROUP BY b.id
ORDER BY date DESC
LIMIT 5
CREATE TABLE IF NOT EXISTS `books` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`caption` varchar(255) COLLATE utf8_bin NOT NULL,
`author` varchar(255) COLLATE utf8_bin NOT NULL,
`pages` int(11) NOT NULL,
`category_id` int(11) NOT NULL,
`filename` varchar(255) COLLATE utf8_bin NOT NULL,
`description` text COLLATE utf8_bin NOT NULL,
`date_added` datetime NOT NULL,
`publisher` varchar(255) COLLATE utf8_bin NOT NULL,
`price` decimal(10,2) NOT NULL,
`times_sold` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
CREATE TABLE IF NOT EXISTS `comments` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`book_id` int(11) NOT NULL,
`author` varchar(255) COLLATE utf8_bin NOT NULL,
`email` varchar(255) COLLATE utf8_bin NOT NULL,
`body` text COLLATE utf8_bin NOT NULL,
`date_added` datetime NOT NULL,
`approved` tinyint(4) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
Thank you for your time.
This should work out. You are specifying the group on b.id, but since you are multiple comments for each book, you need an aggregate function on c.date_added. In this case you can use MAX to show the most recent comment date.
SELECT b.*, MAX(c.date_added) as date FROM books b
LEFT JOIN comments c ON (b.id = c.book_id)
GROUP BY b.id
ORDER BY MAX(c.date_added) DESC
LIMIT 5
The 'Group By' clause is not correct and you need to remove it.
'Group By' is used for aggregate functions like Min() and Max().
Try This
SELECT b.*, c.date_added FROM books b
JOIN comments c ON
c.book_id = b.id
ORDER BY c.date_added DESC
LIMIT 5
There's no field called 'date', you need to order it by date_added (ORDER BY date_added DESC) as it is in your table!
If it's still not in the right order, just use ASC instead of DESC.

Nested selects in MySQL

Setting:
Each page on my site has four widgets that are arranged in different orders (1-4).
I have a table 'content' and table 'widgets'. I have a bridging table that maps content.id to widgets.content_id.
Problem:
What I want to do is run a query that selects * from content along with addition columns widget_1, widget_2, widget_3, widget_4, each containing the id of the widget linked to that page.
I've been trying some nested selects all morning and can't seem to crack it. I've copied the MySQL dumps of the involved tables below :-).
CREATE TABLE `content` (
`id` int(11) NOT NULL auto_increment,
`permalink` varchar(64) character set latin1 NOT NULL,
`parent` int(11) NOT NULL default '1',
`title` varchar(128) character set latin1 NOT NULL,
`content` text character set latin1,
`content_type` varchar(16) NOT NULL default 'page',
PRIMARY KEY (`id`),
FULLTEXT KEY `title` (`title`,`content`,`meta_description`,`meta_keywords`)
)
CREATE TABLE `widgets` (
`id` int(11) unsigned NOT NULL auto_increment,
`title` varchar(64) default NULL,
`text` varchar(256) default NULL,
`image` varchar(128) default NULL,
`target` varchar(128) default NULL,
`code` varchar(32) default NULL,
PRIMARY KEY (`id`)
)
CREATE TABLE `content_widgets` (
`content_id` int(11) NOT NULL,
`widget_id` int(11) NOT NULL,
`order` tinyint(4) NOT NULL
)
thanks a lot!
You don't need a nested query - just a join. Assuming that you want to start with a content record and return the matching widgets....
SELECT c.*, w.*
FROM content c
LEFT JOIN (
content_widgets cw INNER JOIN widgets w
ON cw.widget_id=w.id
) ON c.id=cw.id
WHERE c.id=....
Although a simple innter join is a better idea of you know you've got the widgets:
SELECT c.*, w.*
FROM content c, content_widgets cw widgets w
WHERE cw.widget_id=w.id
AND c.id=cw.id
AND c.id=....