Getting next available date from database - php

I have a system that allows users to assign a specific file to a past or present date. The limitations are that they may only upload one file per day per user. When the user goes to upload a file the date field must default to the current date and when that date is not available it will show the first available date in the past in DESC order. Below is the relevent field names.
file_id (INT - INDEX - AUTO INCREMENT)
user_id (INT - may index this)
upload_date (INT - stores date as a unix timestamp)
The only solution I have really found would be to build them all into an array in DESC order by date and loop through until i found an empty slot. However, I feel this could really cause speed issues if the user had the past thousand days filled. I feel like I am overlooking a simple solution.
PLEASE NOTE: For one reason or another they Date is being stored as a Unix timestamp which I understand the downsides on and I am not concerned about correcting at this time.

To get the most recent date that has not been used:
select user_id, max(date) - 1
from (select ud.*,
(select max(date) from upload_date ud2 where ud2.user_id = ud.user_id and ud2.date < ud.date
) as prevdate
from upload_date ud
) ud
where date(from_unixtime(ud.prevdate)) <> date(from_unixtime(ud.date)) - 1 or
ud.prevdate is null
group by user_id
This query first gets the previous date for any given day using a correlated subquery. It then converts the time values to dates and selects any row where the previous date has a gap. The largest of the date minus one is the date you are looking for.
This SQL is untested, so it may have syntax errors.

