Get most recent result from a LEFT JOIN column - mysql

I'm creating a custom forum from scratch and I'm attempting to use some LEFT JOIN queries to get information such as total posts, total threads and most recent thread. I've managed to get the data but the recent thread keeps returning a random value rather than the most recent thread.
CREATE TABLE forum_categories
(`name` varchar(18), `label` varchar(52), `id` int)
;
INSERT INTO forum_categories
(`name`, `label`, `id`)
VALUES
('General Discussion', 'Talk about anything and everything Digimon!', 1),
('Deck Discussion', 'Talk about Digimon TCG Decks and Strategies!', 2),
('Card Discussion', 'Talk about Digimon TCG Cards!', 3),
('Website Feedback', 'A place to discuss and offer feedback on the website', 4)
;
CREATE TABLE forum_topics
(`name` varchar(18), `id` int, `parent_id` int, `author_id` int, date date)
;
INSERT INTO forum_topics
(`name`, `id`, `parent_id`, `author_id`, `date`)
VALUES
('My First Topic', 1, 1, 16, '2021-03-29'),
('My Second Topic', 2, 1, 16, '2021-03-30')
;
CREATE TABLE forum_topics_content
(`id` int, `topic_id` int, `author_id` int, date datetime, `content` varchar(300))
;
INSERT INTO forum_topics_content
(`id`, `topic_id`, `author_id`, `date`, `content`)
VALUES
(1, 1, 16, '2021-03-29 15:46:55', 'Hey guys! This is my first post!'),
(2, 1, 16, '2021-03-30 08:05:13', 'This is my first topic reply!')
;
My Query:
SELECT forum_categories.name, label, forum_categories.id, COUNT(DISTINCT(forum_topics.id)) as 'topics', COUNT(DISTINCT(forum_topics_content.id)) as 'posts', SUBSTRING(forum_topics.name,1, 32) as 'thread'
FROM forum_categories
LEFT JOIN forum_topics ON forum_categories.id = forum_topics.parent_id
LEFT JOIN forum_topics_content ON forum_topics.id = forum_topics_content.topic_id
GROUP BY forum_categories.id
ORDER BY forum_categories.id, forum_topics.date DESC
I figured having an ORDER BY of forum_topics.date DESC would work for me and output the most recent thread which is "My Second Topic" but it doesn't.
I'm a bit stumped and have tried different variations of ORDER BY to no avail.
thread keeps returning a random result from the two possible results.
Full example with data is available on this fiddle: https://www.db-fiddle.com/f/auDzUABaEpYzLKDkRqE7ok/0
Desired result would 'thread' always being the latest thread which in this example is "My Second Topic". However it always seems to randomly pick between "My First Topic" and "My Second Topic".
The output for the first row should always be:
'General Discussion' , 'Talk about anything and everything Digimon!' 1, 2, 2, 'My Second Topic'

thread keeps returning a random result from the two possible results.
Provided query is simply undeterministic and equivalent to:
SELECT forum_categories.name,
forum_categories.label,
forum_categories.id,
COUNT(DISTINCT(forum_topics.id)) as 'topics',
COUNT(DISTINCT(forum_topics_content.id)) as 'posts',
SUBSTRING(ANY_VALUE(forum_topics.name),1, 32) as 'thread'
FROM forum_categories
LEFT JOIN forum_topics ON forum_categories.id = forum_topics.parent_id
LEFT JOIN forum_topics_content ON forum_topics.id = forum_topics_content.topic_id
GROUP BY forum_categories.id,forum_categories.name,forum_categories.label
ORDER BY forum_categories.id, ANY_VALUE(forum_topics.date) DESC;
Assuming that forum_categories.id is PRIMARY KEY, the name/label are functionally dependent but rest of the column is simply ANY_VALUE.
If a column in SELECT list is not functionally dependent or wrapped with aggregate function the query is incorrect. On MySQL 8.0 or when ONLY_FULL_GROUP_BY is enabled the result is error.
Related: Group by clause in mySQL and postgreSQL, why the error in postgreSQL?
There are different ways to achieve desired result(correlated subqueries, windowed functions, limit) and so on.
Here using GROUP_CONCAT:
SELECT forum_categories.name,
forum_categories.label,
forum_categories.id,
COUNT(DISTINCT(forum_topics.id)) as `topics`,
COUNT(DISTINCT(forum_topics_content.id)) as `posts`,
SUBSTRING_INDEX(GROUP_CONCAT(SUBSTRING(forum_topics.name,1,32)
ORDER BY forum_topics.`date` DESC
SEPARATOR '~'),
'~',1) AS `thread`
FROM forum_categories
LEFT JOIN forum_topics ON forum_categories.id = forum_topics.parent_id
LEFT JOIN forum_topics_content ON forum_topics.id = forum_topics_content.topic_id
GROUP BY forum_categories.id,forum_categories.name,forum_categories.label
ORDER BY forum_categories.id;
How it works:
GROUP_CONCAT is aggregate function that allow to concatenate string preserving order.
My Second Topic~My First Topic~My First Topic
Then SUBSTRING_INDEX returns part of string up to first occurence of delimeter ~.
db<>fiddle demo

