How to keep the leap year when substracting 1 year - sql

I have this query that gives me a given date for each of the past 15 years. When my starting date is February 29 it does not return the 29 for year 2012, 2008 and 2004. How can I have this query to return the 29 for those years?
DECLARE #TempDate1 TABLE (Entry_Date Date)
INSERT INTO #TempDate1 values ('2016-02-29')
;WITH
a AS(SELECT DATEADD(yy,-1,Entry_Date) d, DATEADD(yy,-1,Entry_Date) d2,0 i
FROM #TempDate1
UNION all
SELECT DATEADD(yy,-1,d),DATEADD(yy,-1,d2),i+1 FROM a WHERE i<14),
b AS(SELECT d,d2, DATEDIFF(dd,0,d)%7 dd,i FROM a)
SELECT
d AS Entry_Date
FROM b
It returns this:
Entry_Date
2015-02-28
2014-02-28
2013-02-28
2012-02-28
2011-02-28
2010-02-28
2009-02-28
2008-02-28
2007-02-28
2006-02-28
2005-02-28
2004-02-28
2003-02-28
2002-02-28
2001-02-28
While I would like to have this:
Entry_Date
2015-02-28
2014-02-28
2013-02-28
2012-02-29
2011-02-28
2010-02-28
2009-02-28
2008-02-29
2007-02-28
2006-02-28
2005-02-28
2004-02-29
2003-02-28
2002-02-28
2001-02-28

Perhaps DateAdd in concert with an ad-hoc tally table
Example
Declare #YourTable Table ([Entry_Date] date)
Insert Into #YourTable Values
('2016-02-29')
,('2015-07-22')
Select YearNr = N
,Anniv = dateadd(YEAR,N*-1,Entry_Date)
From #YourTable A
Cross Apply (
Select Top 15 N=Row_Number() Over (Order By (Select NULL)) From master..spt_values n1
) B
Returns

Simply by using EOMONTH function (SQL Server 2012 and above):
DECLARE #TempDate1 TABLE (Entry_Date Date)
INSERT INTO #TempDate1 values ('2016-02-29')
;WITH
a AS(SELECT DATEADD(yy,-1,Entry_Date) d, DATEADD(yy,-1,Entry_Date) d2,0 i
FROM #TempDate1
UNION all
SELECT DATEADD(yy,-1,d),DATEADD(yy,-1,d2),i+1 FROM a WHERE i<14),
b AS(SELECT d,d2, DATEDIFF(dd,0,d)%7 dd,i FROM a)
SELECT EOMONTH(d) AS Entry_Date
FROM b;
Rextester Demo

