how to hardcode sertain datetimes from now for five years ahead - mysql

I'm not very advanced in SQL and hope that someone can help me with one query. I'm building a booking platform in React with backend and API in Symfony. So I have starttime, endtime (will be calculated automatically with DATEADD depends on the duration of the chosen service), I will calculate avaliable timeslots to offer the new bookings (with DATEDIFF).
It seems all possible only in case if I'll have an additional table "Agenda_timeslots" with its own starttime,
endtime and
index - [0]free, [1]booked or [2]not available. So each service depends on its fixed duration will take some certain number of timeslots in this agenda.
So my question is - how to write query to generate such table with conditions - timeslots by 20min start from 9am till 5pm only from Monday to Friday for the next five years. If i'll have this timelots, I can loop through it and use SQL formulas. I believe there should be the way than to do it manually
Or I don't need timeslots at all? Only time from 9am to 5pm and the calculate only available hours in between booking time, to see if requested service will fit in the free time gap?
Thank you for any help

timeslots by 20min start from 9am till 5pm only from Monday to Friday for the next five years.
WITH RECURSIVE
cte1 AS ( SELECT #start_date `date`
UNION ALL
SELECT `date` + INTERVAL 1 DAY FROM cte1 WHERE `date` < #end_date ),
cte2 AS ( SELECT CAST('09:00' AS TIME) `time`
UNION ALL
SELECT `time` + INTERVAL 20 minute FROM cte2 WHERE `time` < '17:00' )
SELECT TIMESTAMP(cte1.`date`, cte2.`time`) `datetime`
FROM cte1
CROSS JOIN cte2
WHERE WEEKDAY(cte1.`date`) < 5
For provided conditions SET #end_date := #start_date + INTERVAL 5 YEAR.

Related

Only count working days in a DATEDIFF (MySQL)

So, next problem :'), I have the following query that #MatBailie provided to me here (thanks again!):
SELECT
taskname,
employee,
SUM(
DATEDIFF(
LEAST( enddate, '2023-12-31'),
GREATEST(startdate, '2023-01-01')
)
+1
) AS total_days,
FROM
schedule
WHERE
startDate <= '2023-12-31'
AND
endDate >= '2023-01-01'
GROUP BY
employee,
taskname
This query will tell me how many days a certain employee has spent on a certain task in a given period of time, and it works great!
The next thing I would like to do however, is to substract non-working days from the SUM of DATEDIFFs for some of the tasks (e.g. when the task has "count_non_working_days= 0" in a reference table called 'activities').
For example, my schedule also keeps track of the amount of days off every employee has taken (days off are also scheduled as tasks). But of course, days off that fall in a weekend or on a holiday should not be counted towards the total of days off a person has taken in a year. (Note that I did consider scheduling days off only on weekdays/non-holidays, but this is not a practical option in the scheduling software I use because employees request a leave from date A to date B, and this request is approved or denied as-is (they don't make 3 holiday requests excluding the weekends if they want to go on a vacation for 3 weeks, if you get my drift).
So, if an employee goes on a vacation for 10 days, this is counted as 10 days off, but this holiday may have 1 or 2 weekends in it, so the sum of days of that the employee has taken off should be 6, 7 or 8, and not 10. Furthermore, if it has a holiday such as Easter Monday in it (I have all dates of my holidays in a PHP array), this should also be subtracted.
I have tried the solutions mentioned here, but I couldn't get them to work (a) because those are in SQL server and (b) because they don't allow putting in an array of holidays, (c) nor allow toggling the subtraction on and off depending on the event type.
Here's my attempt of explaining what I'm trying to do in my pseudo-SQL:
SELECT
taskname,
employee,
IF( activities.count_non_working_days=1,
-- Just count the days that fall in the current year:
SUM(
DATEDIFF(
LEAST( enddate, '2023-12-31'),
GREATEST( startdate, '2023-01-01')
)
+ 1
) AS total_days,
-- Subtract the amount of saturdays, sundays and holidays:
SUM(
DATEDIFF(
LEAST( enddate, '2023-12-31'),
GREATEST( startdate, '2023-01-01')
)
- [some way of getting the amount of saturdays, sundays and holidays that fall within this date range]
+ 1
) AS total_days
)
FROM
schedule
LEFT JOIN
activities
ON activity.name = schedule.name
WHERE
startDate <= '2023-12-31'
AND
endDate >= '2023-01-01'
GROUP BY
employee,
taskname
I know the query above is probably faulty on so many levels, but I hope it clarifies what I'm trying to do.
Thanks once more for all the help!
Edit: basically I need something like this, but in MySQL and preferably with a toggle that turns the subtraction on or off depending on the task type.
Edit 2: To clarify: my schedule table holds ALL activities, including holidays. For example, some records may include:
employee
taskname
startDate
endDate
Mr. Anderson
Programming
2023-01-02
2023-01-06
Mr. Anderson
Programming
2023-01-09
2023-01-14
Mr. Anderson
Vacation
2023-01-14
2023-01-31
In another table, Programming is defined as "count_non_working_days=1", because working in the weekends should count, while Vacation is defined as "count_non_working_days=0", because taking a day off on the weekend should not count towards your total amount of days taken off.
The totals for this month should therefore state that:
Mr. Anderson has done Programming for 11 days (of which 1 was on a saturday)
Mr. Anderson has taken 12 days off for (because the 2 weekends in this period don't count as days off).
Create a calendar table, with every date of interest (so, something like 2000-01-01 to 2099-01-01) and include columns such as is_working_day which can be set to TRUE/FLASE or 1/0. Then you can update that column as necessary, and join on that table in your query to get working dates that the employee has booked off.
In short, you count the relevant dates, rather than deducting the irrelevant dates.
SELECT
s.employee,
s.taskname,
COUNT(*) AS total_days,
FROM
(
schedule AS s
INNER JOIN
activities AS a
ON a.taskname = s.taskname
)
INNER JOIN
calendar AS c
ON c.calendar_date >= s.startDate
AND c.calendar_date <= s.endDate
AND c.is_working_day >= 1 - a.count_non_working_days
WHERE
c.calendar_date >= '2023-01-01'
AND c.calendar_date <= '2023-12-31'
GROUP BY
s.employee,
s.taskname
Your calendar table can then also include flags such as is_weekend, is_bank_holiday, is_fubar, is_amazing, etc, and the is_working_day can be a computed column from those inputs.
Note on is_working_day filter...
WHERE
( count_non_working_day = 1 AND is_working_day IN (0, 1) )
OR
( count_non_working_day = 0 AND is_working_day IN ( 1) )
-- change to (1 - count_non_working_day)
WHERE
( (1 - count_non_working_day) = 0 AND is_working_day IN (0, 1) )
OR
( (1 - count_non_working_day) = 1 AND is_working_day IN ( 1) )
-- simplify
WHERE
( (1 - count_non_working_day) <= is_working_day )
OR
( (1 - count_non_working_day) <= is_working_day )
-- simplify
WHERE
( (1 - count_non_working_day) <= is_working_day )
Demo: https://dbfiddle.uk/YAmpLmVE
This is to calculate all the weeekends between two giving dates It may help you :
SELECT (
((WEEK('2022-12-31') - WEEK('2022-01-01')) * 2) -
(case when weekday('2022-12-31') = 6 then 1 else 0 end) -
(case when weekday('2022-01-01') = 5 then 1 else 0 end)
)
You will have to substract also holidays that fall within this date range.

Query with grouping my multiple date ranges

I need to query data with count and sum by multiple date ranges and I am looking for a faster query than what I am doing now.
I have a transaction table with a date and amount. I need to present a table with a count of transactions and total amount by date ranges of today, yesterday, this week, last week, this month, last month. Currently I am doing sub queries, is there a better way?
select
(select count(date) from transactions where date between ({{today}})) as count_today,
(select sum(amount) from transactions where date between ({{today}})) as amount_today,
(select count(date) from transactions where date between ({{yesterday}})) as count_yesterday,
(select sum(amount) from transactions where date between ({{yesterday}})) as amount_yesterday,
(select count(date) from transactions where date between ({{thisweek}})) as count_thisweek,
(select sum(amount) from transactions where date between ({{thisweek}})) as amount_thisweek,
etc...
Is there a better way?
although you have a marked solution, I have another that will probably simplify your query even further using MySQL variables so you don't have to mis-type / calculate dates and such...
Instead of declaring variables up front, you can do them inline as a select statement, then use them as if they were columns in another table. Since it is created as a single row, there is no Cartesian result. First the query, then I'll describe the computations on it.
select
sum( if( t.date >= #today AND t.date < #tomorrow, 1, 0 )) as TodayCnt,
sum( if( t.date >= #today AND t.date < #tomorrow, amount, 0 )) as TodayAmt,
sum( if( t.date >= #yesterday AND t.date < #today, 1, 0 )) as YesterdayCnt,
sum( if( t.date >= #yesterday AND t.date < #today, amount, 0 )) as YesterdayAmt,
sum( if( t.date >= #FirstOfWeek AND t.date < #EndOfWeek, 1, 0 )) as WeekCnt,
sum( if( t.date >= #FirstOfWeek AND t.date < #EndOfWeek, amount, 0 )) as WeekAmt
from
transations t,
( select #today := curdate(),
#yesterday := date_add( #today, interval -1 day ),
#tomorrow := date_add( #today, interval 1 day ),
#FirstOfWeek := date_add( #today, interval +1 - dayofweek( #today) day ),
#EndOfWeek := date_add( #FirstOfWeek, interval 7 day ),
#minDate := least( #yesterday, #FirstOfWeek ) ) sqlvars
where
t.date >= #minDate
AND t.date < #EndOfWeek
Now, the dates. Since the #variables are prepared in sequence, you can think of it as an inline program to set the variables. Since they are a pre-query, they are done first and available for the duration of the rest of the query as previously stated. So to start, I am working with whatever "curdate()" is which gets the date portion only without respect to time. From that, subtract 1 day (add -1) to get the beginning of yesterday. Add 1 day to get Tomorrow. Then, the first of the week is whatever the current date is +1 - the actual day of week (you will see shortly). Add 7 days from the first of the week to get the end of the week. Finally, get whichever date is the LEAST between a yesterday (which COULD exist at the end of the prior week), OR the beginning of the week.
Now look at today for example... Feb 23rd.
Sun Mon Tue Wed Thu Fri Sat Sun
21 22 23 24 25 26 27 28
Today = 23
Yesterday = 22
Tomorrow = 24
First of week = 23 + 1 = 24 - 3rd day of week = 21st
End of Week = 21st + 7 days = 28th.
Why am I doing a cutoff of the dates stripping times? To simplify the SUM() condition for >= AND <. If I stated some date = today, what if your transactions were time-stamped. Then you would have to extract the date portion only to qualify. By this approach, I can say that "Today" count and amount is any date >= Feb 23 at 12am midnight AND < Feb 24th 12 am midnight. This is all time inclusive Feb 23rd up to 11:59:59pm hence LESS than Feb 24th (tomorrow).
Similar consideration for yesterday is all inclusive UP TO but not including whatever "today" is. Similarly for the week range.
Finally the WHERE clause is looking for the earliest date as the range so it does not have to run through the entire database of transactions to the end.
Lastly, if you ever wanted the counts and totals for a prior week / period, whatever, you could just extrapolate and change
#today := '2015-01-24'
and the computations will be AS IF the query was run ON THAT DATE.
Similar if you cared to alter such as for a month, you could compute the first of the month to the first of a following month for MONTHLY totals.
Hope you enjoy this flexible solution to you.
Yes, you can use aggregate functions on conditional expressions, like so:
SELECT SUM(IF(date between ({{today}})), 1, 0) AS count_today
, SUM(IF(date between ({{today}})), amount, 0) AS amount_today
, ...

Trying to solve date conditional in MySQL select query

I am trying to list upcoming deliveries that can still be chosen for shipping. Each of these deliveries requires a certain amount of lead-time to ship - so it's not realistic to expect your delivery to come tomorrow if you order at 5pm the day before.
The deliveries table defines a "leadTime" which is an integer of days and also a "nextDate" which contains a date field in "YYYY-MM-DD" format. I need to filter out the rows where the "nextDate" is at least one day away - or rather, one day away if before 10am and two days away if after 10am.
SELECT d.* FROM delivery d
WHERE d.nextDate >= DATE_ADD( CURRENT_DATE, INTERVAL d.leadTime DAY )
ORDER BY d.nextDate ASC, d.id ASC
For example, (assuming a leadTime of one day). If it was 9am Wednesday - I can see all records from Thursday on. However, if it's 11am Wednesday I can only see from Friday on (since it's after 10am there isn't enough time to prepare).
Use DATE(NOW() + INTERVAL 14 HOUR) in place of CURRENT_DATE or, combining the whole thing together:
SELECT *
FROM delivery
WHERE nextDate >= DATE(NOW() + INTERVAL 14 HOUR + INTERVAL leadTime DAY)
ORDER BY nextDate ASC, id ASC

MySQL group by week

I have a large number of records with a transaction datetime field going back several years. I would like to do a comparative analysis between the same timespan this year and last. How can I group by week over a 3 month range?
I'm running into problems using the YEARWEEK and WEEK functions because of the day the year 2012 starts of versus the day 2011 starts on.
Given that I have records with datetimes everyday from Jan 1st to the current day, and records with the same datetimes from the prior year, how can I group by week so the output is sums with dates like: 01/01/2011, 01/08/2011, 01/15/2011, etc., and 01/01/2012, 01/08/2012, 01/15/2012, etc.?
My query so far is as follows:
SELECT
DATE_FORMAT(A.transaction_date, '%Y-%m-%d') as date,
ROUND(sum(A.quantity), 3) AS quantity,
ROUND(sum(A.total_amount), 3) AS amount,
A.product_code,
D.fuel_type_code,
D.fuel_type_name,
C.customer_code,
C.customer_name
FROM
cl_transactions AS A
INNER JOIN
card AS B ON A.card_number=B.card_number
INNER JOIN
customer AS C ON B.customer_code=C.customer_code
INNER JOIN
fuel_type AS D ON A.fuel_type=D.fuel_type_code
WHERE
((A.transaction_date >= DATE_FORMAT(NOW() - INTERVAL 3 MONTH, '%Y-%m-01')) OR (A.transaction_date - INTERVAL 1 YEAR >= DATE_FORMAT(NOW() - INTERVAL 15 MONTH, '%Y-%m-01') AND A.transaction_date <= NOW() - INTERVAL 1 YEAR))
GROUP BY
A.transaction_date, fuel_type_code;
I would essentially like something that achieves the following pseudo-query:
GROUP BY
STARTING FROM THE OLDEST DATE (A.transaction_date + INTERVAL 6 DAY)
I started with an inner query using sqlvariables to build out from/to ranges for this year and last year of each respective start of year/month/day (ex: 2012-01-01 and 2011-01-01 respectively). From that, I'm also pre-formatting the date for final output so you have ONE master date basis for display reflecting that of whatever the "this year" week would be.
From that, I do a join to the transaction table where the transaction date is BETWEEN the respective start of current week and start of next week. Since date/time stamps include hour minute, 2012-01-01 by itself is implied as 12:00:00am (midnight) of the day. and between will go UP TO 7 days later 12:00:00 am. And that date will become the start date of the following week.
So, by joining on the date being between EITHER last yr or this yr time period, its the same group qualification. So the field selection does a ROUND( SUM( IF() )) per respective last year or this year. if the incoming transaction date is LESS than the current year's week start, then it must be a record from prior year, otherwise its for the current year. So, respectively, add the value itself, or zero as it applies.
So now, you have the group by. The week that it qualified for was already prepared from the inner query via "ThisYearWeekOf" formatted column, regardless of the otherwise computed "YEARWEEK()" or "WEEK()". The date ranges took care of that qualification for us.
Finally, I added the fuel-type as a join and included that as the group by. You have to group by all non-aggregate columns for proper SQL, although MySQL lets you get by by just grabbing the first entry for the given group if it is NOT so specified in group by.
To close, I DID include the information for the customer as you didn't have it in the group by and did not appear to be applicable... it would just arbitrarily grab one. However, I've added it to the group by, so now your records will show at the per customer level, per product and fuel type, how much sales and quantity between this year and last.
SELECT
JustWeekRange.ThisYearWeekOf,
CTrans.product_code,
FT.fuel_type_code,
FT.fuel_type_name,
C.customer_code,
C.customer_name,
ROUND( SUM( IF( CTrans.transaction_date < JustWeekRange.ThisYrWeekStart, CTrans.Quantity, 0 )), 3) as LastYrQty,
ROUND( SUM( IF( CTrans.transaction_date < JustWeekRange.ThisYrWeekStart, CTrans.total_amount, 0 )), 3) as LastYrAmt,
ROUND( SUM( IF( CTrans.transaction_date < JustWeekRange.ThisYrWeekStart, 0, CTrans.Quantity )), 3) as ThisYrQty,
ROUND( SUM( IF( CTrans.transaction_date < JustWeekRange.ThisYrWeekStart, 0, CTrans.total_amount )), 3) as ThisYrAmt,
FROM
( SELECT
DATE_FORMAT(#ThisYearDate, '%Y-%m-%d') as ThisYearWeekOf,
#LastYearDate as LastYrWeekStart,
#ThisYearDate as ThisYrWeekStart,
#LastYearDate := date_add( #LastYearDate, interval 7 day ) LastYrStartOfNextWeek,
#ThisYearDate := date_add( #ThisYearDate, interval 7 day ) ThisYrStartOfNextWeek
FROM
(select #ThisYearDate := '2012-01-01',
#LastYearDate := '2011-01-01' ) sqlvars,
cl_transactions justForLimit
HAVING
ThisYrWeekStart < '2012-04-01'
LIMIT 15 ) JustWeekRange
JOIN cl_transactions AS CTrans
ON CTrans.transaction_date BETWEEN
JustWeekRange.LastYrWeekStart AND JustWeekRange.LastYrStartOfNextWeek
OR CTrans.transaction_date BETWEEN
JustWeekRange.ThisYrWeekStart AND JustWeekRange.ThisYrStartOfNextWeek
JOIN fuel_type FT
ON CTrans.fuel_type = FT.fuel_type_code
JOIN card
ON CTrans.card_number = card.card_number
JOIN customer AS C
ON card.customer_code = C.customer_code
GROUP BY
JustWeekRange.ThisYearWeekOf,
CTrans.product_code,
FT.fuel_type_code,
FT.fuel_type_name,
C.customer_code,
C.customer_name

Rolling 30 day uniques in sql

Suppose you have a table of the form:
create table user_activity (
user_id int not null,
activity_date timestamp not null,
...);
It's easy enough to select the number of unique user_id's in the past 30 days.
select count(distinct user_id) from user_activity where activity_date > now() - interval 30 day;
But how can you select the number of unique user_ids in the prior 30 days for each of the past 30 days? E.g. uniques for 0-30 days ago, 1-31 days ago, 2-32 days ago and so on to 30-60 days ago.
The database engine is mysql if it matters
You could try using a sub query:
SELECT DISTINCT `activity_date` as `day`, (
SELECT count(DISTINCT `user_id`) FROM `user_activity` WHERE `activity_date` = `day`
) as `num_uniques`
FROM `user_activity`
WHERE `activity_date` > NOW() - INTERVAL 30 day;
This should give you the number of unique users for each day. However, I haven't tested this since I don't have the DB to work with.
I haven't tried this in MySQL, but hopefully the syntax is right. If not, maybe it will point you in the right direction. First, I often employ a Numbers table. It can be a physical table simply made up of numbers or it can be a generated/virtual/temporary table.
SELECT
N.number,
COUNT(DISTINCT UA.user_id)
FROM
Numbers N
INNER JOIN User_Activity UA ON
UA.activity_date > NOW() - INTERVAL 30 + N.number DAY AND
UA.activity_date <= NOW() - INTERVAL N.number DAY
WHERE
N.number BETWEEN 0 AND 30
GROUP BY
N.number
I'm not familiar with the whole INTERVAL syntax, so if I got that wrong, please let me know and I'll try to correct it.
If you get the days number for todays date and mod it by 30 you get the offset of the current day. Then you add that to each number for a date and divide the result by 30, this gives you the group of days. Then group your results by this number. So in code something like this:
select count(distinct user_id), (to_days(activity_date)+(to_days(now()) % 30)) / 30 as period
from user_activity
group by (to_days(activity_date)+(to_days(now()) % 30)) / 30
I will leave calculating the reverse numbering of period up to you (hint: take the period number for the current date as "max" and subtract period above and add 1.)