Random Scheduling - sql

I have the following table in my database:
tbl1
PK
ClientID
ScheduleDay
Time1Start
Time1Stop
Time2Start
Time2Stop
Time3Start
Time3Stop
Status
Here is some sample data
ID ClientID ScheduleDay Time1Start Time1Stop Time2Start Time2Stop Time3Start Time3Stop
-- -------- ----------- ---------- --------- ---------- --------- ---------- ---------
1 3 Sunday 0000 0800 1000 1300 NULL NULL
2 3 Monday 0000 2359 NULL NULL NULL NULL
3 3 Tuesday 1000 1200 1330 1700 1900 2200
4 3 Wednesday 0000 0800 NULL NULL NULL NULL
5 3 Thursday 0800 1200 NULL NULL NULL NULL
6 3 Friday 0400 0800 0900 1600 NULL NULL
The Time fields are CHAR(4) since I am storing the time in a military format.
What I need to accomplish is this; for any given ClientID, insert one or more records into a schedule table with the time value of the record being within the time frames in tbl1. For example, scheduling ClientID 3 on Tuesday, the time scheduled could be 1120.
In the event that multiple records need to be inserted, the times scheduled should not be any closer than one hour.
Any and all help is appreciated!

Here's my best guess as to what you're trying to do. The first two parts of the CTE are really just to get things into a form similar to what FlyingStreudel suggests. Ideally, you should change the database to match that format instead of doing this through CTEs. That will make this significantly simpler and is better for data integrity as well.
Next, I just get the distinct start times in hour increments. You could do that by joining to a Numbers table as well if you can't use CTEs (you didn't mention the version of SQL Server that you're using).
Finally, I grab one of those start times at random, using the RAND function and ROW_NUMBER. You'll want to set a good seed value for RAND().
;WITH TimesAsTimes AS
(
SELECT
ScheduleDay,
CAST(SUBSTRING(T1.Time1Start, 1, 2) + ':' + SUBSTRING(T1.Time1Start, 3, 2) AS TIME) AS time_start,
CAST(SUBSTRING(T1.Time1Stop, 1, 2) + ':' + SUBSTRING(T1.Time1Stop, 3, 2) AS TIME) AS time_stop
FROM
tbl1 T1
WHERE
T1.Time1Start IS NOT NULL
UNION ALL
SELECT
ScheduleDay,
CAST(SUBSTRING(T2.Time2Start, 1, 2) + ':' + SUBSTRING(T2.Time2Start, 3, 2) AS TIME) AS time_start,
CAST(SUBSTRING(T2.Time2Stop, 1, 2) + ':' + SUBSTRING(T2.Time2Stop, 3, 2) AS TIME) AS time_stop
FROM
tbl1 T2
WHERE
T2.Time2Start IS NOT NULL
UNION ALL
SELECT
ScheduleDay,
CAST(SUBSTRING(T3.Time3Start, 1, 2) + ':' + SUBSTRING(T3.Time3Start, 3, 2) AS TIME) AS time_start,
CAST(SUBSTRING(T3.Time3Stop, 1, 2) + ':' + SUBSTRING(T3.Time3Stop, 3, 2) AS TIME) AS time_stop
FROM
tbl1 T3
WHERE
T3.Time3Start IS NOT NULL
),
PossibleTimeStarts AS
(
SELECT
ScheduleDay,
time_start,
time_stop
FROM
TimesAsTimes TAT
UNION ALL
SELECT
ScheduleDay,
DATEADD(hh, 1, time_start) AS time_start,
time_stop
FROM
PossibleTimeStarts PTS
WHERE
DATEADD(hh, 1, time_start) <= DATEADD(hh, -1, PTS.time_stop)
),
PossibleTimesWithRowNums AS
(
SELECT
ScheduleDay,
time_start,
ROW_NUMBER() OVER(PARTITION BY ScheduleDay ORDER BY ScheduleDay, time_start) AS row_num,
COUNT(*) OVER(PARTITION BY ScheduleDay) AS num_rows
FROM
PossibleTimeStarts
)
SELECT
*
FROM
PossibleTimesWithRowNums
WHERE
row_num = FLOOR(RAND() * num_rows) + 1

