TSQL Selecting 2 & 3 week intervals - sql

I am attempting to populate a table based on 2 and 3 week intervals for a semi-monthly pay period in TSQL. The table should populate,
2 week date
2 week date
3 week date
2 week date
2 week date
3 week date
..based on the first date I supply, subsequently adding 2 or 3 weeks to the last date supplied. I should be able to supply a start date and end date. It may be that it's just early in the morning, but can't think of an elegant way to accomplish this task. Any pointers?
Thanks!
George

WITH dates (d, n) AS
(
SELECT #mydate, 1
UNION ALL
SELECT DATEADD(week, CASE n % 3 WHEN 0 THEN 3 ELSE 2 END, d), n + 1
FROM dates
WHERE d < #enddate
)
INSERT
INTO mytable
SELECT d
FROM dates
OPTION (MAXRECURSION 0)

Horrid brute force approach - because the 2,2,3 is difficult to loop just adding it regardless into the temp table and then filtering at the end incase a couple extra entries go in - not the most efficient but if you are needing to just get a range one off then it works.
So the caveat here is: ok for one off, I wouldn't use in production :)
declare #start datetime
declare #end datetime
declare #calculated datetime
set #start = '20010101'
set #end = '20011231'
set #calculated = #start
Create Table #Dates (PayDate datetime)
while #calculated <= #end
begin
set #calculated = DateAdd(wk,2,#calculated)
insert into #Dates(paydate) values (#calculated)
set #calculated = DateAdd(wk,2,#calculated)
insert into #Dates(paydate) values (#calculated)
set #calculated = DateAdd(wk,3,#calculated)
insert into #Dates(paydate) values (#calculated)
end
select * from #Dates where paydate >= #start and paydate <= #end
drop table #dates

So you have a 7-week cycle -- figure out which 7-week period you're in from some known starting point and then which week of this group of 7 you are.

Related

Generate n consecutive dates with even day-part after today's date (for example for 2020-06-08 it will be 2020-06-10, 2020-06-12, 2020-06-14 ect.)