One way to approach this is with a classic "return missing rows" query. Basically, to get a "missing" row returned from the database, you need a way to generate the "missing" rows.
To build such a query, we can start with:
SELECT MAX(t.upload_date)
FROM mytable t
WHERE t.upload_date <= NOW()
AND t.user = 'someuser'
That gets the initial date, that we are going to work backwards from.
For the "one per day" requirement, you probably want to truncate that upload_date to midnight, at least for this query. For now, we'll assume that the expression in the SELECT list is already truncated, to illustrate the approach, without bogging down in the details of dealing with a unix timestamp.
To generate a descending list of dates, starting with that initial date retrieved by the previous query...
SELECT s.upload_date - INTERVAL n.d DAY AS available_date
FROM ( SELECT MAX(t.upload_date) AS upload_date
FROM mytable t
WHERE t.upload_date <= NOW()
AND t.user = 'someuser'
) s
CROSS
JOIN ( SELECT 0 AS d UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3
UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6
UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9
) n
ORDER BY n.d DESC
With that result, we can use an anti-join pattern to find which dates are not already used. This is a LEFT JOIN and a predicate that throws out matching rows:
SELECT s.upload_date - INTERVAL n.d DAY AS available_date
FROM ( SELECT MAX(t.upload_date) AS upload_date
FROM mytable t
WHERE t.upload_date <= NOW()
AND t.user = 'someuser'
) s
CROSS
JOIN ( SELECT 0 AS d UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3
UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6
UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9
) n
LEFT
JOIN mytable u
ON u.upload_date = s.upload_date - INTERVAL n.d DAY
AND u.user = 'someuser'
WHERE u.upload_date IS NULL
ORDER BY n.d DESC
LIMIT 1
That only looks back 9 days, to get it to look back more days, just extend the inline view aliased as n to return more consecutive integers. (There's some tricks we can play with cross joins to get a whole boatload of integers.)
All that remains is working on the "matching" criteria (which works with the MySQL DATE datatype):
ON u.upload_date = s.upload_date - INTERVAL n.d DAY
into something like this:
ON u.upload_date >= UNIX_TIMESTAMP(FROM_UNIXTIME(s.upload_date)-INTERVAL n.d+1 DAY)
AND u.upload_date < UNIX_TIMESTAMP(FROM_UNIXTIME(s.upload_date)-INTERVAL n.d DAY)
And futzing with the integer timestamp value to get a MySQL DATE out of it...
SELECT DATE(FROM_UNIXTIME(s.upload_date)) - INTERVAL n.d DAY AS available_date

Related

Selecting newest row by date with interval and groupping by the client ID (MySQL)

I have been trying to select a (the) NEWEST entry for a customer (which will be run by the WP plugin, sorta cron), which I thought would ba 5 mins job. Four hours later I am here, turning to you for help.
To visualize the output and the problem:
SELECT * FROM `foc_program_partnerski_historia`
WHERE `data` >= SUBDATE('2019-06-30 00:01:00', INTERVAL 10 DAY)
AND `zmiana` NOT LIKE '-%'
ORDER by main_id, `data` DESC
I need to find the newest entry, the highest 'PO' number *, for each client and check if this falls into the interval (the inverval will be later be changed to "equals to", to exactly get the ouput from 10 days ago, 30 days ago or 365 days ago (that will be up to the client).
This is what I would like to see:
When I add 'group by' clause, it does group it, yes, just not the way I would like it:
and it takes forver, too...
What should be the proper sql, can you help me out?
.
.
.
.
`
*
Przed /zmiana /po
means:
before /change /after
From what I see you need to GROUP BY main_id.
Start with something like
SELECT main_id, max(data) AS data, max(przed) AS przed, min(po) AS po
FROM `foc_program_partnerski_historia`
WHERE `data` >= SUBDATE('2019-06-30 00:01:00', INTERVAL 10 DAY)
AND `zmiana` NOT LIKE '-%'
GROUP BY main_id
ORDER BY main_id
then you can join to that like this:
SELECT *
FROM `foc_program_partnerski_historia` A
LEFT JOIN (
SELECT main_id, max(data) AS data, max(przed) AS przed, min(po) AS po
FROM `foc_program_partnerski_historia`
WHERE `data` >= SUBDATE('2019-06-30 00:01:00', INTERVAL 10 DAY)
AND `zmiana` NOT LIKE '-%'
GROUP BY main_id
) B
ON A.main_id = B.main_id AND A.przed=B.po -- OR A.data<>B.data
ORDER BY A.main_id
and I'm using max(data) rather than przed based on what you highlighted, although it looks like either will have the results in this particular case.

Is it possible to select 10 rows for each group by date(timestamp) in mysql table ? How to proceed?

I have a table with attributes as timestamp, SensorName, Temperature, Humidity. What I doing is getting results by ordering then according to the timestamp in DESC order and I have condition to get it for either 1 day 7 days or for a previous month. But the issue is I am getting a Lot more values when I access old records. What I need to do is, since my data is ordered by timestamp, I need at most 15 to 20 rows/records for each date.Suppose I am getting data from June 10 to June 17, and each date have 100 records. I need only 10/20 records for the particular day (top 20 for each day).
My MySQL version is 5.7.26 and i tried using ROW_NUMBER but it is not helping
SELECT datestamp AS TIME,Temperature AS TEMPERATURE FROM table_name WHERE NodeAdd = 'SensorName' and datestamp >= DATE_ADD(CURRENT_DATE,INTERVAL -'$somevar' DAY) ORDER BY datestamp DESC
The above code shows how I am getting data for different days, The $somevar variable is hardcoded, If user select day it is -1 if he selects previous week, it is -7 and -30 for the previous month.. I need at most 20 rows/records for each day.
NOTE :: I don't have an ID column or a Unique/Primary Key in my Table, so I was trying to get particular rows using partitioning via dates only.
You need to create Row Number per group which is Date and then only keep records with row Row Number < 10 as you wants 10 records per group. The following script is just a guideline and you can fit it with your table structure to get your desired output-
SELECT * FROM
(
SELECT *,
(
CASE DateColumn
WHEN #DateColumn THEN #curRow := #curRow + 1
ELSE #curRow := 1 AND #DateColumn := DateColumn
END
) AS rank
FROM
(
SELECT *,CAST(<your_date_column> AS DATE) AS `DateColumn`
FROM Your_table
)A
,(SELECT #curRow := 0, #DateColumn := '') r
ORDER BY <your_date_column>
)A WHERE Rank < 11

Show all dates even those with zero data in it in mySQL?

My following query shows the date and the count of the emails found on each day (last 2 days)
My problem is that if no emails are found today, the today date will not be displayed on the output. (if yesterday has emails, it will show only 1 row with yesterday date and email).
How can I edit my query to always show 2 rows, today and yesterday, date and number of emails even zero?
SELECT maildate,
COUNT(*) AS totalEmails
FROM emails
WHERE maildate >= Date_add(Curdate(), interval - 2 DAY)
AND company_id = 1
GROUP BY DATE(maildate)
ORDER BY maildate desc
There are many tricks to creating a list of dates (or numeric sequences similarly). The one I like to use with MySQL is using #sqlvariables. I will typically start with a baseline value such as your date -2 days. I will do a cross-join to any other table in the database that has at least as many records as you expect in your output... Say 30 days, or a whole year 366 days, or longer. The inner sql variable prep will keep increasing itself by whatever increment (you could even do date ranges such as begin/end of a week, month, etc). Now you have your table of all possible dates you are looking to fill.
Now, I do a secondary query by the value -- in this case your email date and apply the group by. Using the where clause in this query will make IT faster since it can utilize the date on its query result set before returning for the LEFT-JOIN to the date range result set.
Now, your simple left-join gets both parts of all dates to be included and those corresponding counts that do exist.
Note the table alias "AnyTableWithAtLeast3RecordInIt" in the "JustDates" query could in-fact be your "emails" table. Since we don't care about any criteria except a record exists, and we are applying a limit of 30 days in my example, it will be instantaneous.
select
JustDates.DateToInclude,
coalesce( SumCnts.TotalEmails, 0 ) TotalEmails
from
( select
#myDate := DATE_ADD( #myDate, INTERVAL 1 DAY ) as DateToInclude
from
( select #myDate := Date_add(Curdate(), interval - 2 DAY) ) as SQLVars,
AnyTableWithAtLeast3RecordInIt
limit 30 ) JustDates
left join
( select
maildate,
COUNT(*) AS totalEmails
FROM
emails
WHERE
maildate >= Date_add(Curdate(), interval - 2 DAY)
AND company_id = 1
GROUP BY
DATE(maildate) ) SumCnts
ON JustDates.DateToInclude = SumCnts.MailDate
Now, judging by your query, but unclarified request... Your emails table CAN HAVE FUTURE DATES? Is that correct? Such as a Dr. Office and appointments are for the future and you want to get emails out for a given range. This is what I was inferring and hence had my limit to only go out 30 days... If you need longer, just extend the LIMIT clause.
You need a table that contains all dates in the needed range. If its only about today and yesterday, you can easily create it as a subquery (derived table).
SELECT Curdate() as maildate
UNION ALL
SELECT Curdate() - INTERVAL 1 DAY
http://rextester.com/ALH50651
Now you can LEFT JOIN your table and count the rows:
SELECT sub.maildate,
COUNT(m.maildate) AS totalEmails
FROM (
SELECT Curdate() as maildate
UNION ALL
SELECT Curdate() - INTERVAL 1 DAY
) sub
LEFT JOIN emails m
ON DATE(m.maildate) = sub.maildate
AND m.company_id = 1
GROUP BY sub.maildate
ORDER BY sub.maildate desc

Mysql find dates using where in clause, or dates nearest to those in array

I have the following code and query:
//$month is an array of datetime objects
foreach($month as $key => $indMonth){
$formattedMonth[] = $month[$key]->format('Y-m-d');
}
$formattedMonths = implode("','",$formattedMonth);
$query = "SELECT id,date FROM table WHERE date in ('$formattedMonths') ORDER by date DESC";
The database holds dates for the past 450 days, but it is imperfect and there are some missing days. The point of the script is to retrieve data from the current day of the month and then the corresponding day on the five previous months, but I need a failsafe for when a date happens to be missing.
How can I modify this query so it picks either the date in the "where in" portion of the query or it finds the date nearest to that particular date in the array?
Is this best to do in the query, or am I better off returning a more complete data set, then using PHP to find out if the date I want is available?
MySQL offers some decent date arithmetic. For example, if you have the date '2015-11-10' (10-Nov-2015) you can get the same day three months prior with this expression:
'2015-11-10` - INTERVAL 3 MONTH
That will kick back '2015-08-10', which is what you want.
This date arithmetic works predictably even with longer and shorter months, and with leap years. For example,
'2015-03-31' - INTERVAL 1 MONTH, '2016-03-31' - INTERVAL 1 MONTH
gives back '2015-02-28', '2016-02-29' as you might expect. And
'2015-03-31' - INTERVAL 2 MONTH, '2016-03-31' - INTERVAL 2 MONTH
gives back '2015-03-31', '2016-03-31'. Perfect.
Now, only you can decide whether this predictable behavior is correct for your application: only you know what you want to do with the previous five months of data, when the day in question is near the end of the month.
Let's assume it's correct and move on. Here is a subquery that can be used to generate a sequence of six dates, one day per month ending today.
SELECT CURDATE() - INTERVAL seq.seq MONTH day_of_month
FROM ( SELECT 0 AS seq UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL
SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 6) seq
We can use this little query as a subquery, and LEFT JOIN it to your data. That would work like this:
SELECT id, day_of_month
FROM (
SELECT CURDATE() - INTERVAL seq.seq MONTH day_of_month
FROM ( SELECT 0 AS seq UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL
SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 6) seq
) days
LEFT JOIN table ON table.date = days.day_of_month
This is a cool way to do it because you'll always get at least one row in the resultset for each date in the list, even if there's nothing matching in table.
The closest date gets a little hairier. It's possible to write a query like that. But MySQL lacks a WITH clause so the query is ridiculously repetitive.

totals by month even for if there is a missing month

just a quick one, hopefully....
i am after getting some totals (sales value) by month from only a single table.
The problem i have is:
If there are no sales for a month, the month is of course not being returned in the results. Is there a way i can do this in a single query so if there were no sales in i.e "January 2015" the result would return "0.00 - January - 2015"
The basic SQL i currently have is:
SELECT SUM(p.PaymentAmount) AS Total, MONTHNAME(p.PaymentDate) AS Month, YEAR(p.PaymentDate) AS Year
FROM tPayment p
WHERE p.PaymentType = 2
GROUP BY YEAR(p.PaymentDate), MONTH(p.PaymentDate)
i cant think of how to do this without selecting the date range in php and then querying each month and year... this just seems messy... so i would like to know if i can do this in a single query.
Any help is much appreciated!
you should create yourself a separate table containing at dates such as
CREATE TABLE `dates` (
`uid` INT NOT NULL AUTO_INCREMENT,
`datestamp` DATE NOT NULL,
PRIMARY KEY (`uid`))
ENGINE = InnoDB;
and fill it
INSERT INTO dates (datestamp)
SELECT ADDDATE('2015-01-01', INTERVAL SomeNumber DAY)#set start date
FROM (SELECT a.i+b.i*10+c.i*100+d.i*1000 AS SomeNumber
FROM integers a, integers b, integers c, integers d) Sub1
WHERE SomeNumber BETWEEN 0 AND (365 * 3)#3 years
then you can join against it
SELECT SUM(p.PaymentAmount) AS Total, MONTHNAME(p.PaymentDate) AS Month, YEAR(p.PaymentDate) AS Year
FROM tPayment p
LEFT OUTER JOIN dates d
ON d.datestamp = CAST(p.PaymentDate AS DATE)
WHERE p.PaymentType = 2
GROUP BY YEAR(p.PaymentDate), MONTH(p.PaymentDate)
ORDER BY d.datestamp DESC;
regardless of if I fatfingered the queries here, the concept should hold up for you
This wouldn't be my first choice method for accomplishing this task, but for the sake of providing multiple alternatives I offer this if you're trying to keep it all in MySQL and avoid creating an additional table.
SELECT
SUM(p.PaymentAmount) AS Total,
MONTHNAME(p.PaymentDate) AS Month,
YEAR(p.PaymentDate) AS Year
FROM ( SELECT 1 AS m UNION ALL
SELECT 2 UNION ALL
SELECT 3 UNION ALL
SELECT 4 UNION ALL
SELECT 5 UNION ALL
SELECT 6 UNION ALL
SELECT 7 UNION ALL
SELECT 8 UNION ALL
SELECT 9 UNION ALL
SELECT 10 UNION ALL
SELECT 11 UNION ALL
SELECT 12
) AS months
OUTER JOIN tPayment p
ON MONTH(p.PaymentDate) = months.m
WHERE p.PaymentType = 2
GROUP BY YEAR(p.PaymentDate), MONTH(p.PaymentDate)
I think it'd be easier to check against it in some quick PHP code after you run the query.
As suggested by Jon B Creating a months table and joining against that would shorten and clean the query up quite a bit. If you're trying to keep it all in your MySQL query I personally would choose his method.
If you have data in your table for all months -- but the where clause is filtering out all the rows from one or more months -- you can try conditional aggregation:
SELECT SUM(CASE WHEN p.PaymentType = 2 THEN p.PaymentAmount ELSE 0 END) AS Total,
MONTHNAME(p.PaymentDate) AS Month, YEAR(p.PaymentDate) AS Year
FROM tPayment p
GROUP BY YEAR(p.PaymentDate), MONTH(p.PaymentDate)
This isn't guaranteed to work (it depends on the data). But if it does, it is the simplest way to solve this problem.

Categories