MySQL: select row only where closest to date has column value - mysql

I want to return all rows that were public in May (2019-05), so if a row was turned to draft (and not back to public) at any point before the end of May, I don't want it. For example:
id | post_id | status | date
-------------------------
1 | 1 | draft | 2019-03-25
2 | 1 | public | 2019-04-02
3 | 1 | draft | 2019-05-25
4 | 2 | draft | 2019-03-10
5 | 2 | public | 2019-04-01
6 | 2 | draft | 2019-06-01
The desired result for the above would return post_id 2 because its last status change prior to the end of May was to public.
post_id 1 was put back in draft before the end of May, so it would not be included.
I'm not sure how to use the correct join or sub-queries to do this as efficiently as possible.

You seem to want the status as of 2019-05-31. A correlated subquery seems like the simplest solution:
select t.*
from t
where t.date = (select max(t2.date)
from t t2
where t2.post_id = t.post_id and
t2.date <= '2019-05-31'
);
To get the ones that are public, just add a WHERE condition:
select t.*
from t
where t.date = (select max(t2.date)
from t t2
where t2.post_id = t.post_id and
t2.date <= '2019-05-31'
) and
t.status = 'public';
For performance, you want an index on (post_id, date).
You can also phrase this using a JOIN:
select t.*
from t join
(select t2.post_id, max(t2.date) as max_date
from t t2
where t2.date <= '2019-05-31'
group by t2.post_id
) t2
on t2.max_date = t.date
where t.status = 'public';
I would expect the correlated subquery to have better performance with the right indexes. However, sometimes MySQL surprises me.

we need to determine whether
the status of each post_id is public prior to the month May (the subquery with max(date)),
any post_id exists with status not equals public within the month May,
and then exclude the post_id satisfying the matter 2.
So, you can use :
select distinct t1.post_id
from tab t1
where t1.post_id not in
(
select distinct t1.post_id
from tab t1
join
(
select post_id, max(date) as date
from tab
where '2019-05-01'> date
group by post_id ) t2
on t1.post_id = t2.post_id
where t1.status != 'public'
and t1.date < '2019-06-01'
and t1.date > '2019-04-30'
);
+---------+
| POST_ID |
+---------+
| 2 |
+---------+
Demo

Related

SQL: Max ID Group By With Condition

I'm a SQL newbie, here is my Table:
+----+--------+---------+------------+
| ID | Person | Success | Message |
+----+--------+---------+------------+
| 1 | Alice | 1 | Hello |
+----+--------+---------+------------+
| 2 | Bob | 0 | World |
+----+--------+---------+------------+
| 3 | Alice | 0 | Foo |
+----+--------+---------+------------+
| 4 | Clark | 1 | Bar |
+----+--------+---------+------------+
And I want to select latest (MAX ID assuming ID is incremental) records which is success for each person
Expected result:
+----+--------+---------+------------+
| ID | Person | Success | Message |
+----+--------+---------+------------+
| 4 | Clark | 1 | Bar |
+----+--------+---------+------------+
// Alice's latest record is not success, ignore
// Bob has no success record, ignore
Here is my current approach:
SELECT *
FROM test AS t1
RIGHT JOIN (
SELECT MAX(id) AS max_id
FROM test
GROUP BY Person
) AS t2
ON t1.id = t2.max_id
WHERE t1.success = 1
// Select Max ID group by person, join with original table, then filter by success
And I'm wondering if there are better approach or neater SQL lines for this?
Thank you so much!
SELECT *
FROM test t1
WHERE Success
AND NOT EXISTS ( SELECT NULL
FROM test t2
WHERE t2.id > t1.id
AND t1.person = t2.person
AND NOT Success );
SELECT t1.*
FROM test t1
LEFT JOIN t2 ON t1.person = t2.person
AND t2.id > t1.id
AND NOT t2.Success
WHERE t1.Success
AND t2.id IS NULL;
Use ROW_NUMBER to filter down to the latest record for each person. Then, subquery and retain only success records:
WITH cte AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY Person ORDER BY ID DESC) rn
FROM test
)
SELECT ID, Person, Success, Message
FROM cte
WHERE Success = 1;
For an old school way of doing this, if perhaps you are still using MySQL 5.7, we can try:
SELECT t1.ID, t1.Person, t1.Success, t1.Message
FROM test t1
INNER JOIN
(
SELECT Person, MAX(ID) AS MAX_ID
FROM test
GROUP BY Person
) t2
ON t2.Person = t1.Person AND
t2.MAX_ID = t1.ID
WHERE
t1.Success = 1;
You could use:
select * from test t1
where exists (
select 1 from test
where success
having max(id) = t1.id
)
Sql fiddle
Alternative query:
select * from test t1
where id = (
select max(id) from test
where success
)
Sql fiddle
And I want to select latest (MAX ID assuming ID is incremental) records which is success for each person
One way to do this uses a correlated subquery like this:
select t.*
from test t
where t.status = 'success' and
t.id = (select max(t2.id) from test t2 where t2.person = t.person);
This is pretty much a direct translation of what you are asking. The condition on id is the "latest record" for each person. The status = 'success' is the filter you want.