First of all you may want to try a schema like
tbl_sched_avail
PK id INT
FK client_id INT
day INT (1-7)
avail_start varchar(4)
avail_end varchar(4)
This way you are not limited to a finite number of time fences.
As far as checking the schedules availability -
CREATE PROCEDURE sp_ins_sched
#start_time varchar(4),
#end_time varchar(4),
#client_id INT,
#day INT
AS
BEGIN
DECLARE #can_create BIT
SET #can_create = 0
DECLARE #fence_start INT
DECLARE #fence_end INT
--IS DESIRED TIME WITHIN FENCE FOR CLIENT
DECLARE c CURSOR FOR
SELECT avail_start, avail_end FROM tbl_sched_avail
WHERE client_id = #client_id
AND day = #day
OPEN c
FETCH NEXT FROM c
INTO #fence_start, #fence_end
WHILE ##FETCH_STATUS = 0 AND #can_create = 0
BEGIN
IF #start_time >= #fence_start AND #start_time < #fence_end
AND #end_time > #fence_start AND <= #fence_end
SET #can_create = 1
FETCH NEXT FROM c
INTO #fence_start, #fence_end
END
CLOSE c
DEALLOCATE c
IF #can_create = 1
BEGIN
--insert your schedule here
END
END
As far as the code for actually inserting the record I would need to know more about the tables in the database.

Related

Get current item in loop in SQL

If I write loop in SQL like:
DECLARE #Counter INT = 0
SET #length = (SELECT COUNT(*) FROM table)
WHILE #Counter <= #length
BEGIN
DECLARE #id INT = (SELECT TOP 1 id FROM table WHERE id = #Counter)
DECLARE #someValue int = (SELECT TOP 1 someValue FROM table WHERE id = #id)
END
I never wrote loop in SQL before and what I need is to take values from current item in loop and use it.
In code above I use SELECT to get someValue.
Do I need to make select like this for all fields I need or there is some easier way something like foreach loop and get current item.someValue?
UPDATE
Based on comments maybe I don't need loop.
This is example table
create table products
(
tourId int NOT NULL IDENTITY PRIMARY KEY,
title nvarchar(50),
orderTime smalldatetime,
orderSecondTime smalldatetime,
orderThirdTime smalldatetime
);
orderTime has value, orderSecondTime and orderThirdTime are NULL.
I need to make some CASE..WHEN on orderTime and update orderSecondTime and orderThirdTime.
When I say CASE...WHEN I need based on date in orderTime update other columns like:
orderThirdTime can depend on orderSecondTime this is where I thought loops are natural solution.
Those other columns are basically add orderSecondTime = 24h + orderTime orderThirdTime = orderTime to orderSecondTime and similar.
UPDATE #2:
Example rules:
In case that orderTime is between 08:00 and 10:00 :
orderSecondTime = orderTime + 60 min
orderThirdTime = orderSecondTime(calculated above) + 60 min
ELSE use different calculation like add 30 min
What I have
tourId title orderTime orderSecondTime orderThirdTime
-------------------------------------------------------------------
1 prod1 2020-08-02 08:00 null null
2 prod12 2020-08-02 09:00 null null
3 prod13 2020-08-02 10:00 null null
4 prod14 2020-08-02 11:00 null null
What I am trying to calculate
Aren't you just after a couple of CASE expressions?
SELECT id,
title,
orderTime,
CASE WHEN CONVERT(time,orderTime) >= '08:00' AND CONVERT(time,orderTime) <= '10:00' THEN DATEADD(HOUR, 1, orderTime)
ELSE DATEADD(MINUTE, 30, orderTime)
END AS orderSecondTime,
CASE WHEN CONVERT(time,orderTime) >= '08:00' AND CONVERT(time,orderTime) <= '10:00' THEN DATEADD(HOUR, 2, orderTime)
ELSE DATEADD(HOUR, 1, orderTime)
END AS orderSecondTime
FROM dbo.YourTable;
Side Note: SQL Server 2008 has been completely unsupported for over a year now. You should be looking at upgrade paths as soon as possible if not already.

How to get date difference in SQL Server and return value