In you fiddle you have:
SET SESSION sql_mode = '';
You should change that to:
SET SESSION sql_mode = 'ONLY_FULL_GROUP_BY';
You will get an error like this:
Query Error: Error: ER_WRONG_FIELD_WITH_GROUP: Expression #1 of
SELECT list is not in GROUP BY clause and contains nonaggregated
column 'test.forum_categories.name' which is not functionally
dependent on columns in GROUP BY clause; this is incompatible with
sql_mode=only_full_group_by
It is stated in the docs that:
ONLY_FULL_GROUP_BY
Reject queries for which the select list, HAVING condition, or ORDER
BY list refer to nonaggregated columns that are neither named in the
GROUP BY clause nor are functionally dependent on (uniquely determined
by) GROUP BY columns.
As of MySQL 5.7.5, the default SQL mode includes ONLY_FULL_GROUP_BY.
(Before 5.7.5, MySQL does not detect functional dependency and
ONLY_FULL_GROUP_BY is not enabled by default. For a description of
pre-5.7.5 behavior, see the MySQL 5.6 Reference Manual.)
And there is a very good reason why they did that.
see: why should not disable only full group by

Related

MySQL: How can I make this subquery work while updating a value?

I'm trying to write a query that lists an Item ID, Title, old price, and new price. I can't do an update statement because then I don't think I could list the old price.
INSTRUCTION:
USE A SUBQUERY to Increase price of all items by ‘No Rest for the Weary’ by 10%. Show prices before and after. Rollback after.
There are three main goals to this assignment based on instruction:
1. Show old price.
2. Show new price through calculation using a subquery.
3. Use a rollback once finished.
Questions:
Is it possible to put an UPDATE inside of a SELECT statement like a subquery?
(Answer is apparently no.)
Where I've run into issue:
UPDATE items
SET unit_price = ROUND(unit_price + (unit_price * .10),2) as 'New Price'
WHERE item_id =
(SELECT item_id as 'Item ID',
title as 'Title',
unit_price as 'Old Price', --Except this won't work because the unit price is now the new price...
FROM items
WHERE title = 'No Rest for the Weary'
);
This is what I have now, but the ROLLBACK bit has me stuck. Where would one put that in this situation? Or have I misunderstood the instructions entirely?
SELECT item_id as 'Item ID', title as 'Title', unit_price as 'Price',
ROUND(unit_price + (unit_price * .10),2) as 'New Price'
FROM items
WHERE item_id =
(SELECT item_id
FROM items
WHERE title = 'No Rest for the Weary'
);
No, it's not possible to include an UPDATE statement within a SELECT statement. But it is possible to include a SELECT inside an UPDATE.
Why do you need an UPDATE statement? Is the intent to update a column in a table with a new value, so the new value is persisted?
Or, is the intent to return a resultset? An UPDATE statement doesn't return a resultset like a SELECT statement does.
The UPDATE statement in the question won't work, because there are three columns being returned by the subquery. Used in the context
col IN (subquery)
The subquery should return only one column. It's invalid to return two or more columns.
You can write a SELECT statement that returns the result of an expression. In this example, a new unit price as a 10% increase over the current unit_price...
SELECT i.item_id AS `item_id`
, i.title AS `title`
, i.unit_price AS `old_unit_price`
, ROUND(i.unit_price*1.1,2) AS `new_unit_price`
FROM items i
WHERE i.title = 'No Rest for the Weary'
ORDER BY i.item_id
If that's return the result you want, and you want to use that in an UPDATE to assign a new value to the unit_price column, assuming item_id is the PRIMARY or UNIQUE KEY on the items table...
UPDATE ( SELECT r.*
FROM ( -- select statement above goes here
SELECT i.item_id AS `item_id`
, i.title AS `title`
, i.unit_price AS `old_unit_price`
, ROUND(i.unit_price*1.1,2) AS `new_unit_price`
FROM items i
WHERE i.title = 'No Rest for the Weary'
) r
) s
JOIN items t
ON t.item_id = s.item_id
SET t.unit_price = s.new_unit_price
Again, to emphasize the point, this assumes that item_id is the PRIMARY KEY (or a non-NULL UNIQUE KEY) on the items table.
Following the UPDATE statement, re-running the original SELECT query will return different results (assuming the original unit_price was sufficiently greater than zero.)
FOLLOWUP
Create query that shows old price
The first SELECT in the answer above shows the "old price" (assuming that the "old price" is stored in the unit_price column.)
Change the price to 10% more than the current price.
The first SELECT in the answer above shows adding 10% to the unit_price column, rounded to two digits after the decimal point, returned as another column new_unit_price.
Use a subquery
The first query doesn't use a subquery. But we could easily add one. (I'm not understanding why we need to add an unnecessary subquery. Does it matter what that subquery returns, can we use a subquery in the SELECT list, or in the WHERE clause, does an inline view qualify as a subquery?)
This version adds two unnecessary correlated subqueries to the SELECT list, an unnecessary inline view, and an unnecessary subquery in the WHERE clause.
SELECT i.item_id AS `item_id`
, ( SELECT t.title
FROM items t
WHERE t.item_id = i.item_id
ORDER BY t.title
LIMIT 1
) AS `title`
, ( SELECT p.unit_price
FROM items p
WHERE p.item_id = i.item_id
ORDER BY p.unit_price
LIMIT 1
) AS `old_unit_price`
, ROUND(i.unit_price*1.1,2) AS `new_unit_price`
FROM items i
CROSS
JOIN ( SELECT 1 AS i ) i
WHERE i.title = (SELECT 'No Rest for the Weary')
ORDER BY i.item_id
A simpler version of the UPDATE statement (to add 10% to unit_price) also doesn't require a subquery.
UPDATE items t
SET t.unit_price = ROUND(t.unit_price*1.1,2)
WHERE t.title = 'No Rest for the Weary'
Again, it's not possible for an UPDATE statement to return a resultset.