Join table group by with sort desc

I have 2 table
Table 1
id | value
-----------
1 | a
2 | b
3 | c
4 | d
Table 2
id | table1_id | date
------------------------
1 | 1 | 01-01-2020 1:00:00
2 | 1 | 01-01-2020 2:00:00
3 | 1 | 05-01-2020 1:00:00 (*)
4 | 2 | 05-01-2020 1:00:00
5 | 3 | 06-01-2020 1:00:00
6 | 3 | 06-01-2020 2:00:00 (*)
7 | 2 | 07-01-2020 1:00:00 (*)
I want to join table 1 to table 2. get row of table 2 is max value date and group by table1_id
Like exxample, i want get data like this
id | value | table1_id | date
-------------------------------------------------
1 | a | 1 | 05-01-2020 1:00:00
2 | b | 2 | 07-01-2020 1:00:00
3 | c | 1 | 06-01-2020 2:00:00
4 | d | NULL | NULL
I tryed like this, but not work true
SELECT tb1.*, tb2.* FROM table1 AS tb1
LEFT JOIN
( SELECT * FROM table2 ORDER BY date DESC ) AS tb2
ON tb1.id = tb2.table1_id
GROUP BY table1_id
Can someone help me ? Thanks all <3
The old school way of doing this in MySQL might be to join to a subquery which finds the maximum date in the second table for each table1_id:
SELECT
t1.id,
t1.value,
t2.table1_id,
t2.date
FROM table1 t1
LEFT JOIN
(
SELECT t2.table1_id, t2.date
FROM table2 t2
INNER JOIN
(
SELECT table1_id, MAX(date) AS max_date
FROM table2
GROUP BY table1_id
) t
ON t.table1_id = t2.table1_id AND
t.max_date = t2.date
) t2
ON t2.table1_id = t1.id;
Demo
You can try this:
SELECT id, value, table1_id, max(date) date
FROM
(SELECT t1.id, t1.value, t2.table1_id, t2.date
FROM table1 t1 LEFT JOIN table2 t2
ON t1.id = t2.table1_id
) qry
GROUP BY id, value, table1_id
You can also use window function as below
SELECT tb1.*, tb2.table1_id, tb2.date
FROM table1 AS tb1
LEFT JOIN
( SELECT table2.*,
row_number() over(partition by table1_id ORDER BY date DESC) as seq_num
FROM table2 ) AS tb2
ON tb1.id = tb2.table1_id
Where tb2.seq_num = 1 ;
Here is a demo - https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=f52a5a930411dcc04900a1a5bacfe6e9. The demo contains both NULL and not NULL versions.
I strongly recommend that you use window functions for this -- assuming you want mulple columns. This looks like:
select t1.*, t2.*
from table1 t1 left join
(select t2.*,
row_number() over (partition by table1_id order by date DESC) as seqnum
from table2 t2
) t2
on t1.id = t2.table1_id and seq_num = 1 ;
However, if you just want one column -- and the table1_id is redundant so I see no need to include it -- then a correlated subquery is often the fastest method:
select t1.*,
(select max(t2.date) from table2 t2 where t1.id = t2.table1_id)
from table1 t1;
In particular, this can take advantage of an index on table2(table1_id, date).

Query on a table to get 2 counts in a single query.

I have two tables in MySQL
table1(Date(full_date), app_id, type(free, paid))
table2(Date_fk, Year, month, day, quater)
Query for Single Count is :
select Year, count(*)
from Table1, Table2
where Table1.Date = Table2.Date and Table1.Type='Free'
GROUP BY YEAR
---------------------
| year | free_count |
---------------------
| 2019 | 10 |
---------------------
I want output as
---------------------------------
| year | free_count | Paid_count |
----------------------------------
| 2019 | 10 | 12 |
----------------------------------
Here's one option using conditional aggregation:
select year,
count(case when t1.type='free' then 1 end) as freecount,
count(case when t1.type='paid' then 1 end) as paidcount
from table1 t1
join table2 t2 on t1.date = t2.date
group by year
Also please take a look at the join syntax. In general, I'd highly recommend not using commas in your from clause.
Try this out:
SELECT
d.year,
SUM(CASE WHEN a.Type = 'Free' THEN 1 ELSE 0 END) AS free_count,
SUM(CASE WHEN a.Type = 'Paid' THEN 1 ELSE 0 END) AS paid_count
FROM Table2 d -- Dates table
LEFT JOIN Table1 a -- Apps table
ON d.Date_fk = a.Date
GROUP BY d.year;
The LEFT JOIN guarantees that you'll still get results for those years without any apps.

Number of increments through period in MySQL

