Related
Count for each day - the number of days until the closer workday.
It is possible to limit the number of days to look ahead by 20 days.
DATE
IS_HOLIDAY
desirable result
05.01.2008
1
4
06.01.2008
1
3
07.01.2008
1
2
08.01.2008
1
1
09.01.2008
0
1
10.01.2008
0
1
11.01.2008
0
3
12.01.2008
1
2
13.01.2008
1
1
14.01.2008
0
1
15.01.2008
0
1
16.01.2008
0
1
17.01.2008
0
1
data for query:
create table #tmp ( [date] date, is_holiday int )
insert into #tmp ( date, is_holiday )
select '2008-01-05' date, 1 is_holiday union
select '2008-01-06' date, 1 is_holiday union
select '2008-01-07' date, 1 is_holiday union
select '2008-01-08' date, 1 is_holiday union
select '2008-01-09' date, 0 is_holiday union
select '2008-01-10' date, 0 is_holiday union
select '2008-01-11' date, 0 is_holiday union
select '2008-01-12' date, 1 is_holiday union
select '2008-01-13' date, 1 is_holiday union
select '2008-01-14' date, 0 is_holiday union
select '2008-01-15' date, 0 is_holiday union
select '2008-01-16' date, 0 is_holiday union
select '2008-01-17' date, 0 is_holiday
I've tried to use the construction like:
select date, sum(convert(int, is_holiday)) over (
order by date
rows between 5 preceding and current row
) as rsum
from dic_calendar_production
But it looks behind. When I change 'preceding' on 'following', it throws the error
'BETWEEN ... FOLLOWING AND CURRENT ROW' is not a valid window frame
and cannot be used with the OVER clause.
Moreover, it can give me the sum in the some range, but it will not stop when it reach the first zero.
Seems like you just need to put your data into groups, and then ROW_NUMBER:
create table #tmp ( [date] date, is_holiday int )
insert into #tmp ( date, is_holiday )
VALUES ('2008-01-05', 1),
('2008-01-06', 1),
('2008-01-07', 1),
('2008-01-08', 1),
('2008-01-09', 0),
('2008-01-10', 0),
('2008-01-11', 0),
('2008-01-12', 1),
('2008-01-13', 1),
('2008-01-14', 0),
('2008-01-15', 0),
('2008-01-16', 0),
('2008-01-17', 0);
GO
WITH CTE AS(
SELECT [date],
is_holiday,
COUNT(CASE is_holiday WHEN 0 THEN 1 END) OVER (ORDER BY [date] ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING) AS Grp
FROM #tmp t)
SELECT [date],
is_holiday,
ROW_NUMBER() OVER (PARTITION BY Grp ORDER BY [date] DESC) AS desirableresult
FROM CTE
ORDER BY [date];
GO
DROP TABLE #tmp;
You could use a correlated query with datediff:
select *,
IsNull(
DateDiff(
day, t.[date],
(select Min([date]) from #tmp t2 where t2.[date] > t.[date] and t2.is_holiday = 0)
),1) Result
from #tmp t
order by [date];
DB<>Fiddle example
select t.date,t.is_holiday,isnull(app.date,t.date),isnull(DATEDIFF(day,t.date,app.date),1)diff
from #tmp t
cross apply
(
select min(x.date) as date
from #tmp as x
where t.date<x.date
and x.is_holiday=0
)app
You can also try cross apply-approach
I'm struggling with this for a few days... trying to write an SQL query to get all date ranges when all units overlap at the same time. It's better to see it graphically.
Here is the simplified table with the image for reference:
UnitId Start End
====== ========== ==========
1 05/01/2018 09/01/2018
1 10/01/2018 13/01/2018
2 04/01/2018 15/01/2018
2 19/01/2018 23/01/2018
3 06/01/2018 12/01/2018
3 14/01/2018 22/01/2018
Expected result:
Start End
====== ==========
06/01/2018 09/01/2018
10/01/2018 12/01/2018
What I currently have:
DECLARE #sourceTable TABLE (UnitId int, StartDate datetime, EndDate datetime);
INSERT INTO #sourceTable VALUES
(1, '2018-01-05', '2018-01-09')
,(1, '2018-01-10', '2018-01-13')
,(2, '2018-01-04', '2018-01-15')
,(2, '2018-01-19', '2018-01-23')
,(3, '2018-01-06', '2018-01-12')
,(3, '2018-01-14', '2018-01-22');
SELECT DISTINCT
(SELECT max(v) FROM (values(A.StartDate), (B.StartDate)) as value(v)) StartDate
,(SELECT min(v) FROM (values(A.EndDate), (B.EndDate)) as value(v)) EndDate
FROM #sourceTable A
JOIN #sourceTable B
ON A.startDate <= B.endDate AND A.endDate >= B.startDate AND A.UnitId != B.UnitId
I believe it is "count number of overlapping intervals" problem (this picture should help). Here is one solution to it:
DECLARE #t TABLE (UnitId INT, [Start] DATE, [End] DATE);
INSERT INTO #t VALUES
(1, '2018-01-05', '2018-01-09'),
(1, '2018-01-10', '2018-01-13'),
(2, '2018-01-04', '2018-01-15'),
(2, '2018-01-19', '2018-01-23'),
(3, '2018-01-06', '2018-01-12'),
(3, '2018-01-14', '2018-01-22');
WITH cte1(date, val) AS (
SELECT [Start], 1 FROM #t AS t
UNION ALL
SELECT [End], 0 FROM #t AS t
UNION ALL
SELECT DATEADD(DAY, 1, [End]), -1 FROM #t AS t
), cte2 AS (
SELECT date, SUM(val) OVER (ORDER BY date, val) AS usage
FROM cte1
)
SELECT date, MAX(usage) AS usage
FROM cte2
GROUP BY date
It will give you a list of all dates at which the use count (possibly) changed:
date usage
2018-01-04 1
2018-01-05 2
2018-01-06 3
2018-01-09 3
2018-01-10 3
2018-01-12 3
2018-01-13 2
2018-01-14 2
2018-01-15 2
2018-01-16 1
2018-01-19 2
2018-01-22 2
2018-01-23 1
2018-01-24 0
With this approach you do not need a calendar table or rCTE to build missing dates. Converting the above to ranges (2018-01-05 ... 2018-01-15, 2018-01-19 ... 2018-01-22 etc) is not very difficult.
DECLARE #t TABLE (UnitId INT, [Start] DATE, [End] DATE);
INSERT INTO #t VALUES
(1, '2018-01-05', '2018-01-09'),
(1, '2018-01-10', '2018-01-13'),
(2, '2018-01-04', '2018-01-15'),
(2, '2018-01-19', '2018-01-23'),
(3, '2018-01-06', '2018-01-12'),
(3, '2018-01-14', '2018-01-22');
WITH cte1(date, val) AS (
SELECT [Start], 1 FROM #t AS t -- starting date increments counter
UNION ALL
SELECT [End], 0 FROM #t AS t -- we need all edges in the result
UNION ALL
SELECT DATEADD(DAY, 1, [End]), -1 FROM #t AS t -- end date + 1 decrements counter
), cte2 AS (
SELECT date, SUM(val) OVER (ORDER BY date, val) AS usage -- running sum for counter
FROM cte1
), cte3 AS (
SELECT date, MAX(usage) AS usage -- group multiple events on same date together
FROM cte2
GROUP BY date
), cte4 AS (
SELECT date, usage, CASE
WHEN usage > 1 AND LAG(usage) OVER (ORDER BY date) > 1 THEN 0
WHEN usage < 2 AND LAG(usage) OVER (ORDER BY date) < 2 THEN 0
ELSE 1
END AS chg -- start new group if prev and curr usage are on opposite side of 1
FROM cte3
), cte5 AS (
SELECT date, usage, SUM(chg) OVER (ORDER BY date) AS grp -- number groups for each change
FROM cte4
)
SELECT MIN(date) date1, MAX(date) date2
FROM cte5
GROUP BY grp
HAVING MIN(usage) > 1
Result:
date1 date2
2018-01-05 2018-01-15
2018-01-19 2018-01-22
You are looking for date ranges where all units overlap. So look for start dates where all units exist and end dates where all units exist and then join the two.
I'm using ROW_NUMBER to join the first start date with the first end date, the second start date with the second end date and so on.
select s.startdate, e.enddate
from
(
select startdate, row_number() over (order by startdate) as rn
from #sourceTable s1
where
(
select count(*)
from #sourceTable s2
where s1.startdate between s2.startdate and s2.enddate
) = (select count(distinct unitid) from #sourceTable)
) s
join
(
select enddate, row_number() over (order by startdate) as rn
from #sourceTable s1
where
(
select count(*)
from #sourceTable s2
where s1.enddate between s2.startdate and s2.enddate
) = (select count(distinct unitid) from #sourceTable)
) e on e.rn = s.rn
order by s.startdate;
There may be more elegant ways to solve this, but I guess this query is at least easy to understand :-)
Rextester demo: https://rextester.com/GRRSW89045
I have one table (A) with date ranges and another (B) with just a set date. There are missing months in B that are within the date range of A. I need to identify the missing months.
A
Person StartDate EndDate
123 1/1/2016 5/1/2016
B
Person EffectiveDate
123 1/1/2016
123 2/1/2016
123 4/1/2016
123 5/1/2016
Expected result would be
123 3/1/2016
I'm using SQL Server 2012. Any assistance would be appreciated. Thanks!
One approach is to generate all values between the two dates. Here is an approach using a numbers table:
with n as (
select row_number() over (order by (select null)) - 1 as n
from master.spt_values
)
select a.person, dateadd(day, n.n, a.startdate) as missingdate
from a join
n
on dateadd(day, n.n, a.startdate) <= day.enddate left join
b
on b.person = a.person and b.effectivedate = dateadd(day, n.n, a.startdate)
where b.person is null;
Try this:
CREATE TABLE #A (Person INT, StartDate DATE, EndDate DATE)
INSERT INTO #A
SELECT '123','1/1/2016', '5/1/2016'
CREATE TABLE #B(Person INT, EffectiveDate DATE)
INSERT INTO #B
SELECT 123 ,'1/1/2016' UNION ALL
SELECT 123 ,'2/1/2016' UNION ALL
SELECT 123 ,'4/1/2016' UNION ALL
SELECT 123 ,'5/1/2016'
;WITH A1
AS(
SELECT PERSON , StartDate, EndDate
FROM #A
UNION ALL
SELECT PERSON ,DATEADD(MM,1,STARTDATE), EndDate
FROM A1
WHERE DATEADD(MM,1,STARTDATE) <= EndDate
)
SELECT PERSON , StartDate
FROM A1
WHERE
NOT EXISTS
(
SELECT 1 FROM #B B1
WHERE B1.Person = A1.PERSON
AND YEAR(B1.EffectiveDate) = YEAR(A1.STARTDATE) AND MONTH(B1.EffectiveDate) = MONTH(A1.STARTDATE)
)
This should work if you are interested in getting missing months
;WITH n
AS (SELECT ROW_NUMBER() OVER(ORDER BY
(
SELECT NULL
)) - 1 AS n
FROM master.dbo.spt_values)
SELECT a.person,
DATEADD(MONTH, n.n, a.startdate) AS missingdate
FROM a a
INNER JOIN n ON DATEADD(MONTH, n.n, a.startdate) <= a.enddate
LEFT JOIN b b ON MONTH(DATEADD(MONTH, n.n, a.startdate)) = MONTH(b.effectivedate) AND YEAR(DATEADD(MONTH, n.n, a.startdate)) = YEAR(b.effectivedate)
WHERE b.person IS NULL;
Is there any function to check for continuous date. I'm having problem on working with this issue below:
My table has a datetime column with the following data:
----------
2015-03-11
2015-03-12
2015-03-13
2015-03-16
Given start date as 2015-3-11 and end date as 2015-3-17. I want the result as:
----------
2015-03-11
2015-03-12
2015-03-13
Can anyone suggest anything ?
I'm thinking this is somewhat a variation of Grouping Islands of Contiguous Dates problem. This can be done using ROW_NUMBER():
SQL Fiddle
CREATE TABLE Test(
tDate DATETIME
)
INSERT INTO Test VALUES
('20150311'), ('20150312'), ('20150313'), ('20150316');
DECLARE #startDate DATE = '20150311'
DECLARE #endDate DATE = '20150317'
;WITH Cte AS(
SELECT
*,
RN = DATEADD(DD, - (ROW_NUMBER() OVER(ORDER BY tDATE) - 1), tDate)
FROM Test
WHERE
tDate >= #startDate
AND tDate < DATEADD(DAY, 1, #endDate)
)
SELECT CAST(tDate AS DATE)
FROM CTE
WHERE RN = #startDate
RESULT
|------------|
| 2015-03-11 |
| 2015-03-12 |
| 2015-03-13 |
Here is the SQL Server 2005 version:
SQL Fiddle
DECLARE #startDate DATETIME
DECLARE #endDate DATETIME
SET #startDate = '20150311'
SET #endDate = '20150317'
;WITH Cte AS(
SELECT
*,
RN = DATEADD(DD, -(ROW_NUMBER() OVER(ORDER BY tDATE)-1), tDate)
FROM Test
WHERE
tDate >= #startDate
AND tDate < DATEADD(DAY, 1, #endDate)
)
SELECT CONVERT(VARCHAR(10), tDate, 121)
FROM CTE
WHERE RN = #startDate
For MSSQL 2012. This will return MAX continuous groups:
DECLARE #t TABLE(d DATE)
INSERT INTO #t VALUES
('20150311'),
('20150312'),
('20150313'),
('20150316')
;WITH
c1 AS(SELECT d, IIF(DATEDIFF(dd,LAG(d, 1, DATEADD(dd, -1, d)) OVER(ORDER BY d), d) = 1, 0, 1) AS n FROM #t),
c2 AS(SELECT d, SUM(n) OVER(ORDER BY d) AS n FROM c1)
SELECT TOP 1 WITH TIES MIN(d) AS StartDate, MAX(d) AS EndDate, COUNT(*) AS DayCount
FROM c2
GROUP BY n
ORDER BY DayCount desc
Output:
StartDate EndDate DayCount
2015-03-11 2015-03-13 3
For
('20150311'),
('20150312'),
('20150313'),
('20150316'),
('20150317'),
('20150318'),
('20150319'),
('20150320')
Output:
StartDate EndDate DayCount
2015-03-16 2015-03-20 5
Apply filtering in c1 CTE:
c1 AS(SELECT d, IIF(DATEDIFF(dd,LAG(d, 1, DATEADD(dd, -1, d)) OVER(ORDER BY d), d) = 1, 0, 1) AS n FROM #t WHERE d BETWEEN '20150311' AND '20150320'),
For MSSQL 2008:
;WITH
c1 AS(SELECT d, (SELECT MAX(d) FROM #t it WHERE it.d < ot.d) AS pd FROM #t ot),
c2 AS(SELECT d, CASE WHEN DATEDIFF(dd,ISNULL(pd, DATEADD(dd, -1, d)), d) = 1 THEN 0 ELSE 1 END AS n FROM c1),
c3 AS(SELECT d, (SELECT SUM(n) FROM c2 ci WHERE ci.d <= co.d) AS n FROM c2 co)
SELECT TOP 1 WITH TIES MIN(d) AS StartDate, MAX(d) AS EndDate, COUNT(*) AS DayCount
FROM c3
GROUP BY n
ORDER BY DayCount desc
you don't need to declare any start date or end date as other answers says, you need a row_num with datediff function:
create table DateFragTest (cDate date);
insert into DateFragTest
values ('2015-3-11'),
('2015-3-12'),
('2015-3-13'),
('2015-3-16')
with cte as
(select
cDate,
row_number() over (order by cDate ) as rn
from
DateFragTest)
select cDate
from cte t1
where datediff(day,
(select cDate from cte t2 where t2.rn=t1.rn+1),
t1.cDate)<>1
Output:
cDate
2015-03-11
2015-03-12
2015-03-13
SQLFIDDLE DEMO
For sql server 2012-
WITH cte
AS
(
SELECT [datex]
, lead([datex]) OVER ( ORDER BY [datex]) lead_datex
, datediff(dd,[datex],lead([datex]) OVER ( ORDER BY [datex]) ) AS diff
FROM [dbo].[datex]
)
SELECT c.[datex]
FROM [cte] AS c
WHERE diff >=1
Use BETWEEN
The query will go like this:
SELECT *
FROM your_table_name
WHERE your_date_column_name BETWEEN '2015-3-11' AND '2015-3-13'
(dt between x and y) or just (dt >= x and dt <= y).
lets say I have a table with
date,personid
1/1/2001 1
1/2/2001 3
1/3/2001 2
1/4/2001 2
1/5/2001 5
1/6/2001 5
1/7/2001 6
and I'm going to either update 1/2/2001 or 1/5/2001 with personid 2 but before I can update I have to make sure it passes a rule that says you can't have a person three days in a row.
how can i solve this in a mssql stored procedure?
update: It also need to solve this layout as well where I'd update 1/5/2001
date,personid
1/1/2001 1
1/2/2001 3
1/3/2001 2
1/4/2001 2
1/5/2001 1
1/6/2001 2
1/7/2001 2
1/8/2001 5
1/9/2001 5
1/10/2001 6
I've assumed that date is unique let me know if that is not the case!
DECLARE #basedata TABLE ([date] UNIQUE DATE,personid INT)
INSERT INTO #basedata
SELECT GETDATE()+1, 2 union all
SELECT GETDATE()+2, 3 union all
SELECT GETDATE()+3, 2 union all
SELECT GETDATE()+4, 2 union all
SELECT GETDATE()+5, 5 union all
SELECT GETDATE()+6, 5 union all
SELECT GETDATE()+7, 6
DECLARE #date date = GETDATE()+5
DECLARE #personid int = 2
;WITH T AS
(
SELECT TOP 2 [date],personid
FROM #basedata
WHERE [date] < #date
ORDER BY [date] DESC
UNION ALL
SELECT #date, #personid
UNION ALL
SELECT TOP 2 [date],personid
FROM #basedata
WHERE [date] > #date
ORDER BY [date]
),T2 AS
(
SELECT *,
ROW_NUMBER() OVER (ORDER BY [date]) -
ROW_NUMBER() OVER (PARTITION BY personid ORDER BY [date]) AS Grp
FROM T
)
SELECT COUNT(*) /*Will return a result if that date/personid
would cause a sequence of 3*/
FROM T2
GROUP BY personid,Grp
HAVING COUNT(*) >=3
There is a third case not listed, it is the between date case. I included it in the solution below.
The output is
PersonId TrackDate UnallowedBefore UnallowedAfter
----------- ---------- --------------- --------------
2 01/04/2001 01/02/2001 01/05/2001
5 01/06/2001 01/04/2001 01/07/2001
6 01/08/2001 01/08/2001 01/08/2001
USE tempdb
GO
IF OBJECT_ID('PersonDates') IS NOT NULL DROP TABLE PersonDates
CREATE TABLE PersonDates
(
PersonId int NOT NULL,
TrackDate datetime NOT NULL
)
INSERT INTO PersonDates
(
TrackDate,
PersonId
)
SELECT '1/1/2001', 1
UNION ALL
SELECT '1/2/2001', 3
UNION ALL
SELECT '1/3/2001', 2
UNION ALL
SELECT '1/4/2001', 2
UNION ALL
SELECT '1/5/2001', 5
UNION ALL
SELECT '1/6/2001', 5
UNION ALL
SELECT '1/7/2001', 6
UNION ALL
SELECT '1/8/2001', 2
UNION ALL
SELECT '1/9/2001', 6
SELECT
P.PersonId,
TrackDate = CONVERT(varchar(10), DATEADD(day, 1, P.TrackDate), 101),
T.UnallowedBefore,
T.UnallowedAfter
FROM
PersonDates P
CROSS APPLY
(
SELECT TOP 1
UnallowedAfter = CASE
WHEN DATEDIFF(day, P.TrackDate, TrackDate) = 1
THEN CONVERT(varchar(10), DATEADD(day, 1, TrackDate), 101)
ELSE CONVERT(varchar(10), DATEADD(day, -1, TrackDate), 101)
END,
UnallowedBefore = CASE
WHEN DATEDIFF(day, P.TrackDate, TrackDate) = 1
THEN CONVERT(varchar(10), DATEADD(day, -2, TrackDate), 101)
ELSE CONVERT(varchar(10), DATEADD(day, -1, TrackDate), 101)
END
FROM
PersonDates
WHERE
PersonId = P.PersonId
AND
DATEDIFF(day, P.TrackDate, TrackDate) IN (1,2)
) T
SET #TargetDate = '1/2/2001'
SELECT #ForwardCount = COUNT(*) FROM table WHERE ([date] BETWEEN #TargetDate AND DATEADD(dd, 2, #TargetDate)) WHERE PersonID = #PersonID
SELECT #BackwardCount = COUNT(*) FROM table WHERE ([date] BETWEEN #TargetDate AND DATEADD(dd, -2, #TargetDate)) WHERE PersonID = #PersonID
SELECT #BracketCount = COUNT(*) FROM table WHERE ([date] BETWEEN DATEADD(dd, -1, #TargetDate) AND DATEADD(dd, 1, #TargetDate)) WHERE PersonID = #PersonID
IF (#ForwardCount < 2) AND (#BackwardCount < 2) AND (#BracketCount < 2)
BEGIN
-- Do your update here
END
Here's my parametrised solution:
WITH nearby AS (
SELECT
date,
personid = CASE date WHEN #date THEN #personid ELSE personid END
FROM atable
WHERE date BETWEEN DATEADD(day, -#MaxInARow, #date)
AND DATEADD(day, #MaxInARow, #date)
),
nearbyGroups AS (
SELECT
*,
Grp = DATEDIFF(day, 0, date) -
ROW_NUMBER() OVER (PARTITION BY personid ORDER BY date)
FROM nearby
)
UPDATE atable
SET personid = #personid
WHERE date = #date
AND NOT EXISTS (
SELECT Grp
FROM nearbyGroups
GROUP BY Grp
HAVING COUNT(*) > #MaxInARow
)
#date represents the date for which the personid column should be updated. #personid is the new value to be stored. #MaxInARow is the maximum number of days in a row for which the same personid is allowed to be stored.