Get rank of contestants from points table with additional and nested conditions - mysql

We're trying to calculate the rank of contestants for a specific contest, using the following select query. The GROUP_CONCAT workaround is actually a solution that was offered here on SO for a similar question.
However, as we added more conditions the query got long, untidy and is not DRY, I think as we have to repeat the same conditions for the GROUP_CONCAT subquery.
How can it be optimized? Or would a solution like programmatically calculate ranks and check conditions then populate the database, be the best solution in this case?
SELECT *,
-- Get the rank of contestants
FIND_IN_SET(`id`, (
SELECT GROUP_CONCAT(`id` ORDER BY `points` DESC, `created`)
FROM `contestants` `c2`
-- The following query is exactly the same as the on in the main query bellow.
WHERE `contest_id`=:contest_id
AND EXISTS (
SELECT `user_id`
FROM `item`
WHERE `user_id`=`c2`.`user_id`
AND `product_id` IN (
SELECT `id`
FROM `product`
WHERE `price`<=:max_price
AND `available`=:available
)
)
AND `state`=:state
-- ---------------------------------------------------------------------------
)
) AS `rank`
FROM `contestants`
-- ---------------------------------------------------------------------------
WHERE `contest_id`=:contest_id
AND EXISTS (
SELECT `user_id`
FROM `item`
WHERE `user_id`=`c2`.`user_id`
AND `product_id` IN (
SELECT `id`
FROM `product`
WHERE `price`<=:max_price
AND `available`=:available
)
)
AND `state`=:state
-- ---------------------------------------------------------------------------
ORDER BY `rank` ASC
LIMIT 10

Related

Mysql single query to display the max points in table and points corresponding to a particular msisdn

I have a table which is having two columns msisdn,points.I require to display the max points in table and points corresponding to a particular msisdn through a single query.The query that i am using is based on sub queries and i don't think so that it is the most efficient way to do this.Guys kindly share an alternative optimized single query for this.
Table Structure:
CREATE TABLE `tbl_121314_point_base` (
`msisdn` bigint(12) NOT NULL DEFAULT '0',
`points` int(10) NOT NULL DEFAULT 0,
KEY `msisdn` (`msisdn`)
) ENGINE=INnoDB;
Current Query:
select (
select max(points) from tbl_121314_point_base ) as max_points,
(select points from tbl_121314_point_base where msisdn = 9024317476) as ori_points
from tbl_121314_point_base limit 1;
Another way you can rewrite your query using cross join use EXPLAIN plan to see performance of both queries
select p.points ori_points ,
t.max_points
from tbl_121314_point_base p
where p.msisdn = 9024317476
cross join(select max(points) max_points
from tbl_121314_point_base ) t
limit 1;

Optimising a working MYSQL statement