I think this question is gonna be hard to solve.
I have a TABLE in my DDBB as this one:
+----+--------+-------+
| ID | MONTH | VALUE |
+----+--------+-------+
| 1 | 1-2000 | 20.00 |
| 1 | 2-2000 | 21.00 |
| 1 | 3-2000 | 7.00 |
| 1 | 4-2000 | 8.00 |
+----+--------+-------+
With the following definition:
ID INTEGER(7) ZEROFILL NOT NULL
MONTH VARCHAR(7) NOT NULL
VALUE DOUBLE(20,2)
What I'm trying to achieve is the way to retrieve the number of times, through a period, the field {VALUE} has increased from its previous values.
In the example above, if the period is from "1-2000" to "4-2000", {VALUE} has increased 2 times: [20.00->21.00, 7.00->8.00]
At the end, I will like to have the following output:
+----+------------+
| ID | NUM_OF_INC |
+----+------------+
| 1 | 2 |
+----+------------+
What I'm pointing as the main issue, is that {MONTH} is not a DATE type field (of course, it cannot be).
Is there any way to achieve this?
I'm afraid that the solution is to get all the values and then compare one by one from the engine that is executing the queries.
Due to your date format and MySQLs lack of CTEs to convert them a single time, the query gets pretty verbose; this searches the whole range but it's fairly easy to add a range check using the same pattern;
SELECT a.id, COUNT(*) NUM_OF_INC
FROM Table1 a
JOIN Table1 b
ON a.id = b.id
AND a.value < b.value
AND STR_TO_DATE(CONCAT(a.`MONTH`, '-1'), '%c-%Y-%d')
< STR_TO_DATE(CONCAT(b.`MONTH`, '-1'), '%c-%Y-%d')
LEFT JOIN Table1 c
ON a.id = c.id
AND STR_TO_DATE(CONCAT(a.`MONTH`, '-1'), '%c-%Y-%d')
< STR_TO_DATE(CONCAT(c.`MONTH`, '-1'), '%c-%Y-%d')
AND STR_TO_DATE(CONCAT(c.`MONTH`, '-1'), '%c-%Y-%d')
< STR_TO_DATE(CONCAT(b.`MONTH`, '-1'), '%c-%Y-%d')
WHERE c.id IS NULL
GROUP BY a.id;
An SQLfiddle to test with.
Sadly, this query will definitely not use any index you have on MONTH.
If it is an option consider changing the datatype of MONTH into something calculable. Then you can join the last month (Month - 1) and select on a difference > 0:
SELECT
t1.ID, count(*)
FROM
Entity t1
INNER JOIN Entity t2
ON t1.ID = t2.ID
AND t2.MONTH = t1.MONTH - 1
WHERE
t1.VALUE - t2.VALUE > 0
AND t1.MONTH BETWEEN :beginDate AND :endDate
GROUP BY t1.ID
If you can't change the data type. You have to change the t1.MONTH - 1 with some MySQL functions:
DATE_FORMAT(
SUBDATE(
STR_TO_DATE(CONCAT(t1.MONTH, "-1"), "%c-%Y-%d"),
INTERVAL 1 MONTH),
"%c-%Y")
as well as t1.MONTH BETWEEN :beginDate AND :endDate:
STR_TO_DATE(CONCAT(t1.MONTH, "-1"), "%c-%Y-%d")
BETWEEN :beginDate AND :endDate

WHERE condition across multiple rows

I have this table...
--------------------------------------
| user_id | status | status_date |
--------------------------------------
| 1 | Current | 2012-08-01 |
| 1 | Referral | 2012-03-14 |
| 2 | Referral | 2012-04-23 |
| | | |
--------------------------------------
How would I query to find a distinct user_id who has a referral date before 2012-06-30 AND either a current date of after 2012-06-30 or no current status record at all?
Database is MySQL.
You can do this using a LEFT JOIN:
SELECT DISTINCT T.User_ID
FROM T
LEFT JOIN T T2
ON t.User_ID = T2.User_ID
AND t2.Status = 'Current'
WHERE T.Status_Date < '20120630'
AND T.Status = 'Referral'
AND (t2.Status_Date > '20120630' OR t2.Status_date IS NULL)
Or, using GROUP BY with HAVING and COUNT(CASE ...)
SELECT t.User_ID
FROM T
GROUP BY t.user_ID
HAVING COUNT(CASE WHEN t.Status = 'Referral' AND t.Status_Date < '20120630' THEN 1 END) > 0
AND ( COUNT(CASE WHEN t.Status = 'Current' AND t.Status_Date > '20120630' THEN 1 END) > 0
OR COUNT(CASE WHEN t.Status = 'Current' THEN 1 ELSE 0 END) = 0
)
It will depend on your indexes and amount of data as to which performs better, I'd imagine in most cases it will be the former
This should do it:
SELECT DISTINCT user_id
FROM YourTable T
WHERE status = 'Referral'
AND status_date < '2012-06-30'
AND NOT EXISTS (SELECT user_id FROM YourTable
WHERE user_id = T.user_id AND status = 'Current'
AND status_date < '2012-06-30')
Avoid doing innner selects with MySQL. All versions up to and including 5.5 cannot optimize properly with them. Use JOINs:
SELECT distinct t1.user_id
FROM tablename t1
LEFT JOIN tablename t2 on t1.user_id = t2.user_id AND t1.status != t2.status
WHERE t1.status = 'Referral'
AND t1.status_date '2012-06-30'
AND ( (t2.status IS NULL) OR
(t2.status = 'Current' AND t2.status_date > '2012-06-30'));