GROUP BY + HAVING ignore row - mysql

Basically what I wanted is that I can select all the race records with record holder and best time. I looked up about similar queries and managed to find 3 queries that were faster than the rest.
The problem is it completely ignores the race the userid 2 owns the record of.
These are my tables, indexes, and some sample data:
CREATE TABLE `races` (
`raceid` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL,
PRIMARY KEY (`raceid`),
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `users` (
`userid` mediumint(8) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL,
PRIMARY KEY (`userid`),
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `race_times` (
`raceid` smallint(5) unsigned NOT NULL,
`userid` mediumint(8) unsigned NOT NULL,
`time` mediumint(8) unsigned NOT NULL,
PRIMARY KEY (`raceid`,`userid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
INSERT INTO `races` (`raceid`, `name`) VALUES
(1, 'Doherty'),
(3, 'Easter Basin Naval S'),
(5, 'Flint County'),
(6, 'Fort Carson'),
(4, 'Glen Park'),
(2, 'Palomino Creek'),
(7, 'Tierra Robada');
INSERT INTO `users` (`userid`, `name`) VALUES
(1, 'Player 1'),
(2, 'Player 2');
INSERT INTO `race_times` (`raceid`, `userid`, `time`) VALUES
(1, 1, 51637),
(1, 2, 50000),
(2, 1, 148039),
(3, 1, 120516),
(3, 2, 124773),
(4, 1, 101109),
(6, 1, 89092),
(6, 2, 89557),
(7, 1, 77933),
(7, 2, 78038);
So if I run these 2 queries:
SELECT rt1.raceid, r.name, rt1.userid, p.name, rt1.time
FROM race_times rt1
LEFT JOIN users p ON (rt1.userid = p.userid)
JOIN races r ON (r.raceid = rt1.raceid)
WHERE rt1.time = (SELECT MIN(rt2.time) FROM race_times rt2 WHERE rt1.raceid = rt2.raceid)
GROUP BY r.name;
or..
SELECT rt1.*, r.name, p.name
FROM race_times rt1
LEFT JOIN users p ON p.userid = rt1.userid
JOIN races r ON r.raceid = rt1.raceid
WHERE EXISTS (SELECT NULL FROM race_times rt2 WHERE rt2.raceid = rt1.raceid
GROUP BY rt2.raceid HAVING MIN(rt2.time) >= rt1.time);
I receive correct results as shown below:
raceid | name | userid | name | time |
-------+----------------------+--------+----------+--------|
1 | Doherty | 2 | Player 2 | 50000 |
3 | Easter Basin Naval S | 1 | Player 1 | 120516 |
6 | Fort Carson | 1 | Player 1 | 89092 |
4 | Glen Park | 1 | Player 1 | 101109 |
2 | Palomino Creek | 1 | Player 1 | 148039 |
7 | Tierra Robada | 1 | Player 1 | 77933 |
and here is the faulty query:
SELECT rt.raceid, r.name, rt.userid, p.name, rt.time
FROM race_times rt
LEFT JOIN users p ON p.userid = rt.userid
JOIN races r ON r.raceid = rt.raceid
GROUP BY r.name
HAVING rt.time = MIN(rt.time);
and the result is this:
raceid | name | userid | name | time |
-------+----------------------+--------+----------+--------|
3 | Easter Basin Naval S | 1 | Player 1 | 120516 |
6 | Fort Carson | 1 | Player 1 | 89092 |
4 | Glen Park | 1 | Player 1 | 101109 |
2 | Palomino Creek | 1 | Player 1 | 148039 |
7 | Tierra Robada | 1 | Player 1 | 77933 |
As you can see, race "Doherty" (raceid: 1) is owned by "Player 2" (userid: 2) and it is not shown along with the rest of race records (which are all owned by userid 1). What is the problem?
Regards,

Having is a post filter. The query gets all the results, and then further filters them based on having. The GROUP BY compacting the rows based on the group, which gives you the first entry in each set. Since player 1 is the first entry for race 1, that's the result that is being processed by the HAVING. It is then filtered out because its time does not equal the MIN(time) for the group result.
This is why the other ones you posted are using a sub-query. My personal preference is for the first example, as to me it's slightly easier to read. Performance wise they should be the same.
While it's not a bad idea to try and avoid sub queries in the where clause, this is mostly valid when you can accomplish the same result with a JOIN. Other times it's not possible to get the result with a JOIN and a sub query is required.

Related

MySQL Order by show specific data at the top

I have a below data in my activity table. I want to show those records at the top whose followup_date is today onwards in ascending order, after that those records whose followup_date is past date in ascending order and after that those records whose followup_date is null.
DROP TABLE IF EXISTS `activity`;
CREATE TABLE IF NOT EXISTS `activity` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`type_id` int(11) NOT NULL,
`followup_date` date DEFAULT NULL,
`created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;
--
-- Dumping data for table `activity`
--
INSERT INTO `activity` (`id`, `type_id`, `followup_date`, `created`) VALUES
(1, 1, '2022-03-22', '2022-03-24 18:51:23'),
(2, 1, '2022-03-23', '2022-03-24 18:51:23'),
(3, 1, '2022-03-24', '2022-03-24 18:51:58'),
(4, 1, '2022-03-25', '2022-03-24 18:51:58'),
(5, 1, '2022-03-26', '2022-03-24 18:52:21'),
(6, 1, '2022-03-13', '2022-03-24 18:52:21'),
(7, 1, NULL, '2022-03-24 18:54:15'),
(8, 1, NULL, '2022-03-24 18:54:15');
I tried using below query but could not understand how would i use ORDER BY CASE statement to get the result mentioned below.
SELECT * FROM `activity` ORDER BY CASE WHEN followup_date IS NULL THEN 2 WHEN followup_date >= '2022-03-24' THEN 1 END ASC
Current Output:
Expected Output
What changes i will need to make in above query to get Expected Output
I moved the expression into the select-list so we could see it in the result, but you may keep it in the ORDER BY clause:
SELECT CASE WHEN followup_date IS NULL THEN 2
WHEN followup_date < '2022-03-24' THEN 1
ELSE 0 END AS sort_bucket,
id, followup_date
FROM `activity`
ORDER BY sort_bucket ASC, followup_date ASC
Output:
+-------------+----+---------------+
| sort_bucket | id | followup_date |
+-------------+----+---------------+
| 0 | 3 | 2022-03-24 |
| 0 | 4 | 2022-03-25 |
| 0 | 5 | 2022-03-26 |
| 1 | 6 | 2022-03-13 |
| 1 | 1 | 2022-03-22 |
| 1 | 2 | 2022-03-23 |
| 2 | 7 | NULL |
| 2 | 8 | NULL |
+-------------+----+---------------+

MySql: how to get the desired result

I've a table like this:
CREATE TABLE `base_build_floor` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`build_no` varchar(64) NOT NULL,
`build_name` varchar(64) DEFAULT NULL,
`floor_no` varchar(64) DEFAULT NULL,
`floor_name` varchar(64) DEFAULT NULL,
PRIMARY KEY (`id`)
)
and insert some data:
INSERT INTO `base_build_floor` VALUES ('41', 'BUILD40210011', 'A', null, null);
INSERT INTO `base_build_floor` VALUES ('42', 'BUILD40210012', 'B', null, null);
INSERT INTO `base_build_floor` VALUES ('43', 'BUILD40210013', 'C', null, null);
INSERT INTO `base_build_floor` VALUES ('44', 'BUILD40210013', 'C', 'FLOOR40210002', 'C1');
INSERT INTO `base_build_floor` VALUES ('45', 'BUILD40210013', 'C', 'FLOOR40210003', 'C2');
INSERT INTO `base_build_floor` VALUES ('46', 'BUILD40210012', 'B', 'FLOOR40210004', 'B1');
the table is about a build-floor table, first you should make a building, then, a building can has no or some floors. the A building has no floor, the B building has one floor named B1, the C building has two floors named C1 and C2, I want to get the result as below:
41 BUILD40210011 A null null
44 BUILD40210013 C FLOOR40210002 C1
45 BUILD40210013 C FLOOR40210003 C2
46 BUILD40210012 B FLOOR40210004 B1
it means that, if a building has no floors, then get it, while if a building has any one floor, the building itself should not be got, so how to write the mysql?I've tried to use Subquery but doesn't work
I've try like this :
SELECT
b.*
FROM
base_build_floor b
WHERE
b.floor_no IS NOT NULL
OR (
b.floor_no IS NULL
AND b.build_no NOT IN (
SELECT
GROUP_CONCAT(nostr)
FROM
(
SELECT
concat("'", f.build_no, "'") as nostr
FROM
base_build_floor f
WHERE
f.floor_no IS NOT NULL
GROUP BY
f.build_no
) t
)
)
but I get all the data
With NOT EXISTS:
select t.* from base_build_floor t
where t.floor_no is not null
or not exists (
select 1 from base_build_floor
where build_no = t.build_no and floor_no is not null
)
See the demo.
Results:
| id | build_no | build_name | floor_no | floor_name |
| --- | ------------- | ---------- | ------------- | ---------- |
| 41 | BUILD40210011 | A | | |
| 44 | BUILD40210013 | C | FLOOR40210002 | C1 |
| 45 | BUILD40210013 | C | FLOOR40210003 | C2 |
| 46 | BUILD40210012 | B | FLOOR40210004 | B1 |
This query would be much simpler if you had normalized tables. Ideally, you would have a buildings table with building id, no, and name, and a floors table with building id, floor no, and floor name. Then you could just join the two tables. Since that's not the case, we can basically extract the building and floor sub-tables from the main one and join them like this:
SELECT
b.build_no,
b.build_name,
f.floor_no,
f.floor_name
FROM
(SELECT DISTINCT build_no, build_name
FROM base_build_floor) b
LEFT OUTER JOIN
(SELECT *
FROM base_build_floor
WHERE floor_no IS NOT NULL) f ON b.build_no = f.build_no

Sort records on multiple columns and conditions

I have the below table, that stores the rank of person participating in respective events.
event_running and event_jumping are the events and the ranks stored.
CREATE TABLE `ranks` (
`id` int(11) NOT NULL,
`personid` int(11) NOT NULL,
`event_running` int(11) DEFAULT NULL,
`event_longjump` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
Sample data
INSERT INTO `ranks` (`id`, `personid`, `event_running`, `event_longjump`) VALUES
(1, 1, 4, 8),
(2, 2, 10, 6),
(3, 3, 5, 0),
(4, 5, 20, 1),
(5, 4, 9, 3),
(6, 6, 1, 2);
SQL Fiddle Link
I want to build a leaderboard as below
| Standing | PersonID | RunningRank | JumpingRank |
| 1 | 6 | 1 | 2 |
| 2 | 4 | 9 | 3 |
| 3 | 1 | 4 | 8 |
| 4 | 3 | 5 | 0 |
| 5 | 2 | 10 | 6 |
This has to be sorted in ascending order - irrespective of the events lowest come first and also ranks above 20 are ignored.
And inputs on how can this be done?
you can use something similar to below
select PersonID,
RunningRank,
JumpingRank,
(RunningRank + JumpingRank) as Total
from ranks
order by Total asc
limit 20;
Here's your query.
set #row_number = 0;
select (#row_number:=#row_number + 1) as standing, personid, event_running, event_longjump from ranks
where event_running < 20 and event_longjump < 20
order by
case when if(event_longjump=0,event_running ,event_longjump) < event_running
then event_longjump else event_running end
see dbfiddle
Your sorting criteria is a bit vague. I am assuming that you want to sort on the basis of cumulative of the ranks across all events and its jumping score.
Also, please explain the position of person Id 3 in your queation.
You can do,
select PersonID,
RunningRank,
JumpingRank,
(JumpingRank + RunningRank) as cumulativeRank
from ranks
ORDER BY cumulativeRank, JumpingRank aesc
limit 20;
This will get you all the positions baring person id 3

Getting most recent row per group in MySQL [duplicate]

This question already has answers here:
Retrieving the last record in each group - MySQL
(33 answers)
Closed 3 years ago.
I am trying to get all most recent Paper.material's for each Book.isbn. I am using left to get the base of each Book.isbn, since they can have a suffix that indicates the revision, for example : 'X1726748384Z1' or 'X1837943875Z2' etc.. I tried over partition by but it didn't work because MySQL 5.7 doesn't support this. How can I make this work?
GOAL
For each Book.isbn as X:
select left(Book.isbn, 11) BaseISBN, Paper.material, Book.date
from Book
join Page on Book.id = Page.book_id
join Paper on Page.id = Paper.page_id
where Book.name = 'world'
and left(Book.isbn, 11) = X <--
and Page.name = 'test'
order by Book.date desc
limit 1
QUERY
select left(Book.isbn, 11) BaseISBN, Paper.material, max(Book.date)
from Book
join Page on Book.id = Page.book_id
join Paper on Page.id = Paper.page_id
where Book.name = 'world'
and left(Book.isbn, 11) in('X1726748384', 'X1837943875')
and Page.name = 'test'
group by left(Book.isbn, 11);
RETURNS
| Book.isbn | Paper.material | max(Book.date) |
|-------------|----------------|----------------------|
| X1726748384 | 10134248300B | 2018-01-01T00:00:00Z |
| X1837943875 | 10985782343F | 2021-01-01T00:00:00Z |
etc...
SHOULD RETURN
| Book.isbn | Paper.material | max(Book.date) |
|-------------|----------------|----------------------|
| X1726748384 | 10985782343E | 2018-01-01T00:00:00Z |
| X1837943875 | 10985782343H | 2021-01-01T00:00:00Z |
etc...
SQL FIDDLE
And DDLs of same:
CREATE TABLE `Book` (
`id` INT NOT NULL AUTO_INCREMENT,
`isbn` varchar(255),
`name` varchar(255),
`date` DATETIME,
PRIMARY KEY (`id`)
);
CREATE TABLE `Page` (
`id` INT NOT NULL AUTO_INCREMENT,
`name` varchar(255),
`book_id` INT NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `Paper` (
`id` INT NOT NULL AUTO_INCREMENT,
`material` varchar(255),
`page_id` INT NOT NULL,
PRIMARY KEY (`id`)
);
ALTER TABLE `Page` ADD CONSTRAINT `Page_fk0` FOREIGN KEY (`book_id`) REFERENCES `Book`(`id`);
ALTER TABLE `Paper` ADD CONSTRAINT `Paper_fk0` FOREIGN KEY (`page_id`) REFERENCES `Page`(`id`);
INSERT INTO `Book` (`isbn`, `name`, `date`) VALUES
('X1234234403', 'hello', '2016-01-01'),
('X1726748384', 'world', '2017-01-01'),
('X1726748384Z1', 'world', '2018-01-01'),
('X1837943875', 'world', '2019-01-01'),
('X1837943875Z1', 'world', '2020-01-01'),
('X1837943875Z2', 'world', '2021-01-01');
INSERT INTO `Page` (`name`, `book_id`) VALUES
('bla', 1),
('test', 2),
('test', 3),
('test', 4),
('test', 5),
('test', 6);
INSERT INTO `Paper` (`material`, `page_id`) VALUES
('10134248300A', 1),
('10134248300B', 2),
('10134248300C', 2),
('10985782343D', 3),
('10985782343E', 3),
('10985782343F', 4),
('10985782343G', 5),
('10985782343H', 6);
'Etc.' is not very useful when trying to understand what should and shouldn't be included in a result.
Nevertheless, perhaps you're after something a bit like this...
SELECT b.*
, z.material
FROM book b
JOIN
( SELECT LEFT(isbn,11) isbn11
, MAX(date) date
FROM book
WHERE
( isbn LIKE 'X1726748384%'
OR isbn LIKE 'X1837943875%'
)
AND name = 'world'
GROUP
BY isbn11
) x
ON x.isbn11 = LEFT(b.isbn,11)
AND x.date = b.date
JOIN page y
ON y.book_id = b.id
JOIN paper z
ON z.page_id = y.id
WHERE y.name = 'test';
+----+---------------+-------+---------------------+--------------+
| id | isbn | name | date | material |
+----+---------------+-------+---------------------+--------------+
| 3 | X1726748384Z1 | world | 2018-01-01 00:00:00 | 10985782343D |
| 3 | X1726748384Z1 | world | 2018-01-01 00:00:00 | 10985782343E |
| 6 | X1837943875Z2 | world | 2021-01-01 00:00:00 | 10985782343H |
+----+---------------+-------+---------------------+--------------+
3 rows in set (0.00 sec)

How to filter on master table in a left join query

I have 2 tables POST and COMMENT,
each post has ID, TITLE, CATEGORY_ID AND USER_ID
each comment has ID, COMMENT, POST_ID AND USER_ID
I want to list all posts having category_id=2 and comment.user_id=1
CREATE TABLE post
(`ID` int, `TITLE` varchar(9), `CATEGORY_ID` int, `USER_ID` int)
;
INSERT INTO post
(`ID`, `TITLE`, `CATEGORY_ID`, `USER_ID`)
VALUES
(1, 'My post A', 1, 1),
(2, 'My post B', 2, 1),
(3, 'My post C', 2, 2)
;
CREATE TABLE comment
(`ID` int, `COMMENT` varchar(12), `POST_ID` int, `USER_ID` int)
;
INSERT INTO comment
(`ID`, `COMMENT`, `POST_ID`, `USER_ID`)
VALUES
(1, 'My comment X', 1, 1),
(2, 'My comment Y', 2, 1),
(3, 'My comment Z', 1, 2)
;
This command fetch all posts including the one with category_id = 1
SELECT post.*, comment.comment, comment.post_id, comment.user_id c_user_id
FROM post
LEFT JOIN COMMENT
ON POST.id = COMMENT.post_id
AND COMMENT.user_id=1
AND POST.category_id =2
I get this
+---------+-----------+-------------+---------+--------------+---------+-----------+
| USER_ID | TITLE | CATEGORY_ID | USER_ID | COMMENT | POST_ID | C_USER_ID |
+---------+-----------+-------------+---------+--------------+---------+-----------+
| 1 | MY POST A | 1 | 1 | null | null | null |
| 2 | MY POST B | 2 | 1 | My Comment Y | 2 | 1 |
| 1 | MY POST C | 2 | 2 | null | null | null |
+---------+-----------+-------------+---------+--------------+---------+-----------+
and I'd like to get this (all posts with category_id = 2 so 2 records, where the comment for user_id appears and the other comment to null as below:
+---------+-----------+-------------+---------+--------------+---------+-----------+
| USER_ID | TITLE | CATEGORY_ID | USER_ID | COMMENT | POST_ID | C_USER_ID |
+---------+-----------+-------------+---------+--------------+---------+-----------+
| 2 | MY POST B | 2 | 1 | My Comment Y | 2 | 1 |
| 1 | MY POST C | 2 | 2 | null | null | null |
+---------+-----------+-------------+---------+--------------+---------+-----------+
Thank you in advance for your help
That rule about putting conditions in the on clause for left outer joins . . . well, it applies to conditions on the second table, not the first. So, put that condition in a where clause:
SELECT post.*, comment.comment, comment.post_id, comment.user_id c_user_id
FROM post LEFT JOIN
COMMENT
ON POST.id = COMMENT.post_id AND COMMENT.user_id=1
WHERE POST.category_id = 2;
The way the left outer join works, conceptually is to say: Take a row in the first table. Then find all matching rows in the second table subject to the on condition. Keep all the matches. If there are no matches, keep the row in the first table.
Guess what. This is still the logic, even when the condition filters rows on the first table. So, filtering a left outer join on conditions in the first table has no effect.
All of this similarly applies to right outer joins with all the table references reversed.