Background
I have a table of "users", a table of "content", and a table of "content_likes". When a user "likes" an item of content, a relation is added to "content_likes". Simple.
Now what I am trying to do is order content based on the number of likes it has received. This is relatively easy, however, I only want to retrieve 10 items at a time and then with a lazy load I am retrieving the next 10 items and so forth. If the select was ordered by time it would be easy to do the offset in the select statement, however, due to the ordering by number of "likes" I need another column I can offset by. So I've added a "rank" column to the result set, then on the next call of 10 items I can offset by this.
This query WORKS and does what I need to do. However, I am concerned about performance. Could anyone advise on optimising this query. Or even possibly a better way of doing it.
DB SCHEMA
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
CREATE TABLE `content` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`owner_id` int(11) NOT NULL,
`added` int(11) NOT NULL,
`deleted` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
CREATE TABLE `content_likes` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`content_id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
`added` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
*columns omitted for simplicity
Breakdown of query
group content_id in content_likes relations table, and order by likes desc
add a column "rank" (or row number) to result set and order by this
join "content" table so that any content with a deleted flag can be ommited
only return results where "rank" (or row number) is greater than variable
limit result set to 10
THE MYSQL
SELECT
results.content_id, results.likes, results.rank
FROM
(
SELECT
t1.content_id, t1.likes, #rn:=#rn+1 AS rank
FROM
(
SELECT
cl.content_id,
COUNT(cl.content_id) AS likes
FROM
content_likes cl
GROUP BY
cl.content_id
ORDER BY
likes DESC,
added DESC
) t1, (SELECT #rn:=0) t2
ORDER BY
rank ASC
) results
LEFT JOIN
content c
ON
(c.id = results.content_id)
WHERE
c.deleted <> 1
AND
results.rank > :lastRank
LIMIT
10
MYSQL ALTERNATIVE
SELECT
*
FROM
(
SELECT
results.*, #rn:=#rn+1 AS rank
FROM
(
SELECT
c.id, cl.likes
FROM
content c
INNER JOIN
(SELECT content_id, COUNT(content_id) AS likes FROM content_likes GROUP BY content_id ORDER BY likes DESC, added DESC) cl
ON
c.id = cl.content_id
WHERE
c.deleted <> 1
AND
c.added > :timeago
LIMIT
100
) results, (SELECT #rn:=0) t2
) final
WHERE
final.rank > :lastRank
LIMIT
5
The "Alternative" mysql query works as I would like it too also. Content is ordered by number of likes by users and I can offset by inserting the last row number. What I have attempted to do here is limit the result sets so if and when the tables get large performance isn't hindered too badly. In this example only content from within a timespan, and limit to 100 will be returned. Then I can offset by the row number (lazy load/pagination)
Any help or advice always appreciated. I am relatively a newbie to mysql so be kind :)
You can eliminate the subquery:
SELECT results.content_id, results.likes, results.rank
FROM (SELECT cl.content_id, COUNT(cl.content_id) AS likes, #rn:=#rn+1 AS rank
FROMc content_likes cl cross join
(SELECT #rn:=0) t2
GROUP BY cl.content_id
ORDER BY likes DESC, added DESC
) results LEFT JOIN
content c
ON c.id = results.content_id
WHERE c.deleted <> 1 AND
results.rank > :lastRank
LIMIT 10;
However, I don't think that will have an appreciable affect on performance. What you should probably do is store the last number number of likes and "added" value and use these to filter the data. The query needs to be a little fixed up, because added is not unambiguously defined in the order by clause:
SELECT results.content_id, results.likes, results.rank, results.added
FROM (SELECT cl.content_id, COUNT(cl.content_id) AS likes, MAX(added) as added, #rn:=#rn+1 AS rank
FROMc content_likes cl cross join
(SELECT #rn := :lastRank) t2
WHERE likes < :likes or
likes = :likes and added < :added
GROUP BY cl.content_id
ORDER BY likes DESC, added DESC
) results LEFT JOIN
content c
ON c.id = results.content_id
WHERE c.deleted <> 1 AND
results.rank > :lastRank
LIMIT 10;
This will at least reduce the number of rows that need to be sorted.

How to select top 5 usernames with highest score

I have two tables, username and score. Both are connected using user_id.
I want to select the top 5 usernames who have the highest score. I am trying following query but it is not working:
SELECT `user_name`
FROM `username`
WHERE `user_id` = ( SELECT `u_id`
FROM `score`
ORDER BY `high_score` DESC
LIMIT 5 )
I get this error when I run the above query: #1242 - Subquery returns more than 1 row
In your WHERE clause your are trying to assert the congruence or equality of one value (on the left side) to a list of values (on the right side).
Use the IN operator to achieve this because it will compare the left value with any of the right values.
The following is your corrected code.
SELECT `user_name`
FROM `username`
WHERE `user_id` IN(
SELECT `u_id`
FROM `score`
ORDER BY `high_score` DESC
LIMIT 5
);
As a manner of style using a join is clearer and more elegant especially for a simple query like this.
SELECT `u`.`user_name`
FROM `username` AS `u`
INNER JOIN `score` AS `s`
ON `u`.`user_id` = `s`.`u_id`
ORDER BY `s`.`high_score` DESC
LIMIT 5;
Try this:
SELECT `user_name`
FROM `username`,`score`
WHERE `user_id` = `u_id`
ORDER BY `high_score` DESC
LIMIT 5

Aggregate function not working as expected with subquery

Having some fun with MySQL by asking it difficult questions.
Essentially i have a table full of transactions, and from that i want to determine out of all the available products (productid), who (userid) has bought the most of each? The type in the where clause refers to transaction type, 1 being a purchase.
I have a subquery that on its own returns a list of the summed products bought for each person, and it works well by itself. From this i am trying to then pick the max of the summed quantities and group by product, which is a pretty straight forward aggregate. Unfortunately it's giving me funny results! The userid does not correspond correctly to the reported max productid sales.
select
`userid`, `productid`, max(`sumqty`)
from
(select
`userid`, `productid`, sum(`qty`) as `sumqty`
from
`txarchive`
where
`type` = 1
group by `userid`,`productid`) as `t1`
group by `productid`
I have removed all the inner joins to give more verbal results as they don't change the logic of it all.
Here is the structure of tx if you are interested.
id bigint(20) #transaction id
UserID bigint(20) #user id, links to another table.
ProductID bigint(20) #product id, links to another table.
DTG datetime #date and time of transaction
Price decimal(19,4) #price per unit for this transaction
QTY int(11) #QTY of products for this transaction
Type int(11) #transaction type, from purchase to payment etc.
info bigint(20) #information string id, links to another table.
*edit
Working final query: (Its biggish)
select
`username`, `productname`, max(`sumqty`)
from
(select
concat(`users`.`firstname`, ' ', `users`.`lastname`) as `username`,
`products`.`name` as `productname`,
sum(`txarchive`.`qty`) as `sumqty`
from
`txarchive`
inner join `users` ON `txarchive`.`userid` = `users`.`id`
inner join `products` ON `txarchive`.`productid` = `products`.`id`
where
`type` = 1
group by `productname`,`username`
order by `productname`,`sumqty` DESC) as `t1`
group by `productname`
order by `sumqty` desc
Not the best solution (not even guaranteed to work 100% of the times):
select
`userid`, `productid`, max(`sumqty`)
from
( select
`userid`, `productid`, sum(`qty`) as `sumqty`
from
`txarchive`
where
`type` = 1
group by
`productid`
, `userid`
order by
`productid`
, `sumqty` DESC
) as `t1`
group by
`productid`

Two queries merging

I need to merge two queries:
First query:
SELECT `p`.`name`, `posts`.*
FROM `polls` `p`, `posts`
WHERE `p`.`id` = `posts`.`guid`
AND `first` = {$id}
AND `text` LIKE ?
with... second query:
SELECT SUM(`deleted` = 0) AS 'posts'
FROM `posts`
WHERE `first`
NOT IN
(
SELECT `id`
FROM `posts`
WHERE `deleted` = 1
AND `first` = `id`
) AND `first` = {$fid}
Is there any chance to merge them?
You can use a UNION. You'll have to make them return the same columns though. The second query will have to return blank values for everything else.
I don't know what the query is trying to achieve, or what kind of results the queries produce. I think query 1 searches for posts like text, query 2 searches for deleted posts by ID. I've done a jig around, it should work if both queries generate the same amount of rows.
Changes:
1 You'd need to change the AS in the second query to a unique name (so here, posts_sum).
2 I've also removed the second ID join.
Check it out:
SELECT `p`.`name`, `posts`.*, SUM(`deleted` = 0) AS 'posts_sum'
FROM `polls` `p`, `posts`
WHERE `p`.`id` = `posts`.`guid`
AND `first` = {$id}
AND `text` LIKE ?
AND `first`
NOT IN
(
SELECT `id`
FROM `posts`
WHERE `deleted` = 1
AND `first` = `id`
)