How to Optimize a Query With GROUP BY and ORDER BY - mysql

I have got a POSTS table, the structure is like this:
CREATE TABLE IF NOT EXISTS `posts` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) COLLATE utf8_turkish_ci DEFAULT NULL,
`content` longtext COLLATE utf8_turkish_ci,
`excerpt` longtext COLLATE utf8_turkish_ci,
`link` longtext COLLATE utf8_turkish_ci,
`original_link` longtext COLLATE utf8_turkish_ci,
`mime_type` longtext COLLATE utf8_turkish_ci,
`language_id` int(11) DEFAULT NULL,
`user_id` int(11) DEFAULT NULL,
`site_id` int(11) DEFAULT NULL,
`type` varchar(255) COLLATE utf8_turkish_ci DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
`modified_at` datetime DEFAULT NULL,
`is_deleted` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `type` (`type`),
KEY `created_at` (`created_at`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_turkish_ci AUTO_INCREMENT=52487 ;
And a USERS table, structed like this:
CREATE TABLE IF NOT EXISTS `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) COLLATE utf8_turkish_ci NOT NULL,
PRIMARY KEY (`id`),
KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_turkish_ci AUTO_INCREMENT=9422 ;
I'm using this query to get latest "page, file or post" posts ordered by descending time and grouping by user to not show all latest posts from a user:
SELECT p.*, u.*
FROM posts p
LEFT JOIN users u ON p.user_id = u.id
WHERE p.type IN ('post', 'page', 'file')
GROUP BY p.user_id
ORDER BY p.created_at DESC
LIMIT 30
But it is too slow, even limited to 30 records.
now, how can i speed up this query? which columns to index or any other ideas? thanks.

The first thing to do is to add an index on posts.user_id (or maybe posts.user_id + posts.type). And another index on posts.created_at
UPDATE
I've just payed attention that your query grabs all fields from both tables, and posts table has 6 long text columns. So I believe you have a poor performance because mysql has to create quite a large temporary table or temp file to get all rows for satisfying your group by + order by clauses.
I think the following query should help.
SELECT u.*, p1.* FROM
users u
INNER JOIN
(
SELECT p.user_id, p.created_at, p.id FROM posts p
WHERE p.type IN ('post', 'page', 'file') GROUP by p.user_id
ORDER BY p.created_at DESC LIMIT 30
)xxx ON xxx.user_id = u.id
INNER JOIN posts p1 ON (p1.id = xxx.id)

In terms of indices, I would suggest creating ones on posts.type (WHERE), posts.created_at (ORDER). That should help speed up the sorting.

You can try this:
SELECT p., u.
FROM (SELECT * FROM posts WHERE p.type IN ('post', 'page', 'file')) p
LEFT JOIN users u ON p.user_id = u.id
GROUP BY p.user_id ORDER BY p.created_at DESC
LIMIT 30
MySQL first proccess the inner query, and with its result process the outter query with less records.

Try #Gabriel's answer, but with the LIMIT in the inner query.
SELECT p., u.
FROM (SELECT * FROM posts WHERE type IN ('post', 'page', 'file') ORDER BY created_at DESC LIMIT 30) p
LEFT JOIN users u ON p.user_id = u.id
ORDER BY p.created_at;

Related

Get index of certain result in query results list?

I've been trying to even write this title in a way that makes most sense, but through much googling I have not found anything to match what I am looking.
Basically, I have a database that stores players and their different levels, and I already have a working query that fetches a list of all players and ranks them in the required order (Total prestige first, then level, then experience and lastly by the oldest update timestamp)
The query I use for this is:
SELECT u.id, u.username, u.mode, u.total_prestige as prestige, u.total_level as level, u.total_xp as exp, s.created FROM hs_users u JOIN hs_userskill s ON u.id = s.userId
WHERE s.id IN (SELECT MAX(id) FROM hs_userskill WHERE userId = u.id GROUP BY userId)
ORDER BY total_prestige DESC, total_level DESC, total_xp DESC, created ASC;
But now, on a different page, I need to find the players "rank" (so basically their index in the results list)
Is there a proper sql way of doing this (probably), instead of just taking the whole results set into code and looping over it? As I am tempted at doing at this point in time.
My database structure in sql:
CREATE TABLE IF NOT EXISTS `hs_modes` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`modename` varchar(40) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`modeicon` varchar(40) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
CREATE TABLE IF NOT EXISTS `hs_skills` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`skillname` varchar(40) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
`skillicon` varchar(40) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
CREATE TABLE IF NOT EXISTS `hs_users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(40) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL UNIQUE,
`mode` int(11) NOT NULL DEFAULT '0',
`total_prestige` int(11) NOT NULL DEFAULT '0',
`total_level` int(11) NOT NULL DEFAULT '0',
`total_xp` bigint(20) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
FOREIGN KEY (`mode`) REFERENCES `hs_modes`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
CREATE TABLE IF NOT EXISTS `hs_userskill` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`userId` int(11) NOT NULL,
`skillId` int(11) NOT NULL,
`prestige` int(11) NOT NULL,
`experience` int(11) NOT NULL,
`created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
FOREIGN KEY (`userId`) REFERENCES `hs_users`(`id`),
FOREIGN KEY (`skillId`) REFERENCES `hs_skills`(`id`),
UNIQUE KEY `userskill` (`userId`, `skillId`, `prestige`, `experience`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
In mysql 8 you have ROW_NUMBER to signify the position in the specified order
SELECT
u.id,
u.username,
u.mode,
u.total_prestige AS prestige,
u.total_level AS level,
u.total_xp AS exp,
s.created
,ROW_NUMBER() OVER(ORDER BY total_prestige DESC , total_level DESC , total_xp DESC , created ASC) rn
FROM
hs_users u
JOIN
hs_userskill s ON u.id = s.userId
WHERE
s.id IN (SELECT
MAX(id)
FROM
hs_userskill
WHERE
userId = u.id
GROUP BY userId)
ORDER BY total_prestige DESC , total_level DESC , total_xp DESC , created ASC;
Fro Bob rank
SELECT
*
FROM
(SELECT
u.id,
u.username,
u.mode,
u.total_prestige AS prestige,
u.total_level AS level,
u.total_xp AS exp,
s.created
,ROW_NUMBER() OVER(ORDER BY total_prestige DESC , total_level DESC , total_xp DESC , created ASC) rn
FROM
hs_users u
JOIN
hs_userskill s ON u.id = s.userId
WHERE
s.id IN (SELECT
MAX(id)
FROM
hs_userskill
WHERE
userId = u.id
GROUP BY userId)
) t1
WHERE username = 'Bob'
Set the database connection cursor to fetch the result as dictionary, then you can access the data as dictionary index of it's column names

Mysql query with inner subquery and where,order,group not use indexes

all.
I have 2 mysql tables type myisam
CREATE TABLE `users` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
CREATE TABLE `users_ratings` (
`id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT,
`created_date` varchar(255) DEFAULT NULL,
`user_id` mediumint(9) DEFAULT NULL,
`rating1` mediumint(9) DEFAULT NULL,
`rating2` mediumint(9) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Dump can be downloaded here
I want all users to select and sort the list by rating2 and rating2 must be > 1000
SELECT * FROM users as u
INNER JOIN (SELECT rating2, user_id,MAX(created_date) as maxdate FROM `users_ratings` GROUP BY user_id) as x ON x.user_id = u.id
INNER JOIN users_ratings as ur ON(ur.created_date = x.maxdate and ur.user_id = u.id)
WHERE x.rating2 > 1000
ORDER by ur.rating2
Without such an index the query is executed for 9 seconds.
All possible codes, which I thought was allowed to reduce the time of the request to 1-2 seconds.
Help the right to place indexes
I suspect you want the following query. Assuming you do, and performance is still an issue, provide the EXPLAIN for same...
(this assumes a Unique Key on user_id,[rating2,]created_date, which hasn't actually been specified)
SELECT u.*
, x.*
FROM users u
JOIN users_ratings x
ON x.user_id = u.id
JOIN
( SELECT user_id
, MAX(created_date) maxdate
FROM users_ratings
WHERE rating2 > 1000
GROUP
BY user_id
) y
ON y.user_id = x.user_id
AND y.maxdate = x.created_date
ORDER
BY x.rating2;

3 Mysql Inner Joins with last join being the ORDER BY Clause

I have 3 tables I'm trying to inner join (ambitious I know). The first query of the join, just basically queries my members table to pipe into the second query, which is the post table that actually holds the posts for those members (users search by member info to see their posts). The third and final query is simply ordering by the frequency of the most viewed posts. I have these two queries working separately:
$sql_string = "
SELECT m.id
, m.username
, m.gender
, p.*
FROM members m
JOIN posts p
ON p.member_id = m.id
WHERE m.active='y'
AND m.gender='M'
AND m.city='Los Angeles'
AND m.state='California'
AND p.active='y';
";
which accomplishes the first 2 queries and this final query:
$sql_string2 = "SELECT post_id FROM post_views GROUP BY post_id ORDER BY COUNT(*) DESC";
Which accomplishes the final query. I just need to combine the 2, but when I do that:
$final_sql_string = "SELECT members.id, members.username, members.gender, posts.* FROM members INNER JOIN posts ON members.id = posts.member_id WHERE members.active='y' AND members.gender='M' AND members.city='Los Angeles' AND members.state='California' AND posts.active='y' INNER JOIN post_views ON posts.id = post_views.post_id GROUP BY post_views.post_id ORDER BY COUNT(*) DESC";
I get an error:
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'INNER JOIN post_views ON posts.id = post_views.post_id GROUP BY post_views.post_' at line 1
Any ideas? Here are my tables for anyone interested:
CREATE TABLE IF NOT EXISTS `members` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(20) NOT NULL,
`age` varchar(3) NOT NULL,
`gender` varchar(1) NOT NULL,
`city` varchar(20) NOT NULL,
`state` varchar(50) NOT NULL,
`active` enum('y','n') NOT NULL DEFAULT 'y',
`created_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=3 ;
CREATE TABLE IF NOT EXISTS `posts` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`member_id` int(11) NOT NULL,
`title` text NOT NULL,
`comments` enum('y','n') NOT NULL DEFAULT 'y',
`post_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`list_weight` double NOT NULL,
`active` enum('y','n') NOT NULL DEFAULT 'y',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=47 ;
CREATE TABLE IF NOT EXISTS `post_views` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`post_id` int(11) NOT NULL,
`member_id` int(11) NOT NULL,
`post_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=287 ;
The following should work:
SELECT m.id
, m.username
, m.gender
, p.*
, pc.post_count
FROM members m
JOIN posts p
ON p.member_id = m.id
LEFT JOIN (
SELECT post_id, COUNT(*) post_count FROM post_views GROUP BY post_id
) pc ON p.id = pc.post_id
WHERE m.active='y'
AND m.gender='M'
AND m.city='Los Angeles'
AND m.state='California'
AND p.active='y'
ORDER BY post_count DESC;
"join in" the counts you gathered and order by those.
If you want to keep your "style" you may use:
SELECT m.id
, m.username
, m.gender
, p.*
FROM members m
JOIN posts p
ON p.member_id = m.id
WHERE m.active='y'
AND m.gender='M'
AND m.city='Los Angeles'
AND m.state='California'
AND p.active='y'
ORDER BY (SELECT COUNT(*) FROM post_views WHERE post_id = p.id) DESC;

how can i get all post for me and for user who i follow him

i have 3 table.
1st is for user info.
2nd is for post where user insert to it and relation with user table by user_id.
3rd is for user who i follow him and relation with user table by user_id.
now in home page i need to show all posts where i insert on it and where user i follow him insert into table post.
i will try to make this MySQL by :-
SELECT * FROM users ,
(SELECT * FROM events where ev_user_id in
(
( select * from follow where follow.fo_user_id = '".$user_id."' )
, '".$user_id."'
)
) as post
where post.ev_user_id = users.id
order by post.ev_date DESC limit $to , $from
where $user_id is id for user.
here i get error that:-
Operand should contain 1 column(s)
if i follow one user its work, but when i follow more than one user, its display above error.
how can i get all post for me and for user who i follow him
====================================
events table is the table for post
CREATE TABLE `events` (
`ev_id` int(11) NOT NULL auto_increment,
`ev_user_id` int(11) NOT NULL,
`ev_type` varchar(222) NOT NULL,
`ev_text` text NOT NULL,
`ev_pic` varchar(222) NOT NULL,
`ev_date` varchar(22) NOT NULL,
PRIMARY KEY (`ev_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=211 ;
table for user which i follow him
CREATE TABLE `follow` (
`fo_id` int(11) NOT NULL auto_increment,
`fo_user_id` int(11) NOT NULL,
`fo_user_id_follow` int(11) NOT NULL,
`fo_date` varchar(22) NOT NULL,
PRIMARY KEY (`fo_id`),
UNIQUE KEY `fo_user_id` (`fo_user_id`,`fo_user_id_follow`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=15 ;
table for user info
CREATE TABLE `users` (
`id` bigint(20) unsigned NOT NULL auto_increment,
`fullname` varchar(222) NOT NULL,
`username` varchar(60) NOT NULL,
`password` varchar(64) NOT NULL,
`email` varchar(50) NOT NULL,
`address` varchar(300) NOT NULL,
`phone` varchar(20) NOT NULL,
`skype` varchar(20) NOT NULL,
`facebook` varchar(40) NOT NULL,
`msn` varchar(90) NOT NULL,
`mobile` varchar(20) NOT NULL,
`more` text NOT NULL,
`time` varchar(50) NOT NULL,
`lastlogin` varchar(50) NOT NULL,
`role_id` tinyint(1) NOT NULL default '2',
`code` varchar(7) NOT NULL,
`active` tinyint(4) NOT NULL default '3',
`wieght` int(11) NOT NULL,
`tall` int(11) NOT NULL,
`birthdate` varchar(20) NOT NULL,
`gender` varchar(5) NOT NULL,
`fat` int(11) NOT NULL,
`fittnes` int(11) NOT NULL,
`status` int(11) NOT NULL,
`pic` varchar(222) NOT NULL,
`regdate` varchar(22) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`),
UNIQUE KEY `username` (`username`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=396 ;
This should get you the posts and user info of the users your $user_id follows ordered by descending date.
SELECT * FROM events
JOIN follow ON events.ev_user_id = follow.fo_user_id
JOIN users ON events.ev_user_id = user.id
WHERE follow.fo_user_id_follow = '".$user_id."'
ORDER BY events.ev_date DESC
LIMIT $to , $from
Is this what you wanted? not completelly sure.
EDIT: to add also your own posts as well as the ones form the users you follow.
SELECT * FROM events
JOIN follow ON events.ev_user_id = follow.fo_user_id
JOIN users ON events.ev_user_id = user.id
WHERE follow.fo_user_id_follow = '".$user_id."'
OR events.ev_user_id = '".$user_id."'
ORDER BY events.ev_date DESC
LIMIT $to , $from
EDIT: the enquirer's exact solution, Daren had understood the follow relationship reversed.
SELECT * FROM events
JOIN follow ON events.ev_user_id = follow.fo_user_id_follow
JOIN users ON events.ev_user_id = users.id
WHERE follow.fo_user_id = '".$user_id."'
OR events.ev_user_id = '".$user_id."'
ORDER BY events.ev_date DESC
LIMIT $to , $from"
The error message you're getting, is because you're using the IN operator against a subquery that returns more than one column. Maybe rewrite your SQL to something like this:
SELECT * FROM users ,
(SELECT * FROM events where ev_user_id in
(
( select user_id from follow where follow.fo_user_id = '".$user_id."' )
, '".$user_id."'
)
) as post
where post.ev_user_id = users.id
order by post.ev_date DESC limit $to , $from
I guess - You should provide specific column instead of * at the line -
( select * from follow where follow.fo_user_id = '".$user_id."' )
you could try with the below line -
select fo_user_id_follow from follow where follow.fo_user_id = '".$user_id."'
what about the following?
SELECT * FROM users
INNER JOIN follow ON fo_user_id=users.id
INNER JOIN events ON ev_user_id IN (users.id,fo_user_id_follow)
WHERE users.id='$user_id' ORDER BY post.ev_date DESC LIMIT $to , $from
You will probably get too many columns as it will list all the columns from all joined tables.
Edit: added ' around user_idjust in case users.id is a varchar column ...
Edit: widened criterion for user_id: ev_user_id IN (users.id,fo_user_id_follow)
rewrite:
SELECT * FROM follow
INNER JOIN users ON users.id=ev_user_id IN (users.id,fo_user_id_follow)
INNER JOIN events ON ev_user_id=users.id
WHERE users.id='$user_id' ORDER BY post.ev_date DESC LIMIT $to , $from
This should give you information about all users (including yourself) and all posts from these users that follow the user identified by $user_id. Probably not a good choice either, since you will have the full user information given again and again for each post.
It would be better to have two queries and two result tables: 1. user info, 2. posts of these users.
SELECT * FROM user
WHERE users.id IN (
SELECT fo_user_id_follow FROM follow WHERE fo_user_id=$user_id )
OR users.id=$user_id
and
SELECT * FROM events
WHERE ev_user_id IN (
SELECT fo_user_id_follow FROM follow WHERE fo_user_id=$user_id )
OR ev_user_id=$user_id

Join two tables, matching a column with multiple values

I am trying to get a product matching some custom parameters.
So I have to three tables - products, parameters and parametersitems.
Products table:
CREATE TABLE `products` (
`ID` int(10) unsigned NOT NULL AUTO_INCREMENT
`Title` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`Content` longtext COLLATE utf8_unicode_ci NOT NULL,
`Price` float(10,2) unsigned NOT NULL,
PRIMARY KEY (`ID`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
Parameter table:
CREATE TABLE `parameters` (
`ID` int(10) unsigned NOT NULL AUTO_INCREMENT,
`Label` varchar(80) COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`ID`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
Parameter items table:
CREATE TABLE `parametersitems` (
`ProductID` int(10) unsigned NOT NULL DEFAULT '0',
`ParameterID` int(10) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`ProductID`,`ParameterID`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
So my question is how can I get only the products matching all the parameters.
The only way I could think of is joining the parameteritems table couple of times.
For example, here is a query to get the products matching two parameters:
SELECT
products.*
FROM
products
INNER JOIN
parametersitems AS paritems1
ON
paritems1.ItemID = products.ID
AND paritems1.ParameterID = 7
INNER JOIN
parametersitems AS paritems2
ON
paritems2.ItemID = products.ID
AND paritems2.ParameterID = 11
My only concern is that the SELECT query will get slower and slower if there more parameters selected.
So is there a better way to handle this problem?
Thank you
Adjust the value tested in the HAVING clause to match the number of values listed in the IN clause.
SELECT p.*
FROM products p
WHERE p.ID IN (SELECT pi.ItemID
FROM parameteritems pi
WHERE pi.ItemID = p.ID
AND pi.ParameterID IN (7,11)
GROUP BY pi.ItemID
HAVING COUNT(DISTINCT pi.ParameterID) = 2)
select p.*
from products p
inner join (
select ItemID
from parametersitems
where ParameterID in (7, 11)
group by ItemID
having count(distinct ParameterID) = 2
) pm on p.ID = pm.ItemID
SELECT
p.ID, p.Title, p.Content, p.Price
FROM
products AS p
INNER JOIN
parametersitems AS pi ON pi.ProductID = p.ID
GROUP BY
p.ID, p.Title, p.Content, p.Price
HAVING COUNT(DISTINCT pi.ParameterID) = (SELECT COUNT(ID) FROM parameters);
This will always get you products matching every parameter no matter how many parameters you add. (This could become bogus if you delete a parameter without deleting the corresponding rows in paramatersitems. This is what constraints are for.)