SQL Server 2005 Bitwise Which Day is next? - sql

I have a table called Jobs that keeps track of jobs and their next run time. One particular scheduling options allows a job to run several times per week. I use bitwise comparisons to discern which day comes next (Well... I'm trying to anyway.) So for example. I have a table like this..
JobID NextRunTime DaysOfWeek
1 12-26-2011 21
My bitwise enumeration is like this..
Monday = 1
Tuesday = 2
Wednesday = 4
Thursday = 8
Friday = 16
Saturday = 32
Sunday = 64.
So we know that this job should run on Monday, Wednesday, Friday. (12-26-2011) is a Monday, so when it updates, it should run again on 12-28-2011 but I am unable to come up with an algorithm that allows me to do programmatically set the new NextRunTime.
This is the method I'm currently trying to get to work with some pseudo-code for what I'm having problems with..
IF OBJECT_ID('tempdb..#DaysSchedule') IS NOT NULL DROP TABLE #DaysSchedule
CREATE TABLE #DaysSchedule
(
Monday int, Tuesday Int, Wednesday Int, Thursday INT, Friday INT, Saturday INT, Sunday INT
)
INSERT INTO #DaysSchedule (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday)
Values(21 & 1,21 & 2,21 & 4,21 & 8,21 & 16 ,21 & 32,21 & 64)
This gives us a table that looks like this:
Monday Tuesday Wednesday Thursday Friday Saturday Sunday
1 0 4 0 16 0 0
From here the (half) pseudo-code is easy.
for (int i=1; i<7, i++)
{
thisDay = DATENAME(dw, DATEADD(day, i, nextRunTime)) -- we add one day
if (column named thisDay contains a value > 0) -- if that days value > 0
begin
We add the difference of thisDay to NextRunTime to NextRunTime and we're done.
end
}

NOTE: I'm not going to comment on the idea of representing multiple items of data in a single field. It may or may not be appropriate in this case, I'm just commenting on how to make this type of idea work.
The problem that you are facing is that the information does not actually closely match its use.
At present...
- Extract the DAY from NextRunTime
- Identify the BIT representing that day
- SEARCH for the next bit set to one, cycling around to the start if necessary
- Identify the distance traveled in that search
- Add that distance to NextRunTime
It's just not efficient or simple.
I would recommend instead recording the number of days to add to reach the next planned date.
Examples:
-----15 = Saturday and Sunday Only
1111111 = Every Day
11113-- = Every Weekday
2-2-3-- = Monday, Wednesday, Friday
This changes the algorithm to...
- Extract the DAY from NextRunTime
- Identify the character in that position
- Cast it to an INT
- Add that many days to NextRunTime
This avoids a search and count section, replacing it with a straight look-up.
It does allow 'dead-ends', or more complex plans. This may be an advantage or dis-advantage depending on your situation...
1111100 = Every weekday for a week, then stop
2222222 = Every other day, on a two week cycle

Would it be so bad to use three rows to model the three days? e.g.
INSERT INTO Jobs (JobID, NextRunTime, RepeatOption)
VALUES (1, '2011-12-26', 'Y');
INSERT INTO RepeatJobs (JobID, RepeatOption, DaysOffset)
VALUES (1, 'Y', 2),
(1, 'Y', 4);
If you must go with bitwise, how about creating a lookup table e.g.
VALUES (1, 'Monday'),
(2, 'Tuesday'),
(3, 'Monday'),
(3, 'Tuesday'),
(4, 'Wednesday'),
(5, 'Monday'),
(5, 'Wednesday'),
(6, 'Tuesday'),
(6, 'Wednesday'),
(7, 'Monday'),
(7, 'Tuesday'),
(7, 'Wednesday'),
(8, 'Thursday'),
(9, 'Monday'),
(9, 'Thursday'),
(10, 'Tuesday'),
(10, 'Thursday'),
(11, ...
...but rather than 'Monday', 'Tuesday', 'Wednesday' etc store the offset in days from a set day of the week, say Sunday, then round down your NextRunTime to the Sunday then add the offset etc.

Related

How to create a SQL date range query where the date range depends on holidays and weekends?

I would like fetch all records in Postgres that match a particular date range. The column 'date' is of type Date.
Return all rows where date is tomorrow's date
If today is a Friday, fetch rows that are Saturday, Sunday and Monday
If Monday is a holiday (ex: July 4), on Friday (7/1), we will fetch transactions for Saturday (7/2), Sunday (7/3), Monday (7/4) and Tuesday (7/5)
There may be multiple holidays in tandem and the logic needs to take that into account. Assume the holidays are all available as an array of dates.
My current solution is to blindly fetch all rows that match tomorrow, and then using code (the app is Ruby on Rails) perform the above logic and look ahead if necessary.
Is there an elegant SQL solution to the above problem?
If you are able to create array of dates to fetch in ruby, as you should be you are doing that already right now to figure out if you need to fetch anything else after tomorrow, you should be able to call something like this:
array_of_dates = [Date.new(2022, 4, 30), Date.new(2022, 5, 1), Date.new(2022, 5, 2)]
MyModel.where(date: array_of_dates).order(:date)
That should be enough. It gets translated to SQL like this:
select * from my_models
where date IN ('2022-04-30', '2022-05-01', '2022-05-02')
order by date
Order is not necessary.

Is there way to insert the same data into multiple columns is SQL?

Let's say we wanted to put the same data into multiple rows like so.
Column names for this example:
Monday, Tuesday, Wednesday, Thursday, Friday
INSERT INTO sample
VALUES ('long string')
This is an example of what I mean so the same value is in each column
Monday | Tuesday | Wednesday | Thursday | Friday
long string | long string | long string | long string | long string
I want that long string within all columns, is there a way to do this without using multiple insert statements? OR is there a way to create a CTE that could automate this to make it easier?
You have to list all the values:
insert into sample (Monday, Tuesday, Wednesday, Thursday, Friday)
values ('long string', 'long string', 'long string', 'long string', 'long string');
You don't specify the database, but you could use a subquery to reduce the size of the query:
insert into sample (Monday, Tuesday, Wednesday, Thursday, Friday)
select val, val, val, val, val
from (select 'long string' as val) x;
You could expand Gordon's example like this:
DECLARE #valuesToInsert TABLE (val NVARCHAR(MAX))
INSERT INTO #valuesToInsert
VALUES
('This is a long sentence'),
('This is another long sentence'),
('This is yet another long sentence')
INSERT INTO sample (Monday, Tuesday, Wednesday, Thursday, Friday)
SELECT val, val, val, val, val
FROM #valuesToInsert
#valuesToInsert is your datasource that you want to use
(I've created a table variable just to show multiple data rows
Try
insert into sample (Monday, Tuesday, Wednesday, Thursday, Friday)
values select 'long string'

Expand bit fields to multiple rows (kinda join)

let's say I have a table like this:
CREATE TABLE [dbo].[Scheduler](
[DayOfWeek] [tinyint] NOT NULL,
[Time] [time](0) NOT NULL,
[Action] [varchar](255) NOT NULL
)
And some data, like this:
INSERT INTO Scheduler VALUES (1, '11:00:00', 'Sunday')
INSERT INTO Scheduler VALUES (2, '12:00:00', 'Monday')
INSERT INTO Scheduler VALUES (4, '13:00:00', 'Tuesday')
INSERT INTO Scheduler VALUES (8, '14:00:00', 'Wednesday')
INSERT INTO Scheduler VALUES (16, '15:00:00', 'Thursday')
INSERT INTO Scheduler VALUES (32, '16:00:00', 'Friday')
INSERT INTO Scheduler VALUES (64, '17:00:00', 'Saturday')
INSERT INTO Scheduler VALUES (62, '06:00:00', 'Every business day')
INSERT INTO Scheduler VALUES (127, '08:00:00', 'Every day')
How can I produce multiple rows in a SELECT statement if DayOfWeek has more than one flag?
For example, this row:
INSERT INTO Scheduler VALUES (62, '06:00:00', 'Every business day')
It will be represented in 5 rows in a SELECT statement (one for each day/flag set)
DayOfWeek Time Message
--------- ---------------- ---------------------------
2 06:00:00 Every business day
4 06:00:00 Every business day
8 06:00:00 Every business day
16 06:00:00 Every business day
32 06:00:00 Every business day
Running the same query with all the data will give me 19 rows.
7 rows - one row for each individual day (1, 2, 4, 8, 16, 32, 64)
5 rows - business days (62)
7 rows - every day (127)
I'm not sure how can I do this.
I think I can use a cursor to do this, but it is the best option here?
Thanks.
You need SQL Server's Bitwise Operators. This example shows how you can determine which flags are contained in the current value.
DECLARE #Mon INT = 1;
DECLARE #Tue INT = 2;
DECLARE #Wed INT = 4;
DECLARE #MonAndTue INT = 3; -- Mon (1) and Tue (2) = 3.
SELECT
#MonAndTue & #Mon, -- Contains Monday, returns Monday.
#MonAndTue & #Tue, -- Contains Tuesday, returns Tuesday.
#MonAndTue & #Wed -- Does not contain Wednesday, returns 0.
Where possible I'd recommend avoiding bit masking based solutions in SQL Server. These work great in other languages (C springs to mind). But SQL works best when each column holds a single value, describing a single item. Of course you could combined these approaches. This table design allows you to retain the base 2 keys (great for the frontend) and includes simple bit fields that make filtering in the backend a straightforward task.
CREATE TABLE [Day]
(
DayId INT PRIMARY KEY,
[DayName] VARCHAR(9),
IsMonday BIT,
IsTuesday BIT,
...
IsSunday BIT
)
;
EDIT
Sorry! I didn't actually answer your question. To use the bitwise operations in a join your need syntax long these lines:
WITH Scheduler AS
(
/* Sample data.
*/
SELECT
*
FROM
(
VALUES
(1, 'Sunday'),
(2, 'Monday'),
(4, 'Tuesday'),
(8, 'Wednesday'),
(16, 'Thursday'),
(32, 'Friday'),
(64, 'Saturday'),
(62, 'Every business day'),
(127, 'Every day')
) AS r(DayId, [DayName])
)
/* This join returns every day from s2 that
* is contained within the s1 record Every business day.
*/
SELECT
*
FROM
Scheduler AS s1
INNER JOIN Scheduler AS s2 ON (s1.DayId & s2.DayId) = s2.DayId
WHERE
s1.DayId = 62
;
Here S1 is filtered to return Every business day. S2 joins on S1 where a match is found. This returns Mon, Tue, Wed, etc without returning Sat and Sun.

Issues with updating a SQL table using a CASE statement

I'm fairly new to SQL and still trying to get my head around a few concepts...
I have created a simple table and i'm trying to update one of the columns in the table using a CASE statement. I understand why the statment won't work but i'm confused as to how to fix it....
This is my code:
CREATE TABLE q5
(
UserID INT,
Month INT,
Score INT
)
INSERT INTO q5(UserID, Month, Score)
VALUES (1,1,10), (1,2,5), (1,1,6), (2,8,6), (3,1,9), (3,4,11), (3,6,9), (4,9,10), (5,1,2);
UPDATE q5
SET
Month = CASE WHEN Month IN (1, 2, 3) THEN 'First Quarter'
WHEN Month IN (4, 5, 6) THEN 'Second Quarter'
WHEN Month IN (7, 8, 9) THEN 'Third Quarter'
ELSE 'Fourth Quarter'
END
I'm getting this error:
Msg 245, Level 16, State 1, Line 1
Conversion failed when converting the varchar value 'First Quarter' to data type int.
Month is defined as an int, yet you are trying to update the table and set Month to a varchar, like 'First Quarter'. You can either change Month to store a varchar, and then modify your insert statement and WHEN Month IN pieces, or add an additional column that can store the quarter.
The query below shows the second option (adding an additional column).
CREATE TABLE q5
(
UserID INT,
Month INT,
Score INT,
Quarter varchar(20)
)
INSERT INTO q5(UserID, Month, Score)
VALUES (1,1,10), (1,2,5), (1,1,6), (2,8,6), (3,1,9), (3,4,11), (3,6,9), (4,9,10), (5,1,2);
UPDATE q5
SET
Quarter = CASE WHEN Month IN (1, 2, 3) THEN 'First Quarter'
WHEN Month IN (4, 5, 6) THEN 'Second Quarter'
WHEN Month IN (7, 8, 9) THEN 'Third Quarter'
ELSE 'Fourth Quarter'
END
Depending on how this is being utilized, you might be better off adding a View which would calculate the Quarter. This way, you wouldn't have to maintain that additional column, which is entirely dependant on the Month column.
Your column MONTH can only store data of type INT. What you've got now is similar to the following procedural code:
int month = 2;
if (month == 1 || month == 2 || month == 3)
{
month = "First Quarter"; // compiler error
}
As you can see, that conditionally executed line is fairly nonsensical, since month is of type int.
You're probably looking to add another column, say Quarter.
ALTER TABLE q5 ADD Quarter NVARCHAR(100)
UPDATE q5
SET
Quarter = CASE WHEN Month IN (1, 2, 3) THEN 'First Quarter'
WHEN Month IN (4, 5, 6) THEN 'Second Quarter'
WHEN Month IN (7, 8, 9) THEN 'Third Quarter'
ELSE 'Fourth Quarter'
END
Although realistically, listing the value names should be done by the UI. Best practice would be to use a computed column or separate VIEW that has a Quarter INT column. This code would add a computed column with the appropriate datatype.
ALTER TABLE q5 ADD
Quarter AS CASE WHEN Month IN (1, 2, 3) THEN 1
WHEN Month IN (4, 5, 6) THEN 2
WHEN Month IN (7, 8, 9) THEN 3
ELSE 4
END

Get 1st Day of Week (Sunday)

I have this formula in my column to calculate the start date of a given week :
dateadd(week,[Week]-(1),
dateadd(day,(-1),
dateadd(week,
datediff(week,(0),
CONVERT([varchar](4),[Year],(0))+'-01-01'),
(1))))
Where Week and Year are other fields like 38 and 2012
Problem is, it calculates the start date of week 38/2012 as a monday (17th Sept), I would like it to be a sunday instead (16th Sept) is this possible?
Many thanks.
This will return you the first day of the week, given a week number and a year, assuming that the first day of the week is a Sunday.
Standard exclusions apply, e.g. don't try year 1499.
declare #tbl table ([Week] int, [Year] int)
insert #tbl select 38,2012
union all select 1,2012
union all select 0,2012
union all select 1,2013
select DATEADD(
Week,
[Week]-1,
DATEADD(
Day,
(8-##datefirst)-DATEPART(dw, CAST([Year]*10000+101 AS VARCHAR(8))),
CAST([Year]*10000+101 AS VARCHAR(8))))
from #TBL
Result
2012-09-16 00:00:00.000
2012-01-01 00:00:00.000
2011-12-25 00:00:00.000
2012-12-30 00:00:00.000
Note that the Week number starts from 1, and if the week doesn't start on a Sunday, then the first day of that week could end up in an earlier year (row #4). Because the Weeks are relative, you can use Week 0, -1 etc and it will still give you a result (row #3), rightly or wrongly.
You may also notice I used a different method to create a date out of the year, just as an alternative.
The (8-##datefirst) portion of the query makes it robust regardless of your DATEFIRST setting.
If you want the first day of the week to be Sunday you could change your database to make it so, using the DATEFIRST setting.
function getFirstDateOfThisWeek(d)
{
var TempDate = new Date(d || new Date());
TempDate.setDate(TempDate.getDate() + (#Config.WeekStartOn - 1 - TempDate.getDay() - 7) % 7);
return TempDate;
}
var StartDate = getFirstDateOfThisWeek(new Date()); //1st date of this week