Rewrite tour query like this... Not only will handle leap years without jumping through hoops, it's orders of magnitude more efficient than what you currently have.
DECLARE #BaseDate DATE = '2016-02-29';
SELECT
Entry_Date = DATEADD(YEAR, t.n, #BaseDate)
FROM
(VALUES (-1),(-2),(-3),(-4),(-5),
(-6),(-7),(-8),(-9),(-10),
(-11),(-12),(-13),(-14),(-15) ) t (n);
Results...
Entry_Date
----------
2015-02-28
2014-02-28
2013-02-28
2012-02-29
2011-02-28
2010-02-28
2009-02-28
2008-02-29
2007-02-28
2006-02-28
2005-02-28
2004-02-29
2003-02-28
2002-02-28
2001-02-28
EDIT: Same functionality when used with a table of dates (I stole John's table)
DECLARE #YourTable TABLE (id INT, Entry_Date DATE);
INSERT INTO #YourTable VALUES (1, '2016-02-29'), (2, '2015-07-22');
SELECT
yt.id,
Entry_Date = DATEADD(YEAR, t.n, yt.Entry_Date)
FROM
#YourTable yt
CROSS APPLY (VALUES (-1),(-2),(-3),(-4),(-5),
(-6),(-7),(-8),(-9),(-10),
(-11),(-12),(-13),(-14),(-15) ) t (n);
GO
Results...
id Entry_Date
----------- ----------
1 2015-02-28
1 2014-02-28
1 2013-02-28
1 2012-02-29
1 2011-02-28
1 2010-02-28
1 2009-02-28
1 2008-02-29
1 2007-02-28
1 2006-02-28
1 2005-02-28
1 2004-02-29
1 2003-02-28
1 2002-02-28
1 2001-02-28
2 2014-07-22
2 2013-07-22
2 2012-07-22
2 2011-07-22
2 2010-07-22
2 2009-07-22
2 2008-07-22
2 2007-07-22
2 2006-07-22
2 2005-07-22
2 2004-07-22
2 2003-07-22
2 2002-07-22
2 2001-07-22
2 2000-07-22

Related

How to select rows based on a rolling 30 day window SQL

My question involves how to identify an index discharge.
The index discharge is the earliest discharge. On that date, the 30 day window starts. Any admissions during that time period are considered readmissions, and they should be ignored. Once the 30 day window is over, then any subsequent discharge is considered an index and the 30 day window begins again.
I can't seem to work out the logic for this. I've tried different windowing functions, I've tried cross joins and cross applies. The issue I keep encountering is that a readmission cannot be an index admission. It must be excluded.
I have successfully written a while loop to solve this problem, but I'd really like to get this in a set based format, if it's possible. I haven't been successful so far.
Ultimate goal is this -
id
AdmitDate
DischargeDate
MedicalRecordNumber
IndexYN
1
2021-03-03 00:00:00.000
2021-03-09 13:20:00.000
X0090362
1
4
2021-03-05 00:00:00.000
2021-03-10 16:00:00.000
X0012614
1
6
2021-05-18 00:00:00.000
2021-05-21 22:20:00.000
X0012614
1
7
2021-06-21 00:00:00.000
2021-07-08 13:30:00.000
X0012614
1
8
2021-02-03 00:00:00.000
2021-02-09 17:00:00.000
X0019655
1
10
2021-03-23 00:00:00.000
2021-03-26 16:40:00.000
X0019655
1
11
2021-03-15 00:00:00.000
2021-03-18 15:53:00.000
X4135958
1
13
2021-05-17 00:00:00.000
2021-05-23 14:55:00.000
X4135958
1
15
2021-06-24 00:00:00.000
2021-07-13 15:06:00.000
X4135958
1
Sample code is below.
CREATE TABLE #Admissions
(
[id] INT,
[AdmitDate] DATETIME,
[DischargeDateTime] DATETIME,
[UnitNumber] VARCHAR(20),
[IndexYN] INT
)
INSERT INTO #Admissions
VALUES( 1 ,'2021-03-03' ,'2021-03-09 13:20:00.000' ,'X0090362', NULL)
,(2 ,'2021-03-27' ,'2021-03-30 19:59:00.000' ,'X0090362', NULL)
,(3 ,'2021-03-31' ,'2021-04-04 05:57:00.000' ,'X0090362', NULL)
,(4 ,'2021-03-05' ,'2021-03-10 16:00:00.000' ,'X0012614', NULL)
,(5 ,'2021-03-28' ,'2021-04-16 13:55:00.000' ,'X0012614', NULL)
,(6 ,'2021-05-18' ,'2021-05-21 22:20:00.000' ,'X0012614', NULL)
,(7 ,'2021-06-21' ,'2021-07-08 13:30:00.000' ,'X0012614', NULL)
,(8 ,'2021-02-03' ,'2021-02-09 17:00:00.000' ,'X0019655', NULL)
,(9 ,'2021-02-17' ,'2021-02-22 17:25:00.000' ,'X0019655', NULL)
,(10 ,'2021-03-23' ,'2021-03-26 16:40:00.000' ,'X0019655', NULL)
,(11 ,'2021-03-15' ,'2021-03-18 15:53:00.000' ,'X4135958', NULL)
,(12 ,'2021-04-08' ,'2021-04-13 19:42:00.000' ,'X4135958', NULL)
,(13 ,'2021-05-17' ,'2021-05-23 14:55:00.000' ,'X4135958', NULL)
,(14 ,'2021-06-09' ,'2021-06-14 12:45:00.000' ,'X4135958', NULL)
,(15 ,'2021-06-24' ,'2021-07-13 15:06:00.000' ,'X4135958', NULL)
You can use a recursive CTE to identify all rows associated with each "index" discharge:
with a as (
select a.*, row_number() over (order by dischargedatetime) as seqnum
from admissions a
),
cte as (
select id, admitdate, dischargedatetime, unitnumber, seqnum, dischargedatetime as index_dischargedatetime
from a
where seqnum = 1
union all
select a.id, a.admitdate, a.dischargedatetime, a.unitnumber, a.seqnum,
(case when a.dischargedatetime > dateadd(day, 30, cte.index_dischargedatetime)
then a.dischargedatetime else cte.index_dischargedatetime
end) as index_dischargedatetime
from cte join
a
on a.seqnum = cte.seqnum + 1
)
select *
from cte;
You can then incorporate this into an update:
update admissions
set indexyn = (case when admissions.dischargedatetime = cte.index_dischargedatetime then 'Y' else 'N' end)
from cte
where cte.id = admissions.id;
Here is a db<>fiddle. Note that I changed the type of IndexYN to a character to assign 'Y'/'N', which makes sense given the column name.

Summing Records within a Moving Date Range, Date Distances

I have complex calculation requirement for a user logging system. I need to locate the most frequently active users based on their number of logins within a 180 day window. Once two login dates are 181 days apart, they do not count towards a total but could count towards a total when grouped with other dates.
For example here is Jim's login history:
Jim 2018-01-01
Jim 2018-04-01
Jim 2018-05-01
Jim 2018-06-01
Jim 2018-07-01
Jim 2018-08-01
Jim 2018-09-01
Jim 2018-12-01
Using 6 months, instead of 180 days, for simplicity, and only looking 6 months in one direction, Jim had the following totals:
Logins: 5 (2018-01-01 + 6 months)
Logins: 6 (2018-04-01 + 6 months)
Logins: 5 (2018-05-01 + 6 months)
Logins: 5 (2018-06-01 + 6 months)
Logins: 4 (2018-07-01 + 6 months)
Logins: 3 (2018-08-01 + 6 months)
Logins: 2 (2018-09-01 + 6 months)
Logins: 1 (2018-12-01 + 6 months)
So my system would report back 6 because it only wants the maximum total.
Other than brute force calculation, I'm lost on how to construct this system. Yes I can denormalize data to any degree, speed is most important.
Try this:
declare #tbl table(name char(3), dt date);
insert into #tbl values
('Jim', '2018-01-01'),
('Jim', '2018-04-01'),
('Jim', '2018-05-01'),
('Jim', '2018-06-01'),
('Jim', '2018-07-01'),
('Jim', '2018-08-01'),
('Jim', '2018-09-01'),
('Jim', '2018-12-01');
;with cte as (
select name, dt, DATEADD(day, 181, dt) upperDt from #tbl
), cte2 as (
select name,
(select COUNT(*) from cte where dt between c.dt and c.upperDt and name = c.name) cnt
from cte c
)
select name, MAX(cnt) [max]
from cte2
group by name
Try this, using a Common Table Expression to Calculate the EndDate Window and CROSS APPLY to calculate the total number of logins
DECLARE #t TABLE (UserName NVARCHAR(10), LoginDate DATETIME)
INSERT INTO #t
(UserName,LoginDate) VALUES
('Jim','2018-01-01'),
('Jim','2018-04-01'),
('Jim','2018-05-01'),
('Jim','2018-06-01'),
('Jim','2018-07-01'),
('Jim','2018-08-01'),
('Jim','2018-09-01'),
('Jim','2018-12-01')
; WITH CteDateRange
AS(
SELECT
T.UserName
,T.LoginDate
--,EndDateRange = DATEADD(DAY, 181, LoginDate)
,EndDateRange = DATEADD(MONTH, 6, LoginDate)
FROM #t T
)
SELECT
DR.UserName
,DR.LoginDate
,DR.EndDateRange
,T.Total
FROM CteDateRange DR
CROSS APPLY ( SELECT Total = COUNT(D.LoginDate)
FROM CteDateRange D
WHERE D.LoginDate >= DR.LoginDate
AND D.LoginDate <= DR.EndDateRange
AND D.UserName = DR.UserName
) T
Output
UserName LoginDate EndDateRange Total
Jim 2018-01-01 00:00:00.000 2018-07-01 00:00:00.000 5
Jim 2018-04-01 00:00:00.000 2018-10-01 00:00:00.000 6
Jim 2018-05-01 00:00:00.000 2018-11-01 00:00:00.000 5
Jim 2018-06-01 00:00:00.000 2018-12-01 00:00:00.000 5
Jim 2018-07-01 00:00:00.000 2019-01-01 00:00:00.000 4
Jim 2018-08-01 00:00:00.000 2019-02-01 00:00:00.000 3
Jim 2018-09-01 00:00:00.000 2019-03-01 00:00:00.000 2
Jim 2018-12-01 00:00:00.000 2019-06-01 00:00:00.000 1
One basic solution uses a join:
select l.*
from (select l.name, count(*) as cnt,
row_number() over (partition by name order by count(*) desc) as seqnum
from logins l join
logins l2
on l.name = l2.name and
l2.date >= l.date and l2.date < dateadd(day, 181, l.date)
group by l.name
) l
where seqnum = 1;
This might have acceptable performance with an index on logins(name, date).

SQL Server 2014 Exploding Dates show only year

I got this data from my table:
id arrival_date departure_date
1 2017-10-10 2017-10-15
2 2017-11-16 2017-11-30
3 2017-10-14 2017-10-31
4 2018-05-12 2017-05-22
5 2021-01-16 2021-03-15
6 2018-06-02 2017-07-02
All i want is to explode these date but i only want to show the year only
date
2017
2018
2019
2020
2021
these should be the once that I wanted to see is their any query for these?
Note: if this year is 2018 it should start in 2018, so i will use getdate() in that instance, i want to get the max in departure_date part, can I do that?
You can try a recursive cte
DECLARE #maxYear INT
SELECT #maxYear = MAX(YEAR(departure_date)) FROM YourTable
;WITH cte
AS
(
SELECT YEAR(GETDATE()) AS [Year]
UNION ALL
SELECT [Year] + 1
FROM cte
WHERE [Year] < #maxYear
)
SELECT *
FROM cte
DEMO
Just use YEAR(arrival_date) and YEAR(departure_date)

Calculate discount between weeks

I have a table containing product price data, like that:
ProductId RecordDate Price
46 2015-01-17 14:35:05.533 112.00
47 2015-01-17 14:35:05.533 88.00
45 2015-01-17 14:35:05.533 134.00
I have been able to group data by week and product, with this query:
SET DATEFIRST 1;
SELECT DATEADD(WEEK, DATEDIFF(WEEK, 0, [RecordDate]), 0) AS [Week], ProductId, MIN([Price]) AS [MinimumPrice]
FROM [dbo].[ProductPriceHistory]
GROUP BY DATEADD(WEEK, DATEDIFF(WEEK, 0, [RecordDate]), 0), ProductId
ORDER BY ProductId, [Week]
obtaining this result:
Week Product Price
2015-01-12 00:00:00.000 1 99.00
2015-01-19 00:00:00.000 1 98.00
2015-01-26 00:00:00.000 1 95.00
2015-02-02 00:00:00.000 1 95.00
2015-02-09 00:00:00.000 1 95.00
2015-02-16 00:00:00.000 1 95.00
2015-02-23 00:00:00.000 1 80.00
2015-03-02 00:00:00.000 1 97.00
2015-03-09 00:00:00.000 1 85.00
2015-01-12 00:00:00.000 2 232.00
2015-01-19 00:00:00.000 2 233.00
2015-01-26 00:00:00.000 2 194.00
2015-02-02 00:00:00.000 2 194.00
2015-02-09 00:00:00.000 2 199.00
2015-02-16 00:00:00.000 2 199.00
2015-02-23 00:00:00.000 2 199.00
2015-03-02 00:00:00.000 2 214.00
Now for each product I'd like to get the difference between the last two week values, so that I can calculate the discount. I don't know how to write this as a SQL Query!
EDIT:
Expected output would be something like that:
Product Price
1 -12.00
2 15.00
Thank you!
since you are using Sql Server 2014 you can use LAG or LEAD window function to do this.
Generate Row number to find the last two weeks for each product.
;WITH cte
AS (SELECT *,
Row_number()OVER(partition BY product ORDER BY weeks DESC)rn
FROM Yourtable)
SELECT product,
price
FROM (SELECT product,
Price=price - Lead(price)OVER(partition BY product ORDER BY rn)
FROM cte a
WHERE a.rn <= 2) A
WHERE price IS NOT NULL
SQLFIDDLE DEMO
Traditional solution, can be used before Sql server 2012
;WITH cte
AS (SELECT *,
Row_number()OVER(partition BY product
ORDER BY weeks DESC)rn
FROM Yourtable)
SELECT a.Product,
b.Price - a.Price
FROM cte a
LEFT JOIN cte b
ON a.Product = b.Product
AND a.rn = b.rn + 1
WHERE a.rn <= 2
AND b.Product IS NOT NULL

Finding occupied dates from date ranges

This is the data that I have on SQL-Sever 2005 mode
ID FromDate ToDate Diff
ZIM145876-01 03/01/2011 02/29/2012 1
ZIM145876-01 03/01/2012 02/28/2013 1
ZIM145876-01 03/01/2013 02/28/2014 NULL
ZIM145881-02 02/01/2012 03/31/2012 1
ZIM145881-02 04/01/2012 06/30/2012 1
ZIM145881-02 07/01/2012 09/30/2012 1
ZIM145881-02 10/01/2012 03/31/2013 1
ZIM145881-02 04/01/2013 06/30/2013 NULL
ZIM145878-01 05/15/2010 05/14/2011 201
ZIM145878-01 12/01/2011 11/30/2012 1
ZIM145878-01 12/01/2012 11/30/2013 NULL
Now on first case I want to have
ZIM145876-01 03/01/2011 02/28/2014
ZIM145881-02 02/01/2012 06/30/2013
However in 3rd case we have two occupied dates for same IDs and this is what I want
ZIM145878-01 05/15/2010 05/14/2011
ZIM145878-01 12/01/2011 11/30/2013
So any hint would be highly appreciated
(SQLFiddle)
This seems to do the job. Unfortunately, it's not going to be too efficient on large data sets:
declare #t table (ID varchar(50),FromDate date,ToDate date, Diff int)
insert into #t(ID,FromDate,ToDate,Diff) values
('ZIM145876-01','20110301','20120229',1 ),
('ZIM145876-01','20120301','20130228',1 ),
('ZIM145876-01','20130301','20140228',NULL),
('ZIM145881-02','20120201','20120331',1 ),
('ZIM145881-02','20120401','20120630',1 ),
('ZIM145881-02','20120701','20120930',1 ),
('ZIM145881-02','20121001','20130331',1 ),
('ZIM145881-02','20130401','20130630',NULL),
('ZIM145878-01','20100515','20110514',201 ),
('ZIM145878-01','20111201','20121130',1 ),
('ZIM145878-01','20121201','20131130',NULL)
;With Islands as (
select ID,FromDate,ToDate from #t t1 where not exists (select * from #t t2 where t2.ToDate = DATEADD(day,-1,t1.FromDate) and t1.ID = t2.ID)
union all
select i.ID,i.FromDate,t.ToDate
from
Islands i
inner join
#t t
on
i.ID = t.ID and
i.ToDate = DATEADD(day,-1,t.FromDate)
)
select ID,FromDate,MAX(ToDate) from Islands
group by ID,FromDate
Results:
ID FromDate
-------------------------------------------------- ---------- ----------
ZIM145878-01 2010-05-15 2011-05-14
ZIM145876-01 2011-03-01 2014-02-28
ZIM145878-01 2011-12-01 2013-11-30
ZIM145881-02 2012-02-01 2013-06-30
It could be more efficient if the occupancy was recorded as a semi-open interval (e.g. Inclusive FromDate, Exclusive ToDate) - because then we wouldn't have to call DATEADD to find the place where the periods join together.
I don't know what the Diff column is about, and I haven't used it.