How to order this specific Inner Joins? - php

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)

Related

Join a 3rd table when the result of the initial join has NULL values (that I want retained!)

I have 3 tables:
event_timestamps with colums Race_number, timestamp
event_entry with Race_number, User_id
user with user_id, Firstname, lastname
I want to find race_numbers that are not linked to a user_id, and count laps while I'm at it by Joining event_timestamps and event_entry.
select event_entry.user_id, max(timestamp), event_timestamps.race_number, count(event_timestamps.race_number)
from event_timestamps
left join event_entry on event_timestamps.race_number = event_entry.race_number and event_entry.event_id=430
where timestamp > '2022-05-28 11:50:00' and timestamp < '2022-05-29'
group by event_timestamps.race_number
order by count(event_timestamps.race_number) desc , max(timestamp);
Output
user_id
max(timestamp)
race_number
count(event...)
NULL
2022-05-28 12:30:01
1000
5
14694
2022-05-28 12:30:02
32
5
37617
2022-05-28 12:30:17
44
5
16134
2022-05-28 12:34:37
24
5
But when I join tbl.user the Null value disappears. I want to display the NULL so I can see if we are missing user data.
This query sort of works but the NULL value is not displaying:
select user.firstname, user.lastname, max(timestamp), event_timestamps.race_number, count(event_timestamps.race_number)
from event_timestamps
left join event_entry on event_timestamps.race_number = event_entry.race_number
inner join user on user.user_id = event_entry.user_id
where timestamp > '2022-05-28 11:50:00' and timestamp < '2022-05-29' and event_entry.event_id=430
group by event_timestamps.race_number
order by count(event_timestamps.race_number) desc , max(timestamp);```
firstname
lastname
max(timestamp)
race_number
count(event....)
Albert
Coles
12:30:02
32
5
Vince
Butre
12:30:17
44
5
John
Plessis
12:34:37
24
5
So I want race_number 1000 (for example) to display as well with NULL values in firstname, lastname.
Any assistance would be much appreciated because this is breaking my novice brain!
The events_timestamps has multiple occurances of the same race_number as the user completes laps. We count the laps and creat rank by using the last lap time (MAX timestamp) and sorting from there.
Try it like this:
SELECT user.firstname, user.lastname, s.timestamp, s.race_number, s.user_id, s.count
FROM (
SELECT event_entry.user_id as user_id, max(timestamp) as timestamp, event_timestamps.race_number as race_number, count(event_timestamps.race_number) as count
from event_timestamps
Left join event_entry on event_timestamps.race_number = event_entry.race_number and event_entry.event_id=430
Where timestamp > '2022-05-28 11:50:00' and timestamp < '2022-05-29'
group by event_timestamps.race_number, event_entry.user_id
) s
LEFT JOIN user on user.user_id = s.user_id
order by s.count desc , s.timestamp
like #Marshal_c said, inner join doesn't work, cause it gets rid of NULLs, viz. SQL JOIN and different types of JOINs
also like #Honk_der_Hase points out, your code is missing group by for all columns. When your SQL works, i asume, that you work in MariaDB, which takes in this case first found element (equivalent of ORALCE's TOP())
In your scenario, you should also have some kind of integrity rule, which will prevent from having multiple users (user_id) with same race number (race_number). If you had thouse records in your database, the rows would start duplicating.
Also be aware, that some databases (like MariaDB) can have problems with table called user because table named like that is used for authentication into database (in information_scheme)

How to get 2 columns from one table and 2 rows as columns from other table in one row, in MySQL?

I know this is quite complicated, but I sincerely hope someone will check this out.
I made short version (to better understand the problem) and full version (with original SQL)
Short version:
[TABLE A] [TABLE B]
|1|a|b| |1|x
|2|c|d| |1|y
|3| | | |2|z
|5| | | |2|v
|4|w
How can I make MySQL query to get rows like that:
1|a|b|x|y
2|c|d|z|v
2 columns from A and 2 rows from B as columns, only with keys 1 and 2, no empty results
Subquery?
Full version:
I tried to get from Prestashop db in one row:
product id
ean13 code
upc code
feature with id 24
feature with id 25
It's easy to get id_product, ean13 and upc, as it's one row in ps_product table. To get features I used subqueries (JOIN didn't work out).
So, I selected id_product, ean13, upc, (subquery1) as code1, (subquery2) as code2.
Then I needed to throw out empty rows. But couldn't just put code1 or code2 in WHERE.
To make it work I had to put everything in subquery.
This code WORKS, but it is terribly ugly and I bet this should be done differently.
How can I make it BETTER?
SELECT * FROM(
SELECT
p.id_product as idp, p.ean13 as ean13, p.upc as upc, (
SELECT
fvl.value
FROM
`ps_feature_product` fp
LEFT JOIN
`ps_feature_value_lang` fvl ON (fp.id_feature_value = fvl.id_feature_value)
WHERE fp.id_feature = 24 AND fp.id_product = idp
) AS code1, (
SELECT
fvl.value
FROM
`ps_feature_product` fp
LEFT JOIN
`ps_feature_value_lang` fvl ON (fp.id_feature_value = fvl.id_feature_value)
WHERE fp.id_feature = 25 AND fp.id_product = idp
) AS code2,
m.name
FROM
`ps_product` p
LEFT JOIN
`ps_manufacturer` m ON (p.id_manufacturer = m.id_manufacturer)
) mainq
WHERE
ean13 != '' OR upc != '' OR code1 IS NOT NULL OR code2 IS NOT NULL
create table tablea
( id int,
col1 varchar(1),
col2 varchar(1));
create table tableb
( id int,
feature int,
cola varchar(1));
insert into tablea (id, col1, col2)
select 1,'a','b' union
select 2,'c','d' union
select 3,null,null union
select 5,null,null;
insert into tableb (id, feature, cola)
select 1,24,'x' union
select 1,25,'y' union
select 2,24,'z' union
select 2,25,'v' union
select 4,24,'w';
select a.id, a.col1, a.col2, b1.cola b1a, b2.cola b2a
from tablea a
inner join tableb b1 on (b1.id = a.id and b1.feature = 24)
inner join tableb b2 on (b2.id = a.id and b2.feature = 25);
SQLFiddle here.
What you want to do is called a Pivot Query. MySQL has no native support for pivot queries, though other RDBMSen do.
You can simulate a pivot query with derived columns, but you must specify each derived column. That is, it is impossible in MySQL itself to have the number of columns match rows of another table. This has to be known ahead of time.
It would be much easier to query the results as rows and then use PHP to do the aggregation into columns. For example:
while ($row = $result->fetch()) {
if (!isset($table[$row->id])) {
$table[$row->id] = array();
}
$table[$row->id][] = $row->feature;
This is not a simple question because it's not a standard query, by the way if you can make use of views you can do the following procedure. Assuming you're starting from this tables:
CREATE TABLE `A` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`firstA` char(1) NOT NULL DEFAULT '',
`secondA` char(1) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
);
CREATE TABLE `B` (
`id` int(11) unsigned NOT NULL,
`firstB` char(1) NOT NULL DEFAULT ''
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `A` (`id`, `firstA`, `secondA`)
VALUES (1, 'a', 'b'), (2, 'c', 'd');
INSERT INTO `B` (`id`, `firstB`)
VALUES (1, 'x'), (1, 'y'), (2, 'z'), (2, 'v'), (4, 'w');
First create a view that joins the two tables:
create or replace view C_join as
select A.firstA, A.secondA, B.firstB
from A
join B on B.id=A.id;
Create the view that groups the rows in table B:
create or replace view d_group_concat as
select firstA, secondA, group_concat(firstB) groupconcat
from c_join
group by firstA, secondA
Create the view that does what you need:
create or replace view e_result as
select firstA, secondA, SUBSTRING_INDEX(groupconcat,',',1) firstB, SUBSTRING_INDEX(SUBSTRING_INDEX(groupconcat,',',2),',',-1) secondB
from d_group_concat
And that's all. Hope this helps you.
If you can't create views, this could be the query:
select firstA, secondA, SUBSTRING_INDEX(groupconcat,',',1) firstB, SUBSTRING_INDEX(SUBSTRING_INDEX(groupconcat,',',2),',',-1) secondB
from (
select firstA, secondA, group_concat(firstB) groupconcat
from (
select A.firstA, A.secondA, B.firstB
from A
join B on B.id=A.id
) c_join
group by firstA, secondA
) d_group_concat
Big thanks to everyone for the answers. James's answer was first, simplest and works perfectly in my case. The query runs several times faster than mine, with subqueries. Thanks, James!
Just a few words why I needed that:
It's a part of integration component for Prestashop and wholesale exchange platform. There are 4 product code systems that wholesalers use on the platform (ean13, upc and 2 other systems). Those 2 other product codes are added as product feature in Prestashop. There are thousands of products on the shop and hundreds of thousands of products on the platform. Which is why speed is crucial.
Here is the code for full version of my question. Maybe someone will find this helpful.
Query to get Prestashop product codes and certain features in one row:
SELECT
p.id_product, p.ean13, p.upc, fvl1.value as code1, fvl2.value as code2
FROM `ps_product` p
LEFT JOIN
`ps_feature_product` fp1 ON (p.id_product = fp1.id_product and fp1.id_feature = 24)
LEFT JOIN
`ps_feature_value_lang` fvl1 ON (fvl1.id_feature_value = fp1.id_feature_value)
LEFT JOIN
`ps_feature_product` fp2 ON (p.id_product = fp2.id_product and fp2.id_feature = 25)
LEFT JOIN
`ps_feature_value_lang` fvl2 ON (fvl2.id_feature_value = fp2.id_feature_value)
WHERE
ean13 != '' OR upc != '' OR fvl1.value IS NOT NULL OR fvl2.value IS NOT NULL;

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

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]

Retrieving Data From Associated Toxi Table

I'm retrieving my data for part of my site with a typical MySQL query and echoing out the results from various fields etc etc from my main table whose structure is not important but which has a unique id which is 'job_id'
In order to have multiple catagories associated with that 'job_id' i have employed a toxi solution which associates catgories to each 'job_id'.
TABLE `tags` (
`tag_id` INT NOT NULL AUTO_INCREMENT,
`tag_name` VARCHAR(20) NOT NULL,
PRIMARY KEY (`tag_id`)
)
CREATE TABLE `tag_relational` (
`job_id` INT NOT NULL,
`tag_id` INT NOT NULL
)
What i want to do is, when i echo out the info from the main table (using 'job_id') i also want to echo all the catagories which that job_id is matched against.
The query below only returns the first catagory(tag_name) that the job_id is listed against, when it should be up to six (at the moment):
$query = "SELECT * FROM tags t
JOIN tag_relational r
ON t.tag_id=r.tag_id
WHERE r.job_id = $job_id";
$result=mysql_query($query) or die(mysql_error());
$cats=mysql_fetch_assoc($result);
In my code i'm using this to echo out the matched catagories:
<?php echo $cats['tag_name'];?>
Can someone explain how i can get ALL the catagory names to echo out rather than just the first?
Thanks
Dan
BTW, apologies to mu is too short who kindly answered my question when i had dummy/less complete information above.
If you just want to list the category names, then you could use group_concat sort of like this:
select b.*,
group_concat(c.category_name order by c.category_name separator ' ,') as cats
from business b
join tbl_works_categories w on b.id = w.bus_id
join categories c on w.category_id = c.category_name
where ...
group by b.id
You'd need a proper WHERE clause of course. That will give you the usual stuff from business and the category names as a comma delimited list in cats.
If you need the category IDs as well, then two queries might be better: one to get the business information and a second to collect the categories:
select w.bus_id, c.category_id, c.category_name
from tbl_works_categories w
join categories c
where w.bus_id IN (X)
where X is a comma delimited list of business ID values. Then you'd patch things up on the client side.

MySQL query for selecting a maximum element

I have a table with 4 columns: place_id, username, counter, last_checkin
I'm writing a check-in based system and I'm trying to get a query that will give me the "mayor" of each place. The mayor is the one with most check-ins, and if there is more than 1 than the minimum last_checkin wins.
For example, if I have:
place_id, username, counter, last_checkin
123, tom, 3 , 13/4/10
123, jill, 3, 14/4/10
365, bob, 2, 15/4/10
365, alice, 1, 13/4/10
I want the result to be:
123, tom
365, bob
I'm using it in PHP code
Here is the test data:
CREATE TABLE `my_table` ( `place_id` int(11), `username` varchar(50), `counter` int(11), `last_checkin` date);
INSERT INTO `my_table` VALUES (123,'tom',3,'2010-04-13'),(123,'jill',3,'2010-04-14'),(365,'bob',2,'2010-04-15'),(365,'alice',1,'2010-04-13');
How about..
SELECT
place_id,
(SELECT username
FROM my_table MT2
WHERE MT2.place_id = MT1.place_id
ORDER BY counter DESC, last_checkin ASC
LIMIT 1) AS mayor
FROM my_table MT1
GROUP BY place_id;
Edited as Unreason suggests to have ascending order for last_checkin.
Brian's correlated query is something I would write. However I found this different take and it might perform differently depending on the data
SELECT
mt1.place_id,
mt1.username
FROM
my_table mt1 LEFT JOIN my_table mt2
ON mt1.place_id = mt2.place_id AND
(mt1.counter < mt2.counter OR
(mt1.counter = mt2.counter AND mt1.last_checkin > mt2.last_checkin)
)
WHERE
mt2.place_id IS NULL
Which uses left join to get to the top records according to certain conditions.
$data = query("SELECT max(counter) counter,username FROM table GROUP By place_id ORDER By last_checkin DESC");

Categories