How to get day difference from when the user registered to current date? I have this scenario:
I have some fixed value in master table like [0, 6, 12, 18, 24, 30, 36, 42 .....]
and suppose
day difference is greater or equal than 1 and less than 6 then It should be return 1.
day difference is greater than 6 and less than 12 then it should return 2 and so on.
day difference is greater than 12 and less than 18 then return 3.
day difference is greater than 18 and less than 24 then return 4.
.
.
.
And so on.
I don't want to use case statements because values in master table can not be fix but value pattern will be fix. table value pattern is like that:
common difference between two consecutive values is 6 i.e.
if n=0 then
n+1 = (0 + 6) => 6
Thanks
declare #day int;
declare #regdate datetime = '2019-12-09 19:24:19.623';
declare #currentDate datetime = GETDATE();
SET #day = (SELECT DATEDIFF(day, #regdate, #currentDate) % 6 FROM tblMembers WHERE Id = 1)
PRINT #day
I think that you are looking for integer division, not modulo. This is the default behavior in SQL Server when both arguments are integers, so, since DATEDIFF returns an integer, this should do it:
1 + DATEDIFF(day, #regdate, #currentDate) / 6
Here's approach you can build your solution on:
declare #masterTable table (id int, col int);
insert into #masterTable values
(1,0) ,
(2,6) ,
(3,12),
(4,18),
(5,24),
(6,30),
(7,36),
(8,42),
(9,48);
-- test data
declare #day int;
declare #regdate datetime = '2019-12-09 19:24:19.623';
declare #currentDate datetime = GETDATE();
select #day = datediff(day, #regdate, #currentDate)
;with cte as (
select id,
col lowerBound,
-- here we need to provide some fallback value for last record
coalesce(lead(col) over (order by id), 1000) upperBound
from #masterTable
)
select id from (values (#day)) [day]([cnt])
join cte on [day].[cnt] between cte.lowerBound and cte.upperBound

Count each days of week between two dates without loop

I can do it with loop, but if many day is slow. So I need do without loop.
Here is my code:
DECLARE
#FRDT date = '01-SEP-2019'
,#TODT date = '30-SEP-2019'
,#N int
,#SUN int = 0
,#MON int = 0
,#TUE int = 0
,#WED int = 0
,#THU int = 0
,#FRI int = 0
,#SAT int = 0
WHILE #FRDT <= #TODT
BEGIN
SET #N = DATEPART(WEEKDAY, #FRDT)
IF #N = 1
SET #SUN = #SUN + 1
ELSE IF #N = 2
SET #MON = #MON + 1
ELSE IF #N = 3
SET #TUE = #TUE + 1
ELSE IF #N = 4
SET #WED = #WED + 1
ELSE IF #N = 5
SET #THU = #THU + 1
ELSE IF #N = 6
SET #FRI = #FRI + 1
ELSE IF #N = 7
SET #SAT = #SAT + 1
SET #FRDT = DATEADD(DAY, 1, #FRDT)
END
SELECT 1 AS [NO], 'Sunday' AS [DAYNAME], #SUN AS [NUMBEROFDAY]
UNION SELECT 2, 'Monday', #MON
UNION SELECT 3, 'Tuesday', #TUE
UNION SELECT 4, 'Wednesday', #WED
UNION SELECT 5, 'Thursday', #THU
UNION SELECT 6, 'Friday', #FRI
I want to result like code above, but not use loop for better performance.
The date range is 30 days, dividing by 7 gives quotient 4 and remainder 2.
So every day of the week gets 4 and two days need an additional one. These are the ones corresponding to #start_date and the following day in this case.
SQL to implement this approach is below (demo)
SELECT DATENAME(WEEKDAY,base_date),
quotient + IIF(Nums.N < remainder, 1, 0)
FROM (VALUES
(0),
(1),
(2),
(3),
(4),
(5),
(6)) Nums(N)
CROSS APPLY(SELECT 1 + DATEDIFF(DAY,#start_date,#end_date)) DC(day_count)
CROSS APPLY(SELECT DATEADD(DAY, Nums.N, #start_date), day_count/7, day_count% 7) D(base_date, quotient, remainder)
ORDER BY DATEPART(DW,base_date)
You can do it with using recursive CTE as below-
DECLARE #start_date DATE= '01-SEP-2019', #end_date DATE= '30-SEP-2019';
WITH cte
AS (
SELECT #start_date AS date_
UNION ALL
SELECT CAST(DATEADD(day, 1, date_) AS DATE)
FROM cte
WHERE date_ < #end_date
)
SELECT DATEPART(DW,date_) No,
DATENAME(DW,date_) Day_Name,
COUNT(*) Num_Day
FROM cte
GROUP BY DATEPART(DW,date_),DATENAME(DW,date_)
ORDER BY DATEPART(DW,date_)
OPTION(MAXRECURSION 0);
Output-
No Day_Name Num_Day
1 Sunday 5
2 Monday 5
3 Tuesday 4
4 Wednesday 4
5 Thursday 4
6 Friday 4
7 Saturday 4
For such situation you need to have a Number table or Date Table.
In my example I am using a Number table. You can create number table anyway you want and it will help in many situations.
Create Table tblNumber(Number int primary key)
insert into tblNumber (Number) values(1),(2)...... till thousands or millions
Edit: You could generate the numbers for this number table using:
INSERT INTO tblNumber
SELECT TOP 100000 ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS [N]
FROM dbo.syscolumns tb1,dbo.syscolumns tb2
Keep this table permanently as it is useful.
DECLARE #FromDT DATETIME= '2019-09-01';
DECLARE #ToDT DATETIME= '2019-09-30';
SELECT COUNT(*), wkday
FROM
(
SELECT DATEname(weekday, DATEADD(day, number, #FromDT)) wkday
FROM tblNumber
WHERE number BETWEEN DATEPART(day, #FromDT) AND DATEPART(day, #ToDT)
) tbl
GROUP BY wkday;
If you have a Date table then it is more efficient in this situation.

Creating a SQL View for full month when table only includes past days

I'm hoping to create a view in a SQL Server database. It will be based on one table which looks like this:
Date (primary key) | Minimum (decimal) | Target (decimal) | Achieved (decimal)
As a month progresses, e.g. January 2015, that table will be populated daily.
I want to create a view which looks like this (say we are only up to 2nd January 2015):
2015-01-01 | Minimum | Target | Achieved | Calculated Column
2015-01-02 | Minimum | Target | Achieved | Calculated Column
2015-01-03 | null | null | null | Calculated Column
...
2015-01-31 | null | null | null | Calculated Column
So, essentially returning data from the table where available but then also adding a row for each future day too. Don't worry about the calculated column, I think I can do that bit.
Does that makes sense? Grateful for any help.
Thank you.
You can generate a calendar table which has all dates in the ranges of interest for it. Then you just join to that table with a condition on the month. See this web site on how to make a calendar table. There are many ways to do it.
Edit: Per this site, you could use a CTE before your query to create a temporary calendar to join to:
declare #start datetime,
#end datetime
set #start = '2006-01-01'
set #end = '2007-01-01'
;
with calendar(date,isweekday, y, q,m,d,dw,monthname,dayname,w) as
(
select #start ,
case when datepart(dw,#start) in (1,7) then 0 else 1 end,
year(#start),
datepart(qq,#start),
datepart(mm,#start),
datepart(dd,#start),
datepart(dw,#start),
datename(month, #start),
datename(dw, #start),
datepart(wk, #start)
union all
select date + 1,
case when datepart(dw,date + 1) in (1,7) then 0 else 1 end,
year(date + 1),
datepart(qq,date + 1),
datepart(mm,date + 1),
datepart(dd,date + 1),
datepart(dw,date + 1),
datename(month, date + 1),
datename(dw, date + 1),
datepart(wk, date + 1) from calendar where date + 1< #end
)
select * from calendar option(maxrecursion 10000)
Based on your question, apparently you need to use a stored procedure or a function since your output must have an end date. So basically, you need a stored procedure or function that accepts a date parameter and outputs a table containing the data you want. Something similar to the following may help:
USE TEST -- Test database. You may want to replace this with your DB name
GO
CREATE PROCEDURE proStackOverflowQuestion25287207 #date date -- To try it, execute: exec proStackOverflowQuestion25287207 '2008-02-11' -- or any date of your choice.
AS
BEGIN
DECLARE #monthdays INT, #dateday INT, #marker INT, #noRecordDate NVARCHAR(30)
SET #marker = 1 -- we start the first day of the month
SET #monthdays = DATEDIFF(dd,#date,(DATEADD(mm,1,#date))) -- get the number of days in the concerned month
SET #dateday = DAY(#date)
CREATE TABLE #temTbl(
[Date] [date] PRIMARY KEY,
[Minimum] [decimal](18, 0) NULL,
[Target] [decimal](18, 0) NULL,
[Achieved] [decimal](18, 0) NULL,
[Calculated] [decimal](18, 0) NULL
)
WHILE #marker <= #monthdays
BEGIN
--insert new record in temp table
IF exists(SELECT * FROM MyTable WHERE DAY([Date]) = #marker AND MONTH([Date]) = MONTH(#date) AND YEAR([Date]) = YEAR(#date))
INSERT INTO #temTbl SELECT *, null FROM MyTable WHERE DAY([Date]) = #marker --*** you may need to replace null by your Calculated value...
ELSE
BEGIN
SET #noRecordDate = CONVERT(NVARCHAR(4), YEAR(#date)) + '-' + CONVERT(NVARCHAR(2), MONTH(#date)) + '-' + CONVERT(NVARCHAR(2), #marker)
INSERT INTO #temTbl([Date], Minimum, [Target], Achieved, Calculated) VALUES (#noRecordDate, null, null, null, null) -- you may need to replace the last null by your calculated value...
END
-- increment marker
SET #marker = #marker + 1
END
SELECT * FROM #temTbl
END

Query to calculate partial labor hours

I'm not sure if it's possible to do exactly what I'm wanting, but hopefuly someone here can give me some direction. I'm trying to do some labor reports and a few of them require me to make comparisons to labor by hour.
Starting as simple as possible, I would like to know how I could query over employee In and Out times for a given hour and find out how many total hours have been worked.
For example, if I have:
id In Out
-- ------- ------
1 12:00pm 8:30pm
2 12:00pm 8:15pm
3 8:15pm 11:00pm
and I wanted to query for how many hours of labor there were in the 8 o'clock hour then I would expect to see a result of 2 hours (or 120 minutes... 30m + 15m + 45m).
Then moving on from there I'd like to write a query that will show this information for the whole day, but group on the hour showing this same information for each hour. I realize there are other concerns (i.e. I would also need to group on the date, etc.) but if I can figure this out then the rest will be a breeze.
For more context, you can see a similar question I have here: Query for labor cost by hour
(The difference between this question and the other one, if it's not clear, is that I am asking about a specific approach here. If you think you have a better approach to this problem, then please add it to the other question.)
Try this to get your started, need to tweak it a bit, but should give you what you want on your sample data...
select id,
minutes - case when inMinutes < 0 then 0 else inminutes end as TotalMins
from
(
select id,
case when datediff(mi,'8:00pm',OutTime) >60 then 60 else datediff(mi,'8:00pm',OutTime) end as Minutes,
case when datediff(mi,'8:00pm',InTime) >60 then 60 else datediff(mi,'8:00pm',InTime) end as InMinutes
from testhours
) xx
If that is a Microsoft system then datetime values can be converted to double precision floating point numbers where the decimal point divides the date and time like this:
integer part: number of days since 1900-01-01
decimal part: time in the day (so that 0.5 = 12:00:00)
So you could use something like this:
-- sample datetime values
declare #in datetime, #out datetime
set #in = '2012-01-18 12:00:00'
set #out = '2012-01-18 20:30:00'
-- calculation
declare #r_in real, #r_out real
set #r_in = cast(#in as real)
set #r_out = cast(#out as real)
select (24 * (#r_out - #r_in)) as [hours]
BUT it is not that precise, so I would recommend this for calculation:
select cast(datediff(second, #in, #out) as real)/3600 as [hours]
OK, I would also suggest to use functions because that's maybe a lot slower on large tables but also easier and more simple to code.
But here's another solution without functions:
-- the day you're interested in:
declare #day datetime
set #day = '2012-01-20'
-- sample data
declare #moves table (id int, tin datetime, tout datetime)
insert into #moves values
(1, '2012-01-20 06:30:00', '2012-01-20 15:45:00'),
(2, '2012-01-20 13:05:00', '2012-01-20 19:45:00'),
(3, '2012-01-20 10:10:00', '2012-01-20 10:50:00'),
(4, '2012-01-20 19:35:00', '2012-01-20 21:00:00')
-- a helper table with hours
declare #i int
declare #hours table (h int, f datetime, t datetime)
set #i = 0
while #i < 24
begin
insert into #hours values(#i, dateadd(hour, #i, #day), dateadd(hour, #i + 1, #day))
set #i = #i + 1
end
-- here's the code
select h.h,
sum(case sign(datediff(second, h.f, m.tin))
when 1 then
case sign(datediff(second, m.tout, h.t))
when 1 then datediff(minute, m.tin , m.tout)
else datediff(minute, m.tin , h.t)
end
when null then null
else
case sign(datediff(second, m.tout, h.t))
when 1 then datediff(minute, h.f, m.tout)
else datediff(minute, h.f, h.t)
end
end) as minutesWorked,
count(distinct m.id) as peopleWorking
from #hours h inner join #moves m
-- on h.f >= m.tin and h.t <= m.tout
on h.f <= m.tout and h.t >= m.tin
group by h.h
order by h.h
This will give you the following results:
h minutesWorked peopleWorking
----------- ------------- -------------
6 30 1
7 60 1
8 60 1
9 60 1
10 100 2
11 60 1
12 60 1
13 115 2
14 120 2
15 105 2
16 60 1
17 60 1
18 60 1
19 70 2
20 60 1
21 0 1