i need a bit help i dont know wat is wrong in my code . Plz help.
create procedure consecutive_N_even_day
#n int
as
begin
declare #na int
set #na = 0
declare #nexndate date
declare #date date
set #date=GETDATE()
declare #datepart int
while(#na<#n)
begin
set #nexndate=DATEADD(DAY,#na,#date)
set #datepart=DATEPART(day,#nexndate)
if #datepart%2=0
begin
print #nexndate;
end
set #na=#na+1
end
end
You can use a recursive query for this:
with cte as (
select dateadd(day, day(getdate()) % 2, cast(getdate() as date)) dt, 0 n
union all
select dateadd(day, 2, dt), n + 1 from cte where n < #n
)
select * from cte
The query starts from today (if it is an even day) or tomorrow, and then produces a series of dates with a 2 days increment. It iterates #n times.
You don't specify what to do if we reach the end of a month that has an uneven number of days. It is always possible to adapt the logic of the recursive part of the query to what you actually need in this case.
If #n is greater than 100, you need to add option (maxrecursion 0) at the very end of the query.

tsql grouping with duplication based on variable

I want to create some aggregations from a table but I am not able to figure out a solution.
Example table:
DECLARE #MyTable TABLE(person INT, the_date date, the_value int)
INSERT INTO #MyTable VALUES
(1,'2017-01-01', 10),
(1,'2017-02-01', 5),
(1,'2017-03-01', 5),
(1,'2017-04-01', 10),
(1,'2017-05-01', 2),
(2,'2017-04-01', 10),
(2,'2017-05-01', 10),
(2,'2017-05-01', 0),
(3,'2017-01-01', 2)
For each person existing at that time, I want to average the value for the last x (#months_back) months given some starting date (#start_date):
DECLARE #months_back int, #start_date date
set #months_back = 3
set #start_date = '2017-05-01'
SELECT person, avg(the_value) as avg_the_value
FROM #MyTable
where the_date <= #start_date and the_date >= dateadd(month, -#months_back, #start_date)
group by person
This works. I now want to do the same thing again but skip back some months (#month_skip) from the starting date. Then I want to union those two tables together. Then, I again want to skip back #month_skip months from this date and do the same thing. I want to continue doing this until I have skipped back to some specified date (#min_date).
DECLARE #months_back int, #month_skip int, #start_date date, #min_date date
set #months_back = 3
set #month_skip = 2
set #start_date = '2017-05-01'
set #min_date = '2017-03-01'
Using the above variables and the table #MyTable the result should be:
person | avg_the_value
1 | 5
2 | 6
1 | 6
3 | 2
Only one skip is made here since #min_date is 2 months back but I would like to be able to do multiple skips based on what #min_date is.
This example table is simple but the real one has many more automatically created columns and therefore it is not feasible to use a table variable where I would have to declare the scheme of the resulting table.
I asked a related question Here but did not manage to get any of the answers to work for this problem.
It sounds like what you're trying to do is the following:
Starting with a date (e.g. 2017-05-01), look back #months_back months and define a range of dates. For example, if we go 3 months back, we're defining a range from 2017-02-01 through 2017-05-01.
After we define this range, we go back to our starting date and define a new starting date, going back #month_skip months. For example, with an initial starting date of 2017-05-01, we might skip back 2 months, giving us a new starting date of 2017-03-01.
We take this new starting date, and define a range of corresponding dates (as we did above). This produces the range 2016-12-01 through 2017-03-01.
We repeat this as needed through the minimum date specified, to produce a list of date ranges we want to do calculations for:
2017-03-01 through 2017-05-01
2016-12-01 through 2017-03-01
... etc ...
For each of these periods, look at a person and calculate the average of their value.
The query below should do what is described above: rather than taking a value and iterating back to calculate previous values, we use a numbers table to calculate offsets on an interval, which is used to determine the ending and starting dates for each interval/period. This query was built using SQL Server 2008 R2 and should be compatible with future versions.
/* Table, data, variable declarations */
DECLARE #MyTable TABLE(person INT, the_date date, the_value int)
INSERT INTO #MyTable VALUES
(1,'2017-01-01', 10),
(1,'2017-02-01', 5),
(1,'2017-03-01', 5),
(1,'2017-04-01', 10),
(1,'2017-05-01', 2),
(2,'2017-04-01', 10),
(2,'2017-05-01', 10),
(2,'2017-05-01', 0),
(3,'2017-01-01', 2)
DECLARE #months_back int, #month_skip int, #start_date date, #min_date date
set #months_back = 3
set #month_skip = 2
set #start_date = '2017-05-01'
set #min_date = '2017-01-01'
/* Common table expression to build list of Integers */
/* reference http://www.itprotoday.com/software-development/build-numbers-table-you-need if you want more info */
declare #end_int bigint = 50
; WITH IntegersTableFill (ints) AS
(
SELECT
CAST(0 AS BIGINT) AS 'ints'
UNION ALL
SELECT (T.ints + 1) AS 'ints'
FROM IntegersTableFill T
WHERE ints <= (
CASE
WHEN (#end_int <= 32767) THEN #end_int
ELSE 32767
END
)
)
/* What we're going to do is define a series of periods.
These periods have a start date and an end date, and will simplify grouping
(in place of the calculate-and-union approach)
*/
/* Now, we start defining the periods
#months_Back_start defines the end of the range we need to calculate for.
#month_skip defines the amount of time we have to jump back for each period
*/
/* Using the number table we defined above and the data in our variables, calculate start and end dates */
,periodEndDates as
(
select ints as Period
,DATEADD(month, -(#months_back*ints), #start_date) as endOfPeriod
from IntegersTableFill itf
)
,periodStartDates as
(
select *
,DATEADD(month, -(#month_skip), endOfPeriod) as startOfPeriod
from periodEndDates
)
,finalPeriodData as
(
select (period) as period, startOfPeriod, endOfPeriod from periodStartDates
)
/* Link the entries in our original data to the periods they fall into */
/* NOTE: The join criteria originally specified allows values to fall into multiple periods.
You may want to fix this?
*/
,periodTableJoin as
(
select * from finalPeriodData fpd
inner join #MyTable mt
on mt.the_date >= fpd.startOfPeriod
and mt.the_date <= fpd.endOfPeriod
and mt.the_date >= #min_date
and mt.the_date <= #start_date
)
/* Calculate averages, grouping by period and person */
,periodValueAggregate as
(
select person, avg(the_value) as avg_the_value from
periodTableJoin
group by period, person
)
select * from periodValueAggregate
The method I propose is set-based, not iterative.
(I am not following your problem exactly, but please follow along and we can iron out any discrepancies)
Essentially, you are looking to divide a calendar up in to periods of interest. The periods are all equal in width and are sequential.
For this, I propose you build a calendar table and mark the periods using division as illustrated in the code;
DECLARE #CalStart DATE = '2017-01-01'
,#CalEnd DATE = '2018-01-01'
,#CalWindowSize INT = 2
;WITH Numbers AS
(
SELECT TOP (DATEDIFF(MONTH, #CalStart, #CalEnd)) N = CAST(ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS INT) - 1
FROM syscolumns
)
SELECT CalWindow = N / #CalWindowSize
,CalDate = DATEADD(MONTH, N, #CalStart)
FROM Numbers
Once you have correctly configured the variables, you should have a calendar that represents the windows of interest.
It is then a matter of affixing this calendar to your dataset and grouping by not only the person but the CalWindow too;
DECLARE #MyTable TABLE(person INT, the_date date, the_value int)
INSERT INTO #MyTable VALUES
(1,'2017-01-01', 10),
(1,'2017-02-01', 5),
(1,'2017-03-01', 5),
(1,'2017-04-01', 10),
(1,'2017-05-01', 2),
(2,'2017-04-01', 10),
(2,'2017-05-01', 10),
(2,'2017-05-01', 0),
(3,'2017-01-01', 2)
----------------------------------
-- Build Calendar
----------------------------------
DECLARE #CalStart DATE = '2017-01-01'
,#CalEnd DATE = '2018-01-01'
,#CalWindowSize INT = 2
;WITH Numbers AS
(
SELECT TOP (DATEDIFF(MONTH, #CalStart, #CalEnd)) N = CAST(ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS INT) - 1
FROM syscolumns
)
,Calendar AS
(
SELECT CalWindow = N / #CalWindowSize
,CalDate = DATEADD(MONTH, N, #CalStart)
FROM Numbers
)
SELECT TB.Person
,AVG(TB.the_value)
FROM #MyTable TB
JOIN Calendar CL ON TB.the_date = CL.CalDate
GROUP BY CL.CalWindow, TB.person
Hope I have understood your problem.

SELECT all date interval from timestamp to timestamp

Is there a way to select all days between two timestamps.
Say I have 2 timestamps 2/1/2015 and 2/28/2015
Is there a way to SELECT all days between the too
Output:
days
--------
2/1/2015 |
2/2/2015 |
2/3/2015 |
...
2/26/2015 |
2/27/2015 |
2/28/2015 |
Edit:
I am trying to count some records that were created each day between to dates, The table only has a timecreated field with the time stamp of when they were created, what I need to do is generate a table that has all days between 2 dates and a 0 count if no records were created that day or the X for the number of records created.
You can use generate_series to generate all dates between your two dates, and do a join with your table using the timestamp column:
SELECT a_date, count(days) FROM
generate_series('2015-01-01'::timestamp, '2015-03-31', '1 day') a_date
left outer join
my_table
on my_table.days = a_date
group by a_date
Sure. How about:
SELECT * FROM table WHERE days>='2015-02-01' AND days<='2015-02-28'
You can use count() on the column indicating that there's a record or not. Say your column is called "Record" :
SELECT count(record) as NbRecord FROM table WHERE days>='2015-02-01' AND days<='2015-02-28'
If you want a table that actually contains the dates, this is one way to do it:
--gives a table which contains every date between start & end (inclusive)
declare #startdate datetime = '05/01/2015';
declare #enddate datetime = '05/20/2015';
declare #result table(d datetime);
declare #diff int = datediff(d,#startdate,#enddate);
declare #i int = 0;
while(#i<=#diff) begin
insert into #result(d) values (#startdate + #i);
set #i = #i + 1;
end;
select * from #result

Sql Server 2008 Calculate nights between two dates but remove 31st night

I have a booking table with the following information:
BookingID,(unique, not null)
StartDate, (not null)
EndDate (not null)
I need to calculate the number of nights someone remained in residence which I can do with a DATEDIFF between EndDate and StartDate. However, if someone is in residence for the entire month during a 31 day month we only charge them 30.
I'm not sure how to do this in SQL. I was thinking I would have to create a variable, calculate on a monthly basis and add to the variable, but that seems like it would be messy and take a very long time, especially towards the end of year. This needs to be done for about 5,000 records on a daily basis.
So:
If someone starts on 7/25/14 and ends 9/2/14, the nights is 38 not 39.
If someone starts on 10/2/14 and ends on 11/1/14, the nights is 30.
If someone starts on 10/2/14 and ends on 10/31/14, the nights is 29.
We will be calculating into the future so it doesn't matter if the end date is greater than the day the report is being ran.
Does anyone have any ideas how to accomplish this in the best way?
I would first to create a lookup table with all the month with 31 days
Such as
DECLARE #month TABLE (start_date date,end_date date)
INSERT INTO #month VALUES ('2014-07-01','2014-07-31'),('2014-08-01','2014-08-31'),('2014-10-01','2014-10-31'),('2014-12-01','2014-12-31')
//populate all months in your calculate range
Then you can calculate the value with
DECLARE #start DATE = '2014-07-25', #end DATE = '2014-09-02'
SELECT DATEDIFF(day,#start,#end) -
(SELECT COUNT(*) FROM #month WHERE start_date >= #start AND end_date <= #end)
Retrieve the integer part of the datediff divided by 31 :
SELECT DATEDIFF(day,'2014-07-25', '2014-09-02') - DATEDIFF(day,'2014-07-25', '2014-09-02') / 31
The SQLish solution is to create a calendar table that holds all the dates you will ever care about and any business meaning that those dates may have, such as "Is this a holiday?", "Is this off-season?", or "do we charge for this day?"
This may sound insane to someone accustomed to other programming languages, but it is perfectly sensible in the database world. Business rules and business logic get stored as data, not as code.
Make this table and populate it:
CREATE TABLE Calendar (
[date] date
,[is_charged] bit
)
and then you can write code that is nearly plain English:
SELECT
[BookingID]
,COUNT([date])
FROM BookingTable
INNER JOIN Calendar
ON [date] >= [StartDate]
AND [date] < [EndDate]
WHERE [is_charged] = 1
GROUP BY [BookingId]
When your business rules change, you just update the calendar instead of rewriting the code.
If I've read your question correctly then you can't actually use those solutions above which consist of a table of billable and non billable days because the 31st is billable unless the whole month was booked.
I reckon this is probably a job for a user defined function. Which runs up a total starting with the month that the start date is in and finishing with the month that the end date is in.
CREATE FUNCTION dbo.FN_BillableDays (#StartDate date, #EndDate date)
returns int
AS
BEGIN
IF #StartDate > #EndDate
BEGIN
return null --Error
END
DECLARE #Next date
DECLARE #MonthStart date
DECLARE #MonthEnd date
DECLARE #NextMonthStart date
DECLARE #n int =0
SET #Next = #StartDate
SET #MonthStart = DATEADD(day,1-DAY(#Next),#Next)
SET #NextMonthStart = DATEADD(month,1,#MonthStart )
SET #MonthEnd = DATEADD(day,-1,#NextMonthStart)
WHILE DATEDIFF(month,#Next,#EndDate) >0
BEGIN
SET #n = #n +
CASE
WHEN DAY(#next) = 1 AND DAY(#MonthEnd) = 31 THEN 30
WHEN DAY(#next) = 1 THEN DAY(#MonthEnd)
ELSE 1+DAY(#MonthEnd) -DAY(#next) END
SET #MonthStart = #NextMonthStart
SET #NextMonthStart = DATEADD(month,1,#MonthStart )
SET #MonthEnd = DATEADD(day,-1,#NextMonthStart)
SET #Next = #NextMonthStart
END
--Month of the EndDate
SET #n = #n +
CASE
WHEN DAY(#next) = 1 AND DAY(#EndDate) = 31 THEN 29
WHEN DAY(#next) = 1 THEN DAY(#EndDate)-1
ELSE DAY(#MonthEnd) -DAY(#EndDate) END
return #n
END
I tried it with some test dates
SELECT
b.BookingID,
b.StartDate,
b.EndDate,
dbo.FN_BillableDays (b.StartDate,b.EndDate) AS BillableDays
FROM dbo.Booking b
And got the following
BookingID StartDate EndDate BillableDays
----------- ---------- ---------- ------------
1 2013-12-31 2014-01-02 2
2 2013-12-31 2014-01-30 30
3 2014-01-01 2014-01-30 29
4 2014-01-01 2014-01-31 29
5 2014-01-01 2014-02-01 30
6 2014-01-01 2014-02-02 31
7 2014-02-02 2014-02-01 NULL
(7 row(s) affected)
Which matches my understanding of the logic you want to implement but you may want to tweak the last bit which adds on the days for the final month. If they leave on the 31st do you want to give them their last night (30th to 31st) for free.
If you don't then delete the line
WHEN DAY(#next) = 1 AND DAY(#EndDate) = 31 THEN 29

Grouping by contiguous dates, ignoring weekends in SQL

I'm attempting to group contiguous date ranges to show the minimum and maximum date for each range. So far I've used a solution similar to this one: http://www.sqlservercentral.com/articles/T-SQL/71550/ however I'm on SQL 2000 so I had to make some changes. This is my procedure so far:
create table #tmp
(
date smalldatetime,
rownum int identity
)
insert into #tmp
select distinct date from testDates order by date
select
min(date) as dateRangeStart,
max(date) as dateRangeEnd,
count(*) as dates,
dateadd(dd,-1*rownum, date) as GroupID
from #tmp
group by dateadd(dd,-1*rownum, date)
drop table #tmp
It works exactly how I want except for one issue: weekends. My data sets have no records for weekend dates, which means any group found is at most 5 days. For instance, in the results below, I would like the last 3 groups to show up as a single record, with a dateRangeStart of 10/6 and a dateRangeEnd of 10/20:
Is there some way I can set this up to ignore a break in the date range if that break is just a weekend?
Thanks for the help.
EDITED
I didn't like my previous idea very much. Here's a better one, I think:
Based on the first and the last dates from the set of those to be grouped, prepare the list of all the intermediate weekend dates.
Insert the working dates together with weekend dates, ordered, so they would all be assigned rownum values according to their normal order.
Use your method of finding contiguous ranges with the following modifications:
1) when calculating dateRangeStart, if it's a weekend date, pick the nearest following weekday;
2) accordingly for dateRangeEnd, if it's a weekend date, pick the nearest preceding weekday;
3) when counting dates for the group, pick only weekdays.
Select from the resulting set only those rows where dates > 0, thus eliminating the groups formed only of the weekends.
And here's an implementation of the method, where it is assumed, that a week starts on Sunday (DATEPART returns 1) and weekend days are Sunday and Saturday:
DECLARE #tmp TABLE (date smalldatetime, rownum int IDENTITY);
DECLARE #weekends TABLE (date smalldatetime);
DECLARE #minDate smalldatetime, #maxDate smalldatetime, #date smalldatetime;
/* #1 */
SELECT #minDate = MIN(date), #maxDate = MAX(date)
FROM testDates;
SET #date = #minDate - DATEPART(dw, #minDate) + 7;
WHILE #date < #maxDate BEGIN
INSERT INTO #weekends
SELECT #date UNION ALL
SELECT #date + 1;
SET #date = #date + 7;
END;
/* #2 */
INSERT INTO #tmp
SELECT date FROM testDates
UNION
SELECT date FROM #weekends
ORDER BY date;
/* #3 & #4 */
SELECT *
FROM (
SELECT
MIN(date + CASE DATEPART(dw, date) WHEN 1 THEN 1 WHEN 7 THEN 2 ELSE 0 END)
AS dateRangeStart,
MAX(date - CASE DATEPART(dw, date) WHEN 1 THEN 2 WHEN 7 THEN 1 ELSE 0 END)
AS dateRangeEnd,
COUNT(CASE WHEN DATEPART(dw, date) NOT IN (1, 7) THEN date END) AS dates,
DATEADD(d, -rownum, date) AS GroupID
FROM #tmp
GROUP BY DATEADD(d, -rownum, date)
) s
WHERE dates > 0;