Reputation: 912
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.
Upvotes: 0
Views: 166
Reputation: 108370
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
Upvotes: 1
Reputation: 1269463
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.
Upvotes: 1