I'd like to count the first 3 users who has the most attributed lines.
SQL Table:
ID | IdUser | Type |
-----------------------
0 | 1 | like |
1 | 1 | like |
2 | 4 | dislike |
3 | 5 | dislike |
4 | 1 | like |
5 | 4 | like |
6 | 5 | like |
8 | 4 | like |
9 | 4 | like |
10 | 3 | like |
11 | 5 | like |
12 | 9 | like |
Result should be:
idUser[1] with 3 times "like" and 0 "dislike" (3-0 = 3 points)
idUser[4] with 3 times "like" and 1 "dislike" (3-1 = 2 points)
idUser[5] with 2 times "like" and 1 "dislikes (2-1 = 1 point )
So what I'm trying to do is getting idUser 1 (3 points), then idUser4 (2 points) and finally idUser 5 (1 point) with their points.
I've tried different ways but none have worked.
Here I've tried to create a two-dimensional array with all data and then get the highest values but I couldn't do the second part.
Table 'users' has all users of the website
table 'points' has likes and dislikes recorded
$sqlUsers = "SELECT * FROM users";
$resultUsers = $conn->query($sqlUsers);
$recordsArray = array(); //create array
while($rowUsers = $resultUsers->fetch_assoc()) {
$idUser = $rowUsers['id'];
//COUNT LIKES OF THE USER
$sqlLikes = "SELECT COUNT(id) AS numberLikes FROM points WHERE idCibledUser='$idUser' AND type='like'";
$resultLikes = $conn->query($sqlLikes);
$rowLikes = $resultLikes->fetch_assoc();
//COUNT DISLIKES OF THE USER
$sqlDislikes = "SELECT COUNT(id) AS numberDislikes FROM points WHERE idCibledUser='$idUser' AND type='dislike'";
$resultDislikes = $conn->query($sqlDislikes);
$rowDislikes = $resultDislikes->fetch_assoc();
//GET POINTS BY SUBTRACTING DISLIKES FROM LIKES
$points = $rowLikes['numberLikes'] - $rowDislikes['numberDislikes'];
$recordsArray[] = array($idUser => $points);
}
If you ultimately just need the total points without a breakdown of likes and dislikes (not totally clear from your question):
SELECT IdUser, SUM(IF(Type='like',1,-1)) AS points
FROM users
GROUP BY IdUser
ORDER BY points DESC
LIMIT 3
If you want the complete breakdown:
SELECT IdUser,
SUM(IF(Type='like',1,-1)) AS points,
SUM(IF(Type='like',1,0)) as likes,
SUM(IF(Type='dislike',1,0)) as dislikes
FROM users
GROUP BY IdUser
ORDER BY points DESC
LIMIT 3
Explanation
Let's say I wanted to count the total number of rows where the Type column had the value 'like'. I could execute the following:
SELECT COUNT(*) AS cnt FROM users WHERE Type = 'like'
But another, perhaps less direct way, is the following:
SELECT SUM(IF(Type = 'like', 1, 0)) AS cnt FROM users
In the above SQL the Type column in each row is being examined and if equal to 'like', then the value of 1 is assigned to the column otherwise 0. Then all these 1's and 0's are added up using the SUM function. By adding up all the 1's, you are in effect counting the number of rows that had 'like' in the Type column. The second method allows you process the number of likes and dislikes with one pass:
SELECT SUM(IF(Type = 'like', 1, 0)) AS likes,
SUM(IF(Type = 'dislike', 1, 0)) AS dislikes
FROM users
But what if you wanted to get the above counts on a user by user basis? That is the purpose of the GROUP BY clause:
SELECT IdUser,
SUM(IF(Type = 'like', 1, 0)) AS likes,
SUM(IF(Type = 'dislike', 1, 0)) AS dislikes
FROM users
GROUP BY IdUser
The "score" or difference between the likes and dislikes can be computed if we assign the value of 1 to a column if it contains 'like' and a value of -1 if it contains 'dislike' (or isn't 'like') and then sum these values up:
SELECT IdUser,
SUM(IF(Type = 'like', 1, -1)) AS points,
SUM(IF(Type = 'like', 1, 0)) as likes,
SUM(IF(Type = 'dislike', 1, 0)) as dislikes
FROM users
GROUP BY IdUser
Finally, if you want the three top scores, sort the returned rows in descending order (ORDER BY points DESC) and keep only the first 3 rows returned (LIMIT 3):
SELECT IdUser,
SUM(IF(Type = 'like', 1, -1)) AS points,
SUM(IF(Type = 'like', 1, 0)) as likes,
SUM(IF(Type = 'dislike', 1, 0)) as dislikes
FROM users
GROUP BY IdUser
ORDER BY points DESC
LIMIT 3
See the solution below if you need to get likes/dislikes often, points table is being updated often and data relevance is important i.e. you don't want to cache the results.
Create another table like user_points_summary which will have 2 columns e.g. IdUser and Points. IdUser to be unique in this table, the Points recalculation (per user) must be triggered on adding new rows into the points table.
If you need likes/dislikes breakdown then this table will have 3 columns - IdUser (not unique anymore), likes_count, dislikes_count. And then the same - trigger this table update on inserting/updating/deleting rows in the points table.
If you go with the second option (with likes/dislikes breakdown) - here's an example of a create table statement:
CREATE TABLE `user_points_summary` (
`IdUser` int(11) NOT NULL,
`likes_count` int(11) NOT NULL DEFAULT '0',
`dislikes_count` int(11) NOT NULL DEFAULT '0',
KEY `idx_user_points_summary_IdUser` (`IdUser`)
) ENGINE=InnoDB;
Then you can add the following trigger to your users table which will add zero likes/dislikes on adding new users:
CREATE TRIGGER `users_AFTER_INSERT` AFTER INSERT ON `users` FOR EACH ROW
BEGIN
INSERT INTO `user_points_summary` VALUE (NEW.`IdUser`, 0, 0);
END
Then add the following triggers to the points table to update user_points_summary likes/dislikes count:
DELIMITER $$
CREATE TRIGGER `points_AFTER_INSERT` AFTER INSERT ON `points` FOR EACH ROW
BEGIN
IF NEW.`Type` = 'like' THEN
UPDATE `user_points_summary` SET `likes_count` = `likes_count` + 1 WHERE `IdUser` = NEW.`IdUser`;
ELSEIF NEW.`Type` = 'dislike' THEN
UPDATE `user_points_summary` SET `dislikes_count` = `dislikes_count` + 1 WHERE `IdUser` = NEW.`IdUser`;
END IF;
END $$
CREATE TRIGGER `points_AFTER_UPDATE` AFTER UPDATE ON `points` FOR EACH ROW
BEGIN
IF NEW.`Type` = 'dislike' AND OLD.`Type` = 'like' THEN
UPDATE `user_points_summary`
SET
`likes_count` = `likes_count` - 1,
`dislikes_count` = `dislikes_count` + 1
WHERE `IdUser` = `OLD`.`IdUser`;
ELSEIF NEW.`Type` = 'like' AND OLD.`Type` = 'dislike' THEN
UPDATE `user_points_summary`
SET
`dislikes_count` = `dislikes_count` - 1,
`likes_count` = `likes_count` + 1
WHERE `IdUser` = OLD.`IdUser`;
END IF;
END $$
CREATE TRIGGER `points_AFTER_DELETE` AFTER DELETE ON `points` FOR EACH ROW
BEGIN
IF OLD.`Type` = 'like' THEN
UPDATE `user_points_summary`
SET `likes_count` = `likes_count` - 1
WHERE `IdUser` = `OLD`.`IdUser`;
ELSEIF OLD.`Type` = 'dislike' THEN
UPDATE `user_points_summary`
SET `dislikes_count` = `dislikes_count` - 1
WHERE `IdUser` = OLD.`IdUser`;
END IF;
END $$
DELIMITER ;
Then you can use the following query to get user points with likes and dislikes count:
SELECT *, `likes_count` - `dislikes_count` AS `points`
FROM `user_points_summary`
ORDER BY `points` DESC
LIMIT 3
Related
Lets consider the following table-
ID Score
1 95
2 100
3 88
4 100
5 73
I am a total SQL noob but how do I return the Scores featuring both IDs 2 and 4?
So it should return 100 since its featured in both ID 2 and 4
This is an example of a "sets-within-sets" query. I recommend aggregation with the having clause, because it is the most flexible approach.
select score
from t
group by score
having sum(id = 2) > 0 and -- has id = 2
sum(id = 4) > 0 -- has id = 4
What this is doing is aggregating by score. Then the first part of the having clause (sum(id = 2)) is counting up how many "2"s there are per score. The second is counting up how many "4"s. Only scores that have at a "2" and "4" are returned.
SELECT score
FROM t
WHERE id in (2, 4)
HAVING COUNT(*) = 2 /* replace this with the number of IDs */
This selects the rows with ID 2 and 4. The HAVING clause then ensures that we found both rows; if either is missing, the count will be less than 2.
This assumes that id is a unique column.
select Score
from tbl a
where a.ID = 2 -- based off Score with ID = 2
--include Score only if it exists with ID 6 also
and exists (
select 1
from tbl b
where b.Score = a.Score and b.ID = 6
)
-- optional? ignore Score that exists with other ids as well
and not exists (
select 1
from tbl c
where c.Score = a.Score and c.ID not in (2, 6)
)
I'm writing a web app where people can add and vote on ideas. When outputting the ideas I want them to be ordered by their total vote count or time added, but always have a rank based on the vote count.
This is what I have now:
function get_ideas($status, $sortby, $count, $page){
$offset = ($page - 1) * $count;
$dbh = db_connect();
if($sortby === 'popular'){
$stmt = $dbh->prepare("
SELECT i.idea_id, i.idea_datetime, i.user_id, i.idea_title, i.idea_text,
i.idea_vote_count, u.user_name, #curRank := #curRank + 1 AS rank
FROM ideas i
JOIN (SELECT #curRank := :rankoffset) AS q
JOIN users u ON i.user_id = u.user_id
WHERE idea_status = :idea_status
ORDER BY idea_vote_count DESC
LIMIT :count
OFFSET :offset;");
} else {
$stmt = $dbh->prepare("HOW DO I DO THIS???");
}
$stmt->bindParam(':idea_status', $status, PDO::PARAM_STR);
$stmt->bindParam(':rankoffset', $offset, PDO::PARAM_INT);
$stmt->bindParam(':count', $count, PDO::PARAM_INT);
$stmt->bindParam(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
$stmt = NULL;
$dbh = NULL;
return $result;
}
The code in the "if" block works as intended - it returns an array ordered by "idea_vote_count" with correct ranks.
However, I have no idea what the second statement should be. Ordering by time added would be achieved easily enough by just changing "idea_vote_count" in the "ORDER BY" clause to "idea_id". How do I get the ranks, though? I couldn't think of nor find a solution which didn't involve storing the ranks in the table itself.
Hopefully I've explained my problem clearly, but just in case:
How do I get a table like this:
idea_id | idea_vote_count
1 | 20
2 | 40
3 | 30
4 | 5
To produce output like this:
rank | idea_id | idea_vote_count
4 | 4 | 5
2 | 3 | 30
1 | 2 | 40
3 | 1 | 20
Also, I'm kind of new to PHP and MySQL, so if you spot any other problems, please, point them out.
I look forward to your advice. Thanks :)
EDIT: For Strawberry:
My ideas table:
CREATE TABLE `ideas`(
`idea_id` int NOT NULL AUTO_INCREMENT,
`idea_datetime` datetime NOT NULL,
`user_id` int NOT NULL,
`idea_title` varchar(48) NOT NULL,
`idea_text` text NOT NULL,
`idea_vote_count` int NOT NULL DEFAULT 0,
`idea_status` varchar(16) NOT NULL DEFAULT 'active',
PRIMARY KEY(`idea_id`),
FOREIGN KEY(`user_id`) REFERENCES users(`user_id`))
ENGINE=INNODB;
The sample ideas have been generated by following script. I have then manually changed the idea_vote_count of row 100 to 5.
$i = 1;
set_time_limit(150);
while (i<101) {
$datetime = date('Y-m-d h:m:s');
$dbh->exec(INSERT INTO `ideas` (`idea_datetime`, `user_id`, `idea_title`, `idea_text`, `idea_vote_count`, `idea_status`)
VALUES ('{$datetime}', $i, 'Title{$i}', 'Text{$i}', $i, 'active');
$i++
sleep(1);
}
This is what I ended up with after incorporating Strawberry's SQL into my function:
function get_ideas($status, $sortby, $count, $page){
$offset = ($page - 1) * $count;
$dbh = db_connect();
if($sortby === 'popular'){
$stmt = $dbh->prepare("
SELECT i.idea_id, i.idea_datetime, i.user_id, i.idea_title, i.idea_text, i.idea_vote_count, u.user_name, #curRank := #curRank + 1 AS rank
FROM ideas i
JOIN (SELECT #curRank := :rankoffset) AS q
JOIN users u
ON i.user_id = u.user_id
WHERE idea_status = :idea_status
ORDER BY idea_vote_count DESC
LIMIT :count
OFFSET :offset;");
} else {
$stmt = $dbh->prepare("
SELECT n.*
FROM (
SELECT i.idea_id, i.idea_datetime, i.user_id, i.idea_title, i.idea_text, i.idea_vote_count, u.user_name, #curRank := #curRank + 1 AS rank
FROM ideas i
JOIN (SELECT #curRank := :rankoffset) AS q
JOIN users u
ON i.user_id = u.user_id
WHERE idea_status = :idea_status
ORDER BY idea_vote_count DESC
LIMIT :count
OFFSET :offset) n
ORDER BY idea_id DESC;");
}
$stmt->bindParam(':idea_status', $status, PDO::PARAM_STR);
$stmt->bindParam(':rankoffset', $offset, PDO::PARAM_INT);
$stmt->bindParam(':count', $count, PDO::PARAM_INT);
$stmt->bindParam(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
$stmt = NULL;
$dbh = NULL;
return $result;
}
As you can see, the function takes $count and $page as arguments (usually these have value of $_REQUEST['count/page']) and calculates the offset and limit based on them. This is very important, because I don't want to show all the ideas to users at the same time, I want to split them into several pages. However, this messes with the select/ranking SQL in the following way:
When $page = 1 and $count = 100 you get LIMIT 100 OFFSET 0 and the script works as intended - it shows the most recent row (row 100) as the first one ranked 96 (only rows 1, 2, 3, 4 have lower vote count), followed by the other recent rows ranked 1, 2, 3 and so on.
However, when $page = 1 and $count = 10 you get LIMIT 10 OFFSET 0 and the script outputs row 99 first, because it's the highest rated one, but not the most recent. Row 100 becomes the first result in the result set when $page = 10 (the lowest rated and OLDEST rows, despite the fact that row 100 is the most recent).
I could technically select the entire table and then handle the pagination in PHP, but I fear what the performance impact would be.
EDIT2: I have moved OFFSET and LIMIT into the outer SELECT and now everything works as it's supposed to. The side effect of this is, that the inner SELECT selects the entire table, but hopefully it won't grow too big for the server to handle. This is the solution I'm sticking with for the time being. It's based on Strawberry's SQL, so I'll mark that as the answer. Thanks, Strawberry :)
Here's one idea, using purely MySQL, but it's probably better just to return the data set with the ranks and then do any further ordering in the application code...
DROP TABLE IF EXISTS my_table;
CREATE TABLE my_table
(idea_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY
,idea_vote_count INT NOT NULL
);
INSERT INTO my_table VALUES
(1 ,20),
(2 ,40),
(3 ,30),
(4 ,5);
SELECT n.*
FROM
( SELECT x.*
, #i:=#i+1 rank
FROM my_table x
, (SELECT #i:=0) vars
ORDER
BY idea_vote_count DESC
) n
ORDER
BY idea_id DESC;
+---------+-----------------+------+
| idea_id | idea_vote_count | rank |
+---------+-----------------+------+
| 4 | 5 | 4 |
| 3 | 30 | 2 |
| 2 | 40 | 1 |
| 1 | 20 | 3 |
+---------+-----------------+------+
This is how my table looks like:
id | name | value
-----------------
1 | user1| 1
2 | user2| 1
3 | user3| 3
4 | user4| 8
5 | user5| 6
6 | user7| 4
7 | user8| 9
8 | user9| 2
What I want to do is to select all the other users, in one query, who's value is user1's value lower than it's value plus 3, higher than it's value minus 3 or equal to it's value.
Something like this:
$result = mysqli_query($con,"SELECT * FROM users WHERE value<'4' OR value>'-2'") or die("Error: ".mysqli_error($con));
while($row = mysqli_fetch_array($result))
{
echo $row['name'].'<br/>';
}
The problem is that users1's value can vary every time the query is run.
Sorry for lame names, but this should work:
NOTE: I named table with your data as "st".
SELECT b.user, a.value as "user1val", b.value as "otheruservalue" FROM st as a
join st as b
on a.user = "user1" and a.user != b.user
where
(b.value > (a.value - 3)) and (b.value < (a.value + 3))
We get unique pairs of user1's value and other user's value by joining same table. After that we just do some simple comparison to filter rows with suitable values.
$user1 = mysql_fetch_assoc(mysql_query("SELECT `value` FROM `users` WHERE id='1'"));
$result = mysql_query("SELECT * FROM `users` WHERE value<'".$user1['value']."+3' OR value>'".$user1['value']."-3'");
Or nested queries :
$result = mysqli_query($con, "select * from `users` where `value` < (select `value` from `users` where `name`='user1')+3 OR `value` > (select `value` from `users` where `name`='user1')-3");
I have a mysql query of type
select some_value FROM table WHERE (subquery) IN ( values )
which seems to be extremly slow!
I have a mysql table with orders and a second one with the corresponding processing states of them. I want now to show all orders having their last status code = 1 .
table order (id = primary key)
id | value
---+-------
1 + 10
2 + 12
3 + 14
table state (id = primary key)
id | orderid | code
---+---------+-------
1 + 1 + 1
2 + 2 + 1
3 + 2 + 2
4 + 1 + 3
5 + 3 + 1
My query is:
select order.id FROM order WHERE
( select state.code FROM state WHERE state.orderid = order.id ORDER BY state.id DESC LIMIT 0,1 ) IN ( '1' )
It takes roughly 15 seconds to process this for a single order. How to modify the mysql statement in order to speed the query procession time up?
Update
Try this one:
select s1.orderid
from state s1
left outer join state s2 on s1.orderid = s2.orderid
and s2.id > s1.id
where s1.code = 1
and s2.id is null
You may need an index on state.orderid.
SQL Fiddle Example
CREATE TABLE state
(`id` int, `orderid` int, `code` int)
;
INSERT INTO state
(`id`, `orderid`, `code`)
VALUES
(1, 1, 1),
(2, 2, 1),
(3, 2, 2),
(4, 1, 3),
(5, 3, 1)
;
Query:
select s1.orderid
from state s1
left outer join state s2 on s1.orderid = s2.orderid
and s2.id > s1.id
where s1.code = 1
and s2.id is null
Results:
| ORDERID |
|---------|
| 3 |
in this situation I think you could simply forget of order, as all information stays in state.
SELECT x.id, x.orderid
FROM state AS x
WHERE x.code =1
AND x.id = (
SELECT max( id )
FROM a
WHERE orderid = x.orderid )
Maybe would be possible to change your CRUD by putting last state directly in order table too, this would be the better
:-)
I have this script, which find a users position taken from the number of credits.
It all works, but i have a little problem. If two users have the same credits, both of them will be on the same position.
Can I do, so if there are more users with same credits, then the system need to order by the users ID and out from that give them a position?
This is my code so far:
$sql = "SELECT COUNT(*) + 1 AS `number`
FROM `users`
WHERE `penge` >
(SELECT `penge` FROM `users`
WHERE `facebook_id` = ".$facebook_uid.")";
$query_rang = $this->db->query($sql);
So if i have this:
ID -------- Credits
1 -------- 100
2 -------- 100
3 -------- 120
Then the rank list should be like this:
Number 1 is user with ID 3
Number 2 is user with ID 1
Number 3 is user with ID 2
ORDER BY credits DESC, id ASC. This will sort by credits and break ties with the id.
UPDATE
I understand now that you want the ranking information for the user, not just to sort the users by credits and ids. This will give you the complete list of users and their rankings:
SELECT #rank:=#rank+1 AS rank, users.id, users.facebook_id FROM users, (SELECT #rank:=0) dummy ORDER BY penge DESC, id ASC
Getting the row number is the tricky bit solved by this blog post:
http://jimmod.com/blog/2008/09/displaying-row-number-rownum-in-mysql/
$sql = "SELECT COUNT(*) + 1 AS `number` FROM `users` WHERE `penge` > (SELECT `penge` FROM `users` WHERE `facebook_id` = ".$facebook_uid.") ORDER BY COUNT(*) + 1 desc, users.ID";
$query_rang = $this->db->query($sql);
Later EDIT:
I don't understand why you still have the same results....
I made a quick test. I have created a table:
Test: ID (Integer) and No (Integer)
I have inserted some values:
id no
1 1
1 1
1 1
2 1
3 1
4 1
4 1
5 1
Now, if I run:
SELECT
id, COUNT(*) + 1 AS `number`
FROM
test
GROUP BY
id
I get:
id number
1 4
2 2
3 2
4 3
5 2
But if I add ORDER BY:
SELECT
id, COUNT(*) + 1 AS `number`
FROM
test
GROUP BY
id
ORDER BY
count(*) desc, id
then I get:
id number
1 4
4 3
2 2
3 2
5 2