MySQL Query Fixing/Optimisation for my configuration table

I got a mySQL table, that holds the configuration of my project, each configuration change creates a new entry, so that i have a history of all changes, and who changed it.
CREATE TABLE `configurations` (
`name` varchar(255) NOT NULL,
`value` text NOT NULL,
`lastChange` datetime NOT NULL,
`changedBy` bigint(32) NOT NULL,
KEY `lastChange` (`lastChange`),
KEY `name` (`name`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `configurations` (`name`, `value`, `lastChange`, `changedBy`) VALUES
('activePageLimit', 'activePageLimit-old-value', '2016-01-06 12:25:05', 1096775260340178),
('activePageLimit', 'activePageLimit-new-value', '2016-01-06 12:27:57', 1096775260340178),
('customerLogo', 'customerLogo-old-value', '2016-02-06 00:00:00', 1096775260340178),
('customerLogo', 'customerLogo-new-value', '2016-01-07 00:00:00', 1096775260340178);
Right now i have a problem with my select query, that should return all names and their latest value (ordered by lastChange).
| name | value | lastChange |
|-----------------|---------------------------|---------------------------|
| customerLogo | customerLogo-new-value | January, 07 2016 00:00:00 |
| activePageLimit | activePageLimit-new-value | January, 06 2016 12:27:57 |
My current Query is:
SELECT `name`, `value`, `lastChange`
FROM (
SELECT `name`, `value`, `lastChange`
FROM `configurations`
ORDER BY `lastChange` ASC
) AS `c`
GROUP BY `name` DESC
But unfortunately this does not always return the right values, and i don't like to use a subquery, there has to be a cleaner and faster way to do this.
I also created a SQL-Fiddle for you as a playground: http://sqlfiddle.com/#!9/f1dc9/1/0
Is there any other clever solution i missed?
Your method is documented to return indeterminate results (because you have columns in the select that are not in the group by).
Here are three alternatives. The first is standard SQL, using an explicit aggregation to get the most recent change.
SELECT c.*
FROM configurations c JOIN
(SELECT `name`, MAX(`lastChange`) as maxlc
FROM `configurations`
GROUP BY name
) mc
ON c.name = mc.name and c.lasthange = mc.maxlc ;
The second is also standard SQL, using not exists:
select c.*
from configurations c
where not exists (select 1
from configurations c2
where c2.name = c.name and c2.lastchange > c.lastchange
);
The third uses a hack which is available in MySQL (and it assumes that the value does not have any commas in this version and is not too long):
select name, max(lastchange),
substring_index(group_concat(value order by lastchange desc), ',', 1) as value
from configurations
order by name;
Use this version carefully, because it is prone to error (for instance, the intermediate group_concat() result could exceed a MySQL parameter, which would then have to be re-set).
There are other methods -- such as using variables. But these three should be sufficient for you to consider your options.
If we want to avoid SUBQUERY the only other option is JOIN
SELECT cc.name, cc.value, cc.lastChange FROM configurations cc
JOIN (
SELECT name, value, lastChange
FROM configurations
ORDER BY lastChange ASC
) c on c.value = cc.value
GROUP BY cc.name DESC
You have two requirements: a historical log, and a "state". Keep them in two different tables, in spite of that providing redundant information.
That is, have one table that faithfully records who changed what when.
Have another table that faithfully specifies the current state for the configuration.
Plan A: INSERT into the Log and UPDATE the `State whenever anything happens.
Plan B: UPDATE the State and use a TRIGGER to write to the Log.

How to Update Two tables with single JOIN statement in MYSQL server?

I have read that A JOIN statement can connect two tables for update and I'm having a hard time warping my brain around it..
I have 7 tables and growing all different languages all different charsets.
I need to update and insert 3 columns..
sudo naming
table1EN
table1DE
table1ZH
table1PT
table1FR
table1ES
table1RU
The tables are 100% the same structure..
To do an update now I use this query
UPDATE `Table1EN` SET `Details`= 'String Value', `Name` = 'String Name',
`Info`= 1 WHERE `ID` = 1;
and then repeat 5000 times and change table1EN to table1** and re-run that
Is there a way to simplify this to?
UPDATE `Table1EN`,`Table1ZH`,`Table1DE`,`Table1FR`,`Table1RU`,`Table1ES`,`Table1PT`
SET `Details`= 'String Value', `Name` = 'String Name', `Info`= 1 WHERE `ID` = 1;
Running this query returns ambiguous name details, name and info.
using a join statement seems to lock up the server..
My questions are how can I run a multi-table update query where all values are the same nothing changes? Not lock up the server? Not have an ambiguous name error? Finally not have to run the query in chucks of 5000 after renaming the table?
Update1:
As Arth has pointed below, I did not include the actual JOIN Query here.
UPDATE table1EN
INNER JOIN table1ZH USING (ID)
INNER JOIN table1DE USING (ID)
INNER JOIN table1FR USING (ID)
INNER JOIN table1PT USING (ID)
INNER JOIN table1ES USING (ID)
INNER JOIN table1RU USING (ID)
SET table1EN.Info = 1, table1EN.Details ='String Value',
table1ZH.Info = 1, table1ZH.Details ='String Value',
table1DE.Info = 1, table1DE.Details ='String Value',
table1FR.Info = 1, table1FR.Details ='String Value',
table1ES.Info = 1, table1ES.Details ='String Value',
table1RU.Info = 1, table1RU.Details ='String Value',
table1PT.Info = 1, table1PT.Details ='String Value'
WHERE table1EN.ID = 1;
I'm posting it now in hopes to simplify it to stop the server from crashing anytime I try to run it with 5000 different query's at one time.
I have tried reducing this based on something I read
to
set table1EN.Info = 1, table1EN.Details ='String Value',
table1ZH.Info=table1EN.Info,
table1DE.Info=table1EN.Info,
table1FR.Info=table1EN.Info
etc ........
However this seems to cause even more server lag and crashes witch I would expect..
First off when you do the UPDATE JOIN, you haven't included any JOIN conditions.. so that will try and do a cartesian product of each table:
#rows = #rows_table1EN * #rows_table1ZH * ... * #rows_table1PT
You should JOIN on table1EN.id = table1xx.id for each table.
The next problem is that you will have to reference each column you want to change. This will result in:
SET table1EN.detail = 'String Value',
table1ZH.detail = 'String Value'
...
table1PT.detail = 'String Value'
This could be done with dynamically building the statement, but that is fairly hideous.
This all leads me to question your database structure. Did you consider using one table with an extra language column; either the two letter identifier
(OKish) or a foreign key to a languages table (better)?
I wonder if....
Update (
Select ID, details, name, info, 'table1EN' src FROM table1EN UNION ALL
Select ID, details, name, info, 'table1ZH' FROM table1ZH UNION ALL
Select ID, details, name, info, 'table1DE' FROM table1DE UNION ALL
Select ID, details, name, info, 'table1RU' FROM table1RU UNION ALL
Select ID, details, name, info, 'table1FR' FROM table1FR UNION ALL
Select ID, details, name, info, 'table1ES' FROM table1ES UNION ALL
Select ID, details, name, info, 'table1PT' FROM table1PT) b
set `details` = 'String value',
`Name` = 'String Name',
`Info` = 1
where ID=1
would work... based on https://dev.mysql.com/worklog/task/?id=3701
ID and src would be a composite key.

MySQL - Select row only if previous row field was 0?

I have a table as below:
CREATE TABLE IF NOT EXISTS `status`
(`code` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY
,`IMEI` varchar(15) NOT NULL
,`ACC` tinyint(1) NOT NULL
,`datetime` datetime NOT NULL
);
INSERT INTO status VALUES
(1, 123456789012345, 0, '2014-07-09 10:00:00'),
(2, 453253453334445, 0, '2014-07-09 10:05:00'),
(3, 912841851252151, 0, '2014-07-09 10:08:00'),
(4, 123456789012345, 1, '2014-07-09 10:10:00'),
(5, 123456789012345, 1, '2014-07-09 10:15:00');
I need to get all rows for a given IMEI (e.g 123456789012345) where ACC=1 AND the previous row for same IMEI has ACC=0. The rows may be one after the other or very apart.
Given the exampl above, I'd want to get the 4th row (code 4) but not 5th (code 5).
Any ideas? Thanks.
Assuming that you mean previous row by datetime
SELECT *
FROM status s
WHERE s.imei='123456789012345'
AND s.acc=1
AND (
SELECT acc
FROM status
WHERE imei=s.imei
AND datetime<s.datetime
ORDER BY datetime DESC
LIMIT 1
) = 0
The way I would approach this problem is much different from the approaches given in other answers.
The approach I would use would be to
1) order the rows, first by imei, and then by datetime within each imei. (I'm assuming that datetime is how you are going to determine if a row is "previous" to another row.
2) sequentially process the rows, first comparing imei from the current row to the imei from the previous row, and then checking if the ACC from the current row is 1 and the ACC from the previous row is 0. Then I would know that the current row was a row to be returned.
3) for each processed row, in the resultset, include a column that indicates whether the row should be returned or not
4) return only the rows that have the indicator column set
A query something like this:
SELECT t.code
, t.imei
, t.acc
, t.datetime
FROM ( SELECT IF(s.imei=#prev_imei AND s.acc=1 AND #prev_acc=0,1,0) AS ret
, s.code AS code
, #prev_imei := s.imei AS imei
, #prev_acc := s.acc AS acc
, s.datetime AS datetime
FROM (SELECT #prev_imei := NULL, #prev_acc := NULL) i
CROSS
JOIN `status` s
WHERE s.imei = '123456789012345'
ORDER BY s.imei, s.datetime, s.code
) t
WHERE t.ret = 1
(I can unpack that a bit, to explain how it works.)
But the big drawback of this approach is that it requires MySQL to materialize the inline view as a derived table (temporary MyISAM table). If there was no predicate (WHERE clause) on the status table, the inline view would essentially be a copy of the entire status table. And with MySQL 5.5 and earlier, that derived table won't be indexed. So, this could present a performance issue for large sets.
Including predicates (e.g. WHERE s.imei = '123456789' to limit rows from the status table in the inline view query may sufficiently limit the size of the temporary MyISAM table.
The other gotcha with this approach is that the behavior of user-defined variables in the statement is not guaranteed. But we do observe a consistent behavior, which we can make use of; it does work, but the MySQL documentation warns that the behavior is not guaranteed.
Here's a rough overview of how MySQL processes this query.
First, MySQL runs the query for the inline view aliased as i. We don't really care what this query returns, except that we need it to return exactly one row, because of the JOIN operation. What we care about is the initialization of the two MySQL user-defined variables, #prev_imei and #prev_acc. Later, we are going to use these user-defined variables to "preserve" the values from the previously processed row, so we can compare those values to the current row.
The rows from the status table are processed in sequence, according to the ORDER BY clause. (This may change in some future release, but we can observe that it works like this in MySQL 5.1 and 5.5.)
For each row, we compare the values of imei and acc from the current row to the values preserved from the previous row. If the boolean in the IF expression evaluates to TRUE, we return a 1, to indicate that this row should be returned. Otherwise, we return a 0, to indicate that we don't want to return this row. (For the first row processed, we previously initialized the user-defined variables to NULL, so the IF expression will evaluate to 0.)
The #prev_imei := s.imei and #prev_acc := s.acc assigns the values from the current row to the user-defined values, so they will be available for the next row processed.
Note that it's important that the tests of the user-defined variables (the first expression in the SELECT list) before we overwrite the previous values with the values from the current row.
We can run just the query from the inline view t, to observe the behavior.
The outer query returns rows from the inline view that have the derived ret column set to a 1, rows that we wanted to return.
select * from status s1
WHERE
ACC = 1
AND code = (SELECT MIN(CODE) FROM status WHERE acc = 1 and IMEI = s1.IMEI)
AND EXISTS (SELECT * FROM status WHERE IMEI = s1.IMEI AND ACC = 0)
AND IMEI = 123456789012345
SELECT b.code,b.imei,b.acc,b.datetime
FROM
( SELECT x.*
, COUNT(*) rank
FROM status x
JOIN status y
ON y.imei = x.imei
AND y.datetime <= x.datetime
GROUP
BY x.code
) a
JOIN
( SELECT x.*
, COUNT(*) rank
FROM status x
JOIN status y
ON y.imei = x.imei
AND y.datetime <= x.datetime
GROUP
BY x.code
) b
ON b.imei = a.imei
AND b.rank = a.rank + 1
WHERE b.acc = 1
AND a.acc = 0;
you can do a regular IN() and then group any duplicates (you could also use a limit but that would only work for one IMEI)
SETUP:
INSERT INTO `status`
VALUES
(1, 123456789012345, 0, '2014-07-09 10:00:00'),
(2, 453253453334445, 0, '2014-07-09 10:05:00'),
(3, 912841851252151, 0, '2014-07-09 10:08:00'),
(4, 123456789012345, 1, '2014-07-09 10:10:00'),
(5, 123456789012345, 1, '2014-07-09 10:15:00'),
(6, 123456789012345, 1, '2014-07-09 10:15:00'),
(7, 453253453334445, 1, '2014-07-09 10:15:00');
QUERY:
SELECT * FROM status
WHERE ACC = 1 AND IMEI IN(
SELECT DISTINCT IMEI FROM status
WHERE ACC = 0)
GROUP BY imei;
RESULTS:
works with multiple IMEI that have a 0 then a 1... IMAGE
EDIT:
if you would like to go by the date entered as well then you can just order it first by date and then group.
SELECT * FROM(
SELECT * FROM status
WHERE ACC = 1 AND IMEI IN(
SELECT DISTINCT IMEI FROM status
WHERE ACC = 0)
ORDER BY datetime
) AS t
GROUP BY imei;

Mysql: Select most recent entry by user and case number

I have a table whose data looks like this:
INSERT INTO `cm_case_notes` (`id`, `case_id`, `date`, `time`, `description`, `username`, `supervisor`, `datestamp`) VALUES
(45977, '1175', '2010-11-19 16:27:15', 600, 'Motion hearing...Denied.', 'bjones', 'jharvey,', '2010-11-19 21:27:15'),
(46860, '1175', '2010-12-11 16:11:19', 300, 'Semester Break Report', 'bjones', 'jharvey,', '2010-12-11 21:11:19'),
(48034, '1175', '2011-05-04 17:30:03', 300, 'test', 'bjones', 'jharvey,', '2011-05-04 22:30:03'),
(14201, '1175', '2009-02-06 00:00:00', 3600, 'In court to talk to prosecutor, re: the file', 'csmith', 'sandrews', '2009-02-07 14:33:34'),
(14484, '1175', '2009-02-13 00:00:00', 6300, 'Read transcript, note taking', 'csmith', 'sandrews', '2009-02-16 17:22:36');
I'm trying to select the most recent case note (by date) on each case by each user. The best I've come up with is:
SELECT * , MAX( `date` ) FROM cm_case_notes WHERE case_id = '1175' GROUP BY username
This, however, doesn't give the most recent entry, but the first one for each user. I've seen several similar posts here, but I just can't seem to get my brain around them. Would anybody take pity on the sql-deficient and help?
If you want only the dates of the most recent case note for every user and every case, you can use this:
--- Q ---
SELECT case_id
, username
, MAX( `date` ) AS recent_date
FROM cm_case_notes
GROUP BY case_id
, username
If you want all the columns from these row (with most recent date) follow the Quassnoi link for various solutions (or the other provided links). The easiest to write would be to make the above query into a subquery and join it to cm_case_notes:
SELECT cn.*
FROM
cm_case_notes AS cn
JOIN
( Q ) AS q
ON ( q.case_id, q.username, q.recent_date )
= ( cn.case_id, cn.username, cn.`date` )
If you just want the lastet case note but only for a particular case_id, then you could add the where condition in both cn and Q (Q slightly modified):
SELECT cn.*
FROM
cm_case_notes AS cn
JOIN
( SELECT username
, MAX( `date` ) AS recent_date
FROM cm_case_notes
WHERE case_id = #particular_case_id
GROUP BY username
) AS q
ON ( q.username, q.recent_date )
= ( cn.username, cn.`date` )
WHERE cn.case_id = #particular_case_id
the reason why you don't get what would like to fetch from the database is the use of SELECT * together with GROUP.
In fact, only the results of aggregate functions and / or the group field(s) itself can be safely SELECTed. selecting anything else leads to unexpected results. (the exact result depends on order, query optimization and such).
What you are trying to achieve is called fetching a "groupwise maximum". This is a common problem / common task in SQL, you can read a nice writeup here:
http://jan.kneschke.de/projects/mysql/groupwise-max/
or in the MySQL manual here:
http://dev.mysql.com/doc/refman/5.1/en/example-maximum-column-group-row.html
or a detailed long explanation by stackoverflow user Quassnoi here:
http://explainextended.com/2009/11/24/mysql-selecting-records-holding-group-wise-maximum-on-a-unique-column/
Have you considered a DESC ordering and simply limiting 1?