MySQL Query: How many times has a candidate won an election? - php

I am writing a web app in PHP using mySQL that models an election.
I have three tables: Candidates, Elections, and Votes. Votes contains CandidateID, ElectionID and Count, which is the number of times that the given candidate was voted for in the given Election. Votes also contains TimeStamp which is the last time the row was modified which is used for breaking ties (the earlier vote wins). A candidate may have run in multiple elections. How do I find how many elections a given candidate has ever won?
All help greatly appreciated, thanks.
Some sample data:
CREATE TABLE IF NOT EXISTS `Votes` (
`ElectionID` int(11) unsigned NOT NULL,
`CandidateID` int(11) unsigned NOT NULL,
`Count` smallint(5) unsigned NOT NULL DEFAULT '0',
`stamp` int(11) unsigned NOT NULL,
PRIMARY KEY (`ElectionID`,`CandidateID`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `Votes` (`ElectionID`, `CandidateID`, `Count`, `stamp`)
VALUES
(1, 1, 3, 1332897534),
(4, 1, 3, 1333149930),
(4, 4, 2, 1333149947),
(4, 5, 3, 1333149947),
(1, 4, 4, 1333153373);
Desired output: One row, with one column, being the number of wins for a certain candidate

You can write:
SELECT COUNT(1)
FROM Elections AS e
INNER
JOIN Votes AS v1 -- representing the candidate of interest
ON v1.ElectionID = e.ID
AND v1.CandidateID = ...
LEFT
OUTER
JOIN Votes AS v2 -- representing a candidate who beat the candidate of interest
ON v2.ElectionID = e.ID
AND ( v2.Count > v1.Count
OR ( v2.Count = v1.Count
AND v2.stamp < v1.stamp
)
)
WHERE v2.ElectionID IS NULL -- meaning that no candidate beat the candidate of interest
;
(It's also possible to represent either or both of those joins with EXISTS and a correlated subquery; or the first join could be changed to IN with an uncorrelated subquery; but the above is the most likely to perform best, IMHO, and my experience on StackOverflow has been that people seem to like joins better than subqueries for some reason. If you'd prefer a subquery answer, let me know.)

SELECT CandidateID, MAX(Count) FROM Votes GROUP BY ElectionID
should do the trick

Your query basically needs to return every election and WHO Won it. Then apply that result to the specific candidate your are interested in finding out how many that person won out of all elections. Ex: in the U.S. Republican Race, you have 4 candidates... 2 are really the only real considered by most regardless of party affiliation. Each party runs their campaign in each state and they all have their respective votes tallied. So, at the end of ex: 20 states, you will only have 21 winners, but who won how many. Candidate "A" may win 10, "B" wins 6, "C" wins 3 and "D" wins 2. So if you wanted to know how many Candidate "B" won, your answer desired is 6... from my impression of your question.
This will give you all qualifying "First Place" elections for a given candidate. If all you care about is the HOW MANY, you can just change the Prequery.fields to COUNT(*). If you want to get the candidate's name and the name/info of the election, you can add that as join conditions AFTER the PreQuery has been executed.
select
PreQuery.idVotes,
PreQuery.CandidateID,
PreQuery.ElectionID,
PreQuery.Votes,
PreQuery.LastEntry
from
( select
v.*,
#WinRow := if( #LastElection = v.ElectionID, #WinRow +1, 1 ) as FinalPlace,
#LastElection := v.ElectionID as ignoreMe
from
Votes v,
( select #WinRow := 0, #LastElection := 0 ) sqlvars
order by
v.ElectionID,
v.Votes DESC,
v.LastEntry ASC ) PreQuery
where
PreQuery.FinalPlace = 1
AND PreQuery.CandidateID = CandidateIDYouAreInterestedIn

Basically what you want to do is group the Votes rows by ElectionID, and order by Count descending, stamp ascending. That will give you a result set of ordered Votes rows, with the "winners" of each election as the first row within each group.
Next, you want to select these first rows within each group and discard the rest (see here for how to do a Top-N query: http://www.sqlines.com/mysql/how-to/get_top_n_each_group).
Finally, you want to select count(*) from this result set where CandidateID = whatever candidate you're looking for. Alternatively, you can group by CandidateID and leave out the where clause if you want the number of wins for all candidates instead of a specific one.
Hope this helps.

Apparently I'm late to the party, but this is the first thing I though of:
First have one subquery to select the winning count for each unique electionid in Votes, call this table wins.
Then, join wins with Votes where electionid and count are equal. Because there may be a tie, we also need to choose the Votes row with the lowest stamp, so we'll group by electionid and count but this time choose the minimum stamp. We'll call this resulting table wins_with_stamp/wws for short.
Now, wins_with_stamp has all of the rows from Votes that are "winning" rows, so selecting how many a particular candidate won is just a matter of a where candidateid = ? clause.
-- Returns how many Votes rows that is the winner of its election
-- and candidateid is the candidate in question
select count(*)
from Votes v2
right join (
-- Gets the earliest stamp for the votes with the winning count for each election
select v.electionid, v.count, min(v.stamp) as minstamp
from Votes v
right join (
-- Gets the winning count for each election
select electionid, max(count) as max
from Votes
group by electionid
) wins on wins.max = v.count and wins.electionid = v.electionid
group by electionid, count
) wws on wws.count = v2.count and wws.electionid = v2.electionid and wws.minstamp = v2.stamp
where candidateid = [YOUR_CANDIDATEID]

Related

Sum of 2 different non matching columns from 2 different tables

This s my first question on SO, so please bear if I am not super clear! I am trying to sum up values of 2 columns from 2 tables. Both table values have a common ID called 'imid'. These imids are further divided as 'pmid' (say 1 imid can have one or multiple pmids). Both tables have different structures. I would want to sum values on 1 column from table1 and another column from table(to use it for a php calculation). When I try JOIN it always gives me a timeout error. Query below.
SELECT F.`imid` AS imid, SUM( F.Impression ) AS si, D.accmgr AS accmgr, D.cmname AS cmname,sum(D.Item_Impression_Cap) AS sim
FROM trasactions F, rawdata D
WHERE F.`imid` = D.`imid`
AND F.EntryDate LIKE '2017-%-%'
GROUP BY D.`imid`, F.`imid` ORDER BY F.`imid` ASC
I get results, but not even close to the correct numbers.(eg) Impression as 6,557,824 instead of 1,233,287 for a particular imid.
Not sure where I am wrong! Any help would be great...
Edit: Thanks for the responses...I managed to write the query...
SELECT F.`imid`, F.si , F.imname,D.Item_Start_Date ,D.sim, D.Item_End_Date, D.accmgr, D.cmname
FROM (SELECT `imid` AS imid, SUM(Impression ) AS si, adname AS imname, EntryDate FROM trasactions GROUP BY imid) F LEFT JOIN
(SELECT imid,Item_Start_Date, Item_End_Date, accmgr AS accmgr, cmname AS cmname, sum(Item_Impression_Cap) AS sim FROM rawdata GROUP BY imid) D ON F.`imid`=D.`imid`
WHERE D.cmname IS NOT NULL
GROUP BY F.`imid`
ORDER BY F.`imid` ASC
Now there is a new question! I am doing some calculations based on the array values derived from the query above...(eg) $pacing = $row['si']/$avgday*100;
Say I am listing the 'Pacing' for all items active. Would it be possible to count values from variables. (eg) Would want to show the count of items where $pacing is less than 100%. Would it even be possible to do that!! Thanks again.

Is this SQLite Query efficient?

i'm having a good time coding a little visitor counter. it's a PHP5/SQLite3 mix.
made two database tables, one for the visitors, and one for the hits. structure and sample data:
CREATE TABLE 'visitors' (
'id' INTEGER DEFAULT NULL PRIMARY KEY AUTOINCREMENT,
'ip' TEXT DEFAULT NULL,
'hash' TEXT DEFAULT NULL,
UNIQUE(ip)
);
INSERT INTO "visitors" ("id","ip","hash") VALUES ('1','1.2.3.4','f9702c362aa9f1b05002804e3a65280b');
INSERT INTO "visitors" ("id","ip","hash") VALUES ('2','1.2.3.5','43dc8b0a4773e45deab131957684867b');
INSERT INTO "visitors" ("id","ip","hash") VALUES ('3','1.2.3.6','9ae1c21fc74b2a3c1007edf679c3f144');
CREATE TABLE 'hits' (
'id' INTEGER DEFAULT NULL PRIMARY KEY AUTOINCREMENT,
'time' INTEGER DEFAULT NULL,
'visitor_id' INTEGER DEFAULT NULL,
'host' TEXT DEFAULT NULL,
'location' TEXT DEFAULT NULL
);
INSERT INTO "hits" ("id","time","visitor_id","host","location") VALUES ('1','1418219548','1','localhost','/some/path/example.php');
INSERT INTO "hits" ("id","time","visitor_id","host","location") VALUES ('2','1418219550','1','localhost','/some/path/example.php');
INSERT INTO "hits" ("id","time","visitor_id","host","location") VALUES ('3','1418219553','1','localhost','/some/path/example.php');
INSERT INTO "hits" ("id","time","visitor_id","host","location") VALUES ('4','1418219555','2','localhost','/some/path/example.php');
INSERT INTO "hits" ("id","time","visitor_id","host","location") VALUES ('5','1418219557','1','localhost','/some/path/example.php');
INSERT INTO "hits" ("id","time","visitor_id","host","location") VALUES ('6','1418219558','3','localhost','/some/path/example.php');
i now want to fetch the visitors data, but only from those who where active in the last 30 seconds for example. i need the following data as output, here with user id 1 as example:
$visitor = Array(
[id] => 1
[ip] => 1.2.3.4
[hash] => f9702c362aa9f1b05002804e3a65280b
[first_hit] => 1418219548
[last_hit] => 1418219557
[last_host] => localhost
[last_location] => /some/path/example.php
[total_hits] => 4
[idle_since] => 11
)
i'll get this with my current query, all good, but as you can see i need a lot of sub-selects for this:
SELECT
visitors.id,
visitors.ip,
visitors.hash,
(SELECT hits.time FROM hits WHERE hits.visitor_id = visitors.id ORDER BY hits.id ASC LIMIT 1) AS first_hit,
(SELECT hits.time FROM hits WHERE hits.visitor_id = visitors.id ORDER BY hits.id DESC LIMIT 1) AS last_hit,
(SELECT hits.host FROM hits WHERE hits.visitor_id = visitors.id ORDER BY hits.id DESC LIMIT 1) AS last_host,
(SELECT hits.location FROM hits WHERE hits.visitor_id = visitors.id ORDER BY hits.id DESC LIMIT 1) AS last_location,
(SELECT COUNT(hits.id) FROM hits WHERE hits.visitor_id = visitors.id) AS total_hits,
(SELECT strftime('%s','now') - hits.time FROM hits WHERE hits.visitor_id = visitors.id ORDER BY hits.id DESC LIMIT 1) AS idle_since
FROM visitors
WHERE idle_since < 30
ORDER BY last_hit DESC
so, is this ok for my use case or do you know a better approach to get this data out of those two tables? i already played around with JOINS, but no matter how i tweaked it, COUNT() gave me wrong outputs, like user id 1 has only one total hit for example.
i probably have to re-model the database, if i wanna use JOINS properly, i guess.
Update: based on AeroX' Answer i've built the new query. it basically had just one little bug. you can't have MAX() in a WHERE clause. using HAVING now after the GROUPING.
i also tested both the old and the new one with EXPLAIN and EXPLAIN QUERY PLAN. looks much better. Thank you guys!
SELECT
V.id,
V.ip,
V.hash,
MIN(H.time) AS first_hit,
MAX(H.time) AS last_hit,
strftime('%s','now') - MAX(H.time) AS idle_since,
COUNT(H.id) AS total_hits,
LH.host AS last_host,
LH.location AS last_location
FROM visitors AS V
INNER JOIN hits AS H ON (V.id = H.visitor_id)
INNER JOIN (
SELECT visitor_id, MAX(id) AS id
FROM hits
GROUP BY visitor_id
) AS L ON (V.id = L.visitor_id)
INNER JOIN hits AS LH ON (L.id = LH.id)
GROUP BY V.id, V.ip, V.hash, LH.host, LH.location
HAVING idle_since < 30
ORDER BY last_hit DESC
You probably want to clean this up but this should give you the idea of how to make the joins and how to use the GROUP BY statement to aggregate your hits table for each visitor. This should be more efficient then using lots of sub-queries.
I've included comments on the joins so that you can see why I'm making them.
SELECT
V.id,
V.ip,
V.hash,
MIN(H.time) AS first_hit,
MAX(H.time) AS last_hit,
COUNT(H.id) AS total_hits,
strftime('%s','now') - MAX(H.time) AS idle_since,
LH.host AS last_host,
LH.location AS last_location
FROM visitors AS V
-- Join hits table so we can calculate aggregates (MIN/MAX/COUNT)
INNER JOIN hits AS H ON (V.id = H.visitor_id)
-- Join a sub-query as a table which contains the most recent hit.id for each visitor.id
INNER JOIN (
SELECT visitor_id, MAX(id) AS id
FROM hits
GROUP BY visitor_id
) AS L ON (V.id = L.visitor_id)
-- Use the most recent hit.id for each visitor.id to fetch that most recent row (for last_host/last_location)
INNER JOIN hits AS LH ON (L.id = LH.id)
GROUP BY V.id, V.ip, V.hash, LH.host, LH.location
HAVING idle_since < 30
ORDER BY last_hit DESC
One of the best ways to measure query performance is using explain.
From sqlite
The EXPLAIN QUERY PLAN SQL command is used to obtain a high-level
description of the strategy or plan that SQLite uses to implement a
specific SQL query. Most significantly, EXPLAIN QUERY PLAN reports on
the way in which the query uses database indices. This document is a
guide to understanding and interpreting the EXPLAIN QUERY PLAN output.
Background information is available separately:
Notes on the query optimizer.
How indexing works.
The next generation query planner.
An EXPLAIN QUERY PLAN command returns zero or more rows of four
columns each. The column names are "selectid", "order", "from",
"detail". The first three columns contain an integer value. The final
column, "detail", contains a text value which carries most of the
useful information.
EXPLAIN QUERY PLAN is most useful on a SELECT statement, but may also
be appear with other statements that read data from database tables
(e.g. UPDATE, DELETE, INSERT INTO ... SELECT).
An example of an explain query is:
EXPLAIN SELECT * FROM COMPANY WHERE Salary >= 20000;
http://www.tutorialspoint.com/sqlite/sqlite_explain.htm
Below are more complex usage examples.
How can I analyse a Sqlite query execution?

How to order this specific Inner Joins?

Right now I'm creating an online game where I list the last transfers of players.
The table that handles the history of players, has the columns history_join_date and history_end_date.
When history_end_date is filled, it means that player left a club, and when it is like the default (0000-00-00 00:00:00) and history_join_date has some date it means player joined the club (in that date).
Right now, I've the following query:
SELECT
player_id,
player_nickname,
team_id,
team_name,
history_join_date,
history_end_date
FROM
players
INNER JOIN history
ON history.history_user_id = players.player_id
INNER JOIN teams
ON history.history_team_id = teams.team_id
ORDER BY
history_end_date DESC,
history_join_date DESC
LIMIT 7
However, this query returns something like (filtered with PHP above):
(22-Aug-2012 23:05): Folha has left Portuguese Haxball Team.
(22-Aug-2012 00:25): mancini has left United.
(21-Aug-2012 01:29): PatoDaOldSchool has left Reign In Power.
(22-Aug-2012 23:37): Master has joined Born To Win.
(22-Aug-2012 23:28): AceR has joined Born To Win.
(22-Aug-2012 23:08): Nasri has joined Porto Club of Haxball.
(22-Aug-2012 18:53): Lloyd Banks has joined ARRIBA.
PHP Filter:
foreach ($transfers as $transfer) {
//has joined
if($transfer['history_end_date']<$transfer['history_join_date']) {
$type = ' has joined ';
$date = date("d-M-Y H:i", strtotime($transfer['history_join_date']));
} else {
$type = ' has left ';
$date = date("d-M-Y H:i", strtotime($transfer['history_end_date']));
}
As you can see, in the transfers order, the date is not being followed strictly (22-Aug => 21-Aug => 22-Aug).
What am I missing in the SQL?
Regards!
The issue is you are ordering based upon two different values. So your results are ordered first by history_end_date, and when the end dates are equal (i.e. when it is the default value), they are then ordered by history_join_date
(Note that your first results are all ends, and then your subsequent results are all joins, and each subset is properly ordered).
How much control do you have over this data structure? You might be able to restructure the history table such that there is only a single date, and a history type of JOINED or END... You might be able to make a view of joined_date and end_date and sort across that...
From what you have in the question I made up the following DDL & Data:
create table players (
player_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
player_nickname VARCHAR(255) NOT NULL UNIQUE
);
create table teams (
team_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
team_name VARCHAR(255) NOT NULL UNIQUE
);
create table history (
history_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
history_user_id INT NOT NULL, history_team_id INT NOT NULL,
history_join_date DATETIME NOT NULL,
history_end_date DATETIME NOT NULL DEFAULT "0000-00-00 00:00:00"
);
insert into players VALUES
(1,'Folha'),
(2,'mancini'),
(3,'PatoDaOldSchool'),
(4,'Master'),
(5,'AceR'),
(6,'Nasri'),
(7,'Lloyd Banks');
insert into teams VALUES
(1,'Portuguese Haxball Team'),
(2,'United'),
(3,'Reign In Power'),
(4,'Born To Win'),
(5,'Porto Club of Haxball'),
(6,'ARRIBA');
insert into history VALUES
(DEFAULT,1,1,'2012-08-01 00:04','2012-08-22 23:05'),
(DEFAULT,2,2,'2012-08-21 19:04','2012-08-22 00:25'),
(DEFAULT,3,3,'2012-08-19 01:29','2012-08-21 01:29'),
(DEFAULT,4,4,'2012-08-22 23:37',DEFAULT),
(DEFAULT,5,4,'2012-08-22 23:28',DEFAULT),
(DEFAULT,6,5,'2012-08-22 23:08',DEFAULT),
(DEFAULT,7,6,'2012-08-22 18:53',DEFAULT);
SOLUTION ONE - History Event View
This is obviously not the only solution (and you'd have to evaluate options as they suit your needs, but you could create a view in MySQL for your history events and join to it and use it for ordering similar to the following:
create view historyevent (
event_user_id,
event_team_id,
event_date,
event_type
) AS
SELECT
history_user_id,
history_team_id,
history_join_date,
'JOIN'
FROM history
UNION
SELECT
history_user_id,
history_team_id,
history_end_date,
'END'
FROM history
WHERE history_end_date <> "0000-00-00 00:00:00";
Your select then becomes:
SELECT
player_id,
player_nickname,
team_id,
team_name,
event_date,
event_type
FROM players
INNER JOIN historyevent
ON historyevent.event_user_id = players.player_id
INNER JOIN teams
ON historyevent.event_team_id = teams.team_id
ORDER BY
event_date DESC;
Benefit here is you can get both joins and leaves for the same player.
SOLUTION TWO - Pseudo column. use the IF construction to pick one or the other column.
SELECT
player_id,
player_nickname,
team_id,
team_name,
history_join_date,
history_end_date,
IF(history_end_date>history_join_date,history_end_date,history_join_date) as order_date
FROM
players
INNER JOIN history
ON history.history_user_id = players.player_id
INNER JOIN teams
ON history.history_team_id = teams.team_id
ORDER BY
order_date DESC;
Building from #Barmar's answer, you can also use GREATEST() to pick the greatest of the arguments. (MAX() is a grouping function... not actually what you're looking for)
I think what you want is:
ORDER BY MAX(history_join_date, history_end_date)

Need expert advice on complex nested queries

I have 3 queries. I was told that they were potentially inefficient so I was wondering if anyone who is experienced could suggest anything. The logic is somewhat complex so bear with me.
I have two tables: shoutbox, and topic. Topic stores all information on topics that were created, while shoutbox stores all comments pertaining to each topic. Each comment comes with a group labelled by reply_chunk_id. The earliest timestamp is the first comment, while any following with the same reply_chunk_id and a later timestamp are replies. I would like to find the latest comment for each group that was started by the user (made first comment) and if the latest comment was made this month display it.
What I have written achieves that with one problem: all the latest comments are displayed in random order. I would like to organize these groups/latest comments. I really appreciate any advice
Shoutbox
Field Type
-------------------
id int(5)
timestamp int(11)
user varchar(25)
message varchar(2000)
topic_id varchar(35)
reply_chunk_id varchar(35)
Topic
id mediumint(8)
topic_id varchar(35)
subject_id mediumint(8)
file_name varchar(35)
topic_title varchar(255)
creator varchar(25)
topic_host varchar(255)
timestamp int(11)
color varchar(10)
mp3 varchar(75)
custom_background varchar(55)
description mediumtext
content_type tinyint(1)
Query
$sql="SELECT reply_chunk_id FROM shoutbox
GROUP BY reply_chunk_id
HAVING count(*) > 1
ORDER BY timestamp DESC ";
$stmt16 = $conn->prepare($sql);
$result=$stmt16->execute();
while($row = $stmt16->fetch(PDO::FETCH_ASSOC)){
$sql="SELECT user,reply_chunk_id, MIN(timestamp) AS grp_timestamp
FROM shoutbox WHERE reply_chunk_id=? AND user=?";
$stmt17 = $conn->prepare($sql);
$result=$stmt17->execute(array($row['reply_chunk_id'],$user));
while($row2 = $stmt17->fetch(PDO::FETCH_ASSOC)){
$sql="SELECT t.topic_title, t.content_type, t.subject_id,
t.creator, t.description, t.topic_host,
c1.message, c1.topic_id, c1.user, c1.timestamp AS max
FROM shoutbox c1
JOIN topic t ON (t.topic_id = c1.topic_id)
WHERE reply_chunk_id = ? AND c1.timestamp > ?
ORDER BY c1.timestamp DESC, c1.id
LIMIT 1";
$stmt18 = $conn->prepare($sql);
$result=$stmt18->execute(array($row2['reply_chunk_id'],$month));
while($row3 = $stmt18->fetch(PDO::FETCH_ASSOC)){
Make the first query:
SELECT reply_chunk_id FROM shoutbox
GROUP BY reply_chunk_id
HAVING count(*) > 1
ORDER BY timestamp DESC
This does the same, but is faster.
Make sure you have an index on reply_chunk_id.
The second query:
SELECT user,reply_chunk_id, MIN(timestamp) AS grp_timestamp
FROM shoutbox WHERE reply_chunk_id=? AND user=?
The GROUP BY is unneeded, because only one row gets returned, because of the MIN() and the equality tests.
The third query:
SELECT t.topic_title, t.content_type, t.subject_id,
t.creator, t.description, t.topic_host,
c1.message, c1.topic_id, c1.user, c1.timestamp AS max
FROM shoutbox c1
JOIN topic t ON (t.topic_id = c1.topic_id)
WHERE reply_chunk_id = ? AND c1.timestamp > ?
ORDER BY c1.timestamp DESC, c1.id
LIMIT 1
Doing it all in one query:
SELECT
t.user,t.reply_chunk_id, MIN(t.timestamp) AS grp_timestamp,
t.topic_title, t.content_type, t.subject_id,
t.creator, t.description, t.topic_host,
c1.message, c1.topic_id, c1.user, c1.timestamp AS max
FROM shoutbox c1
INNER JOIN topic t ON (t.topic_id = c1.topic_id)
LEFT JOIN shoutbox c2 ON (c1.id = c2.id and c1.timestamp < c2.timestamp)
WHERE c2.timestamp IS NULL AND t.user = ?
GROUP BY t.reply_chunk_id
HAVING count(*) > 1
ORDER BY t.reply_chunk_id
or the equivalent
SELECT
t.user,t.reply_chunk_id, MIN(t.timestamp) AS grp_timestamp,
t.topic_title, t.content_type, t.subject_id,
t.creator, t.description, t.topic_host,
c1.message, c1.topic_id, c1.user, c1.timestamp AS max
FROM shoutbox c1
INNER JOIN topic t ON (t.topic_id = c1.topic_id)
WHERE c1.timestamp = (SELECT max(timestamp) FROM shoutbox c2
WHERE c2.reply_chunk_id = c1.reply_chunk_id)
AND t.user = ?
GROUP BY t.reply_chunk_id
HAVING count(*) > 1
ORDER BY t.reply_chunk_id
How does this work?
The group by selects one entry per topic.reply_chunk_id
The left join (c1.id = c2.id and c1.`timestamp` < c2.`timestamp`) + WHERE c2.`timestamp` IS NULL selects only those items from shoutbox which have the highest timestamp. This works because MySQL keeps increasing c1.timestamp to get c2.timestamp to be null as soon as that is true, it c1.timestamp will have reached its maximum value and will select that row within the possible rows to choose from.
If you don't understand point 2, see: http://dev.mysql.com/doc/refman/5.0/en/example-maximum-column-group-row.html
Note that the PDO is autoescaping the fields with backticks
Sounds like most of it should be directly from your ShoutBox table. Prequery to find all "Chunks" the user replied to... of those chunks (and topic_ID since each chunk is always the same topic), get their respective minimum and maximum. Using the "Having count(*) > 1" will force only those that HAVE a second posting by a given user (what you were looking for).
THEN, re-query to the chunks to get the minimum regardless of user. This prevents the need of querying ALL chunks. Then join only what a single user is associated with back to the Topic.
Additionally, and I could be incorrect and need to adjust (minimally), but it appears that the SOUNDBOX table ID column would be an auto-increment column, and just happens to be time-stamped too at time of creation. That said, for a given "Chunk", the earliest ID would be the same as the earliest timestamp as they would be stamped at the same time they are created. Also makes easier on subsequent JOINs and sub query too.
By using STRAIGHT_JOIN, should force the "PreQuery" FIRST, come up with a very limited set, then qualify the WHERE clause and joins afterwords.
select STRAIGHT_JOIN
T.topic_title,
T.content_type,
T.subject_id,
T.creator,
T.description,
T.topic_host,
sb2.Topic_ID
sb2.message,
sb2.user,
sb2.TimeStamp
from
( select
sb1.Reply_Chunk_ID,
sb1.Topic_ID,
count(*) as TotalEntries,
min( sb1.id ) as FirstIDByChunkByUser,
min( sbJoin.id ) as FirstIDByChunk,
max( sbJoin.id ) as LastIDByChunk,
max( sbJoin.timestamp ) as LastTimeByChunk
from
ShoutBox sb1
join ShoutBox sbJoin
on sb1.Reply_Chunk_ID = sbJoin.Reply_Chunk_ID
where
sb1.user = CurrentUser
group by
sb1.Reply_Chunk_ID,
sb1.Topic_ID
having
min( sb1.id ) = min( sbJoin.ID ) ) PreQuery
join Topic T on
PreQuery.Topic_ID = T.ID
join ShoutBox sb2
PreQuery.LastIDByChunk = sb2.ID
where
sb2.TimeStamp >= YourTimeStampCriteria
order by
sb2.TimeStamp desc
EDIT ---- QUERY EXPLANATION -- with Modified query.
I've changed the query from re-reading (as was almost midnight when answered after holiday weekend :)
First, "STRAIGHT_JOIN" is a MySQL clause telling the engine to "do the query in the way / sequence I've stated". Basically, sometimes an engine will try to think for you and optimize in ways that may appear more efficient, but if based on your data, you know what will retrieve the smallest set of data first, and then join to other lookup fields next might in fact be better. Second the "PreQuery". If you have a "SQL-Select" statement (within parens) as Alias "From" clause, The "PreQuery" is just the name of the alias of the resultset... I could have called it anything, just makes sense that this is a stand-alone query of it's own. (Ooops... fixed to ShoutBox :) As for case-sensitivity, typically Column names are NOT case-sensitive... However, table names are... You could have a table name "MyTest" different than "mytest" or "MYTEST". But by supplying "alias", it helps shorten readability (especially with VeryLongTableNamesUsed ).
Should be working after the re-reading and applying adjustments.. Try the first "Prequery" on its own to see how many records it returns. On its own merits, it should return... for a single "CurrentUser" parameter value, every "Reply_Chunk_ID" (which will always have the same topic_id", get the first ID the person entered (min()). By JOINing again to Shoutbox on the chunk id, we (only those qualified as entered by the user), get the minimum and maximum ID per the chunk REGARDLESS of who started or responded. By applying the HAVING clause, this should only return those where the same person STARTED the topic (hence both have the same min() value.)
Finally, once those have been qualified, join directly to the TOPIC and SHOUTBOX tables again on their own merits of topic_id and LastIDByChunk and order the final results by the latest comment response timestamp descending.
I've added a where clause to further limit your "timestamp" criteria where the most recent final timestamp is on/after the given time period you want.
I would be curious how this query's time performance works compared to your already accepted answer too.

Calculating a row position in the table

Let's say I have a table like this (this is just a simplified example, the real table I'm talking about is much more complex):
CREATE TABLE media (
id INT NOT NULL AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
voted INT NOT NULL DEFAULT 0,
rating FLOAT NOT NULL DEFAULT 0
) ENGINE = INNODB;
The voted column represents a number of votes the item has received and the rating column represents the total rating of the item.
Now, what I want to do is select a single item from the table based on id, something like:
SELECT m.* FROM media AS m WHERE id = 5;
But, in addition, I want to calculate the position of this row based on the rating column and fetch that as an additional column, let's say called a site_rank (so the bigger the rating of the row is the higher its site_rank will be, I hope I explained it well). My guess is this can be achieved with a subselect query but I'm not sure how to do it.
Any help?
SELECT count(*) FROM media WHERE rating > (SELECT rating FROM media WHERE id = 5);
This will output high rank for most voted media.
If you want low rank for the most voted (like, the most voted gets the rank of 1), just reverse the sign in the subquery.
SELECT mo.*,
(
SELECT COUNT(*)
FROM media mi
WHERE (mi.rating, mi.id) <= (mo.rating, mo.id)
) AS rank
FROM media mo
WHERE mo.id = 5
SELECT
m1.*,
(SELECT COUNT(*) FROM media AS m2 WHERE m2.rating > m1.rating) AS site_rank
FROM
media AS m1
WHERE
id = 5;
Note that this does not define a complete ordering because there might be items with equal rating and they will report the same site rank.
Does MySQL support the standard SQL rank() function? It does exactly what you want.
select
*,
rank() over (
order by rating desc
) as site_rank
from media

Categories