Conditional counting based on comparison to previous row sql - sql-server

Let's start with a sample of the data I'm working with:
Policy No | start date
1 | 2/15/2006
1 | 2/15/2009
1 | 2/15/2012
2 | 3/15/2006
3 | 3/19/2006
3 | 3/19/2012
4 | 3/31/2006
4 | 3/31/2009
I'm trying to write code in SQL Server 2008 that counts a few things. The principle is that the policyholder's earliest start date is when the policy began. Every three years an increase is offered to the client. If they agree to the increase, the start date is refreshed with the same date as the original, three years later. If they decline, nothing is added to the database at all.
I'm trying to not only count the number of times a customer accepted the offer (or increased the start date by three years), but separate it out by first offer or second offer. Taking the original start date and dividing the number of days between now and then by 1095 gets me the total number of offers, so I've gotten that far. What I really want it to do is compare each policy number to the one before it to see if it's the same (it's already ordered by policy number), then count the date change in a new "accepted" column and count the times it didn't change but could have as "declined".
Is this a case where I would need to self-join the table to itself to compare the dates? Or is there an easier way?

are you looking for this :-
Set Nocount On;
Declare #Test Table
(
PolicyNo Int
,StartDate Date
)
Declare #PolicyWithInc Table
(
RowId Int Identity(1,1) Primary Key
,PolicyNo Int
,StartDate Date
)
Insert Into #Test(PolicyNo,StartDate) Values
(1,'2/15/2006')
,(1,'2/15/2009')
,(1,'2/15/2012')
,(2,'3/15/2006')
,(3,'3/19/2006')
,(3,'3/19/2012')
,(4,'3/31/2006')
,(4,'3/31/2009')
Insert Into #PolicyWithInc(PolicyNo,StartDate)
Select t.PolicyNo
,t.StartDate
From #Test As t
Select pw.PolicyNo
,Sum(Case When Datediff(Year,t.StartDate, pw.StartDate) = 3 Then 1 Else 0 End) As DateArrived
,Sum(Case When Datediff(Year,t.StartDate, pw.StartDate) > 3 Then 1 Else 0 End) As DateNotArrived
,Sum(Case When Isnull(Datediff(Year,t.StartDate,pw.StartDate),0) = 3 Then 1 Else 0 End) As Years3IncrementCount
From #PolicyWithInc As pw
Left Join #PolicyWithInc As t On pw.PolicyNo = t.PolicyNo And pw.RowId = (t.RowId + 1)
Group By pw.PolicyNo

Probably below could help:
Set Nocount On;
Declare #Test Table
(
PolicyNo Int
,StartDate Date
)
Insert Into #Test(PolicyNo,StartDate) Values
(1,'2/15/2006')
,(1,'2/15/2009')
,(1,'2/15/2012')
,(2,'3/15/2006')
,(3,'3/19/2006')
,(3,'3/19/2012')
,(4,'3/31/2006')
,(4,'3/31/2009')
select PolicyNo, StartDate, dateadd(yy, 3, StartDate)Offer1, dateadd(yy, 6, StartDate)Offer2, dateadd(yy, 9, StartDate)Offer3 from
(select * , row_number() over (partition by PolicyNo order by StartDate) rn from #Test)A
where rn = 1
select
count(*) * 3 TotalOffersMade,
count(Data1.StartDate) FirstOfferAccepted,
count(Data2.StartDate) SecondOfferAccepted,
count(Data3.StartDate) ThirdOfferAccepted,
count(*) - count(Data1.StartDate) FirstOfferDeclined,
count(*) - count(Data2.StartDate) SecondOfferDeclined,
count(*) - count(Data3.StartDate) ThirdOfferDeclined
from
(
select PolicyNo, StartDate, dateadd(yy, 3, StartDate)Offer1, dateadd(yy, 6, StartDate)Offer2, dateadd(yy, 9, StartDate)Offer3 from
(select * , row_number() over (partition by PolicyNo order by StartDate) rn from #Test)A
where rn = 1
)Offers
LEFT JOIN
#Test Data1
on Offers.PolicyNo = Data1.PolicyNo and Offers.Offer1 = Data1.StartDate
LEFT JOIN
#Test Data2
on Offers.PolicyNo = Data2.PolicyNo and Offers.Offer2 = Data2.StartDate
LEFT JOIN
#Test Data3
on Offers.PolicyNo = Data3.PolicyNo and Offers.Offer3 = Data3.StartDate

Related

Is it possible to use the SQL DATEADD function but exclude dates from a table in the calculation?

Is it possible to use the DATEADD function but exclude dates from a table?
We already have a table with all dates we need to exclude. Basically, I need to add number of days to a date but exclude dates within a table.
Example: Add 5 days to 01/08/2021. Dates 03/08/2021 and 04/08/2021 exist in the exclusion table. So, resultant date should be: 08/08/2021.
Thank you
A bit of a "wonky" solution, but it works. Firstly we use a tally to create a Calendar table of dates, that exclude your dates in the table, then we get the nth row, where n is the number of days to add:
DECLARE #DaysToAdd int = 5,
#StartDate date = '20210801';
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT 0 AS I
UNION ALL
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1, N N2, N N3), --Up to 1,000
Calendar AS(
SELECT DATEADD(DAY,T.I, #StartDate) AS D,
ROW_NUMBER() OVER (ORDER BY T.I) AS I
FROM Tally T
WHERE NOT EXISTS (SELECT 1
FROM dbo.DatesTable DT
WHERE DT.YourDate = DATEADD(DAY,T.I, #StartDate)))
SELECT D
FROM Calendar
WHERE I = #DaysToAdd+1;
A best solution is probably a calendar table.
But if you're willing to traverse through every date, then a recursive CTE can work. It would require tracking the total iterations and another column to substract if any traversed date was in the table. The exit condition uses the total difference.
An example dataset would be:
CREATE TABLE mytable(mydate date); INSERT INTO mytable VALUES ('20210803'), ('20210804');
And an example function run in it's own batch:
ALTER FUNCTION dbo.fn_getDays (#mydate date, #daysadd int)
RETURNS date
AS
BEGIN
DECLARE #newdate date;
WITH CTE(num, diff, mydate) AS (
SELECT 0 AS [num]
,0 AS [diff]
,DATEADD(DAY, 0, #mydate) [mydate]
UNION ALL
SELECT num + 1 AS [num]
,CTE.diff +
CASE WHEN DATEADD(DAY, num+1, #mydate) IN (SELECT mydate FROM mytable)
THEN 0 ELSE 1 END
AS [diff]
,DATEADD(DAY, num+1, #mydate) [mydate]
FROM CTE
WHERE (CTE.diff +
CASE WHEN DATEADD(DAY, num+1, #mydate) IN (SELECT mydate FROM mytable)
THEN 0 ELSE 1 END) <= #daysadd
)
SELECT #newdate = (SELECT MAX(mydate) AS [mydate] FROM CTE);
RETURN #newdate;
END
Running the function:
SELECT dbo.fn_getDays('20210801', 5)
Produces output, which is the MAX(mydate) from the function:
----------
2021-08-08
For reference the MAX(mydate) is taken from this dataset:
n diff mydate
----------- ----------- ----------
0 0 2021-08-01
1 1 2021-08-02
2 1 2021-08-03
3 1 2021-08-04
4 2 2021-08-05
5 3 2021-08-06
6 4 2021-08-07
7 5 2021-08-08
You can use the IN clause.
To perform the test, I used a W3Schools Test DB
SELECT DATE_ADD(BirthDate, INTERVAL 10 DAY) FROM Employees WHERE FirstName NOT IN (Select FirstName FROM Employees WHERE FirstName LIKE 'N%')
This query shows all the birth dates + 10 days except for the only employee with name starting with N (Nancy)

GROUP BY DAY, CUMULATIVE SUM

I have a table in MSSQL with the following structure:
PersonId
StartDate
EndDate
I need to be able to show the number of distinct people in the table within a date range or at a given date.
As an example i need to show on a daily basis the totals per day, e.g. if we have 2 entries on the 1st June, 3 on the 2nd June and 1 on the 3rd June the system should show the following result:
1st June: 2
2nd June: 5
3rd June: 6
If however e.g. on of the entries on the 2nd June also has an end date that is 2nd June then the 3rd June result would show just 5.
Would someone be able to assist with this.
Thanks
UPDATE
This is what i have so far which seems to work. Is there a better solution though as my solution only gets me employed figures. I also need unemployed on another column - unemployed would mean either no entry in the table or date not between and no other entry as employed.
CREATE TABLE #Temp(CountTotal int NOT NULL, CountDate datetime NOT NULL);
DECLARE #StartDT DATETIME
SET #StartDT = '2015-01-01 00:00:00'
WHILE #StartDT < '2015-08-31 00:00:00'
BEGIN
INSERT INTO #Temp(CountTotal, CountDate)
SELECT COUNT(DISTINCT PERSON.Id) AS CountTotal, #StartDT AS CountDate FROM PERSON
INNER JOIN DATA_INPUT_CHANGE_LOG ON PERSON.DataInputTypeId = DATA_INPUT_CHANGE_LOG.DataInputTypeId AND PERSON.Id = DATA_INPUT_CHANGE_LOG.DataItemId
LEFT OUTER JOIN PERSON_EMPLOYMENT ON PERSON.Id = PERSON_EMPLOYMENT.PersonId
WHERE PERSON.Id > 0 AND DATA_INPUT_CHANGE_LOG.Hidden = '0' AND DATA_INPUT_CHANGE_LOG.Approved = '1'
AND ((PERSON_EMPLOYMENT.StartDate <= DATEADD(MONTH,1,#StartDT) AND PERSON_EMPLOYMENT.EndDate IS NULL)
OR (#StartDT BETWEEN PERSON_EMPLOYMENT.StartDate AND PERSON_EMPLOYMENT.EndDate) AND PERSON_EMPLOYMENT.EndDate IS NOT NULL)
SET #StartDT = DATEADD(MONTH,1,#StartDT)
END
select * from #Temp
drop TABLE #Temp
You can use the following query. The cte part is to generate a set of serial dates between the start date and end date.
DECLARE #ViewStartDate DATETIME
DECLARE #ViewEndDate DATETIME
SET #ViewStartDate = '2015-01-01 00:00:00.000';
SET #ViewEndDate = '2015-02-25 00:00:00.000';
;WITH Dates([Date])
AS
(
SELECT #ViewStartDate
UNION ALL
SELECT DATEADD(DAY, 1,Date)
FROM Dates
WHERE DATEADD(DAY, 1,Date) <= #ViewEndDate
)
SELECT [Date], COUNT(*)
FROM Dates
LEFT JOIN PersonData ON Dates.Date >= PersonData.StartDate
AND Dates.Date <= PersonData.EndDate
GROUP By [Date]
Replace the PersonData with your table name
If startdate and enddate columns can be null, then you need to add
addditional conditions to the join
It assumes one person has only one record in the same date range
You could do this by creating data where every start date is a +1 event and end date is -1 and then calculate a running total on top of that.
For example if your data is something like this
PersonId StartDate EndDate
1 20150101 20150201
2 20150102 20150115
3 20150101
You first create a data set that looks like this:
EventDate ChangeValue
20150101 +2
20150102 +1
20150115 -1
20150201 -1
And if you use running total, you'll get this:
EventDate Total
2015-01-01 2
2015-01-02 3
2015-01-15 2
2015-02-01 1
You can get it with something like this:
select
p.eventdate,
sum(p.changevalue) over (order by p.eventdate asc) as total
from
(
select startdate as eventdate, sum(1) as changevalue from personnel group by startdate
union all
select enddate, sum(-1) from personnel where enddate is not null group by enddate
) p
order by p.eventdate asc
Having window function with sum() requires SQL Server 2012. If you're using older version, you can check other options for running totals.
My example in SQL Fiddle
If you have dates that don't have any events and you need to show those too, then the best option is probably to create a separate table of dates for the whole range you'll ever need, for example 1.1.2000 - 31.12.2099.
-- Edit --
To get count for a specific day, it's possible use the same logic, but just sum everything up to that day:
declare #eventdate date
set #eventdate = '20150117'
select
sum(p.changevalue)
from
(
select startdate as eventdate, 1 as changevalue from personnel
where startdate <= #eventdate
union all
select enddate, -1 from personnel
where enddate < #eventdate
) p
Hopefully this is ok, can't test since SQL Fiddle seems to be unavailable.

About sql server2008 running total

I have a table , the table structure is:
TransactionRecordID State TransactionMoney CreateTime
1 1 200 2014/6/19
2 0 100 2014/6/12
3 1 50 2014/3/17
4 1 400 2014/2/23
......
I want to get the result:
TransactionRecordID State TransactionMoney CreateTime TotalMoney
1 1 200 2014/6/19 650
2 0 100 2014/6/12 450
3 1 50 2014/3/17 450
4 1 400 2014/2/23 400
If current record field state=1,I would like to get sum TransactionMoney after current
record add current TransactionMoney , else get sum TransactionMoney after current record
Sorry for my english!
SQL Server 2012 makes this a lot simpler using SUM() OVER (ORDER BY ...), but in SQL Server 2008 you can use a LEFT JOIN for the same effect;
SELECT a.TransactionRecordID, a.State, a.TransactionMoney, a.CreateTime,
COALESCE(SUM(b.TransactionMoney), 0) TotalMoney
FROM transactions a
LEFT JOIN transactions b ON a.CreateTime >= b.CreateTime AND b.state = 1
GROUP BY a.TransactionRecordID, a.State, a.TransactionMoney, a.CreateTime
ORDER BY a.TransactionRecordID;
An SQLfiddle to test with.
Here is a CTE Example for Northwind. it might give you some alternate ideas.
Use Northwind
GO
declare #CustomerID varchar(6)
declare #BeginDate datetime
declare #EndDate datetime
select #CustomerID = (select top 1 CustomerID from dbo.Orders )
select #BeginDate = '01/01/1900'
select #EndDate = '12/31/2010'
;
WITH
MyCTE /* http://technet.microsoft.com/en-us/library/ms175972.aspx */
( ShipName,ShipAddress,ShipCity,ShipRegion,ShipPostalCode,ShipCountry,CustomerID,CustomerName,[Address],
City,Region,PostalCode,Country,Salesperson,OrderID,OrderDate,RequiredDate,ShippedDate,ShipperName,
ProductID,ProductName,UnitPrice,Quantity,Discount,ExtendedPrice,Freight,ROWID) AS
(
SELECT
ShipName ,ShipAddress,ShipCity,ShipRegion,ShipPostalCode,ShipCountry,CustomerID,CustomerName,[Address]
,City ,Region,PostalCode,Country,Salesperson,OrderID,OrderDate,RequiredDate,ShippedDate,ShipperName
,ProductID ,ProductName,UnitPrice,Quantity,Discount,ExtendedPrice,Freight
, ROW_NUMBER() OVER ( ORDER BY OrderDate , ProductName ASC ) as ROWID
FROM
dbo.Invoices inv /* “Invoices” is a VIEW, FYI */
where
inv.CustomerID = #CustomerID and (inv.OrderDate between #BeginDate and #EndDate)
)
SELECT
/*
ShipName,ShipAddress,ShipCity,ShipRegion,ShipPostalCode,ShipCountry,CustomerID,CustomerName,[Address],
City,Region,PostalCode,Country,Salesperson,OrderID,OrderDate,RequiredDate,ShippedDate,ShipperName,
ProductID,ProductName,UnitPrice,Quantity,Discount,ExtendedPrice,Freight,
*/
/*trim the list down a little for the final output */
CustomerID ,Salesperson,OrderID,OrderDate,ProductName,UnitPrice,Quantity,Discount,ExtendedPrice,Freight,(ExtendedPrice + Freight) as ComputedTotal
/*The below line is the “trick”. I reference the above CTE, but only get data that is less than or equal to the row that I am on (outerAlias.ROWID)*/
, (Select SUM (ExtendedPrice + Freight) from MyCTE innerAlias where innerAlias.ROWID <= outerAlias.ROWID ) as RunningTotal
, ROWID as ROWID_SHOWN_FOR_KICKS , OrderDate as OrderDateASecondTimeForConvenience
FROM
MyCTE outerAlias
/*Two Order By Options*/
ORDER BY outerAlias.OrderDate , ProductName
/* << Whatever the ORDER BY is here, should match the “ROW_NUMBER() OVER ( ORDER BY ________ ASC )” statement inside the CTE */
/*ORDER BY outerAlias.ROWID */ /* << Or, to keep is more “trim”, ORDER BY the ROWID, which will of course be the same as the “ROW_NUMBER() OVER ( ORDER BY” inside the CTE */

Set based solution for processing rows in a SQL table

Can someone steer me in the right direction for solving this issue with a set-based solution versus cursor-based?
Given a table with the following rows:
Date Value
2013-11-01 12
2013-11-12 15
2013-11-21 13
2013-12-01 0
I need a query that will give me a row for each date between 2013-11-1 and 2013-12-1, as follows:
2013-11-01 12
2013-11-02 12
2013-11-03 12
...
2013-11-12 15
2013-11-13 15
2013-11-14 15
...
2013-11-21 13
2013-11-21 13
...
2013-11-30 13
2013-11-31 13
Any advice and/or direction will be appreciated.
The first thing that came to my mind was to fill in the missing dates by looking at the day of the year. You can do this by joining to the spt_values table in the master DB and adding the number to the first day of the year.
DECLARE #Table AS TABLE(ADate Date, ANumber Int);
INSERT INTO #Table
VALUES
('2013-11-01',12),
('2013-11-12',15),
('2013-11-21',13),
('2013-12-01',0);
SELECT
DateAdd(D, v.number, MinDate) Date
FROM (SELECT number FROM master.dbo.spt_values WHERE name IS NULL) v
INNER JOIN (
SELECT
Min(ADate) MinDate
,DateDiff(D, Min(ADate), Max(ADate)) DaysInSpan
,Year(Min(ADate)) StartYear
FROM #Table
) dates ON v.number BETWEEN 0 AND DaysInSpan - 1
Next I would wrap that to make a derived table, and add a subquery to get the most recent number. Your end result may look something like:
DECLARE #Table AS TABLE(ADate Date, ANumber Int);
INSERT INTO #Table
VALUES
('2013-11-01',12),
('2013-11-12',15),
('2013-11-21',13),
('2013-12-01',0);
-- Uncomment the following line to see how it behaves when the date range spans a year end
--UPDATE #Table SET ADate = DateAdd(d, 45, ADate)
SELECT
AllDates.Date
,(SELECT TOP 1 ANumber FROM #Table t WHERE t.ADate <= AllDates.Date ORDER BY ADate DESC)
FROM (
SELECT
DateAdd(D, v.number, MinDate) Date
FROM
(SELECT number FROM master.dbo.spt_values WHERE name IS NULL) v
INNER JOIN (
SELECT
Min(ADate) MinDate
,DateDiff(D, Min(ADate), Max(ADate)) DaysInSpan
,Year(Min(ADate)) StartYear
FROM #Table
) dates ON v.number BETWEEN 0 AND DaysInSpan - 1
) AllDates
Another solution, not sure how it compares to the two already posted performance wise but it's a bit more concise:
Uses a numbers table:
Linky
Query:
DECLARE #SDATE DATETIME
DECLARE #EDATE DATETIME
DECLARE #DAYS INT
SET #SDATE = '2013-11-01'
SET #EDATE = '2013-11-29'
SET #DAYS = DATEDIFF(DAY,#SDATE, #EDATE)
SELECT Num, DATEADD(DAY,N.Num,#SDATE), SUB.[Value]
FROM Numbers N
LEFT JOIN MyTable M ON DATEADD(DAY,N.Num,#SDATE) = M.[Date]
CROSS APPLY (SELECT TOP 1 [Value]
FROM MyTable M2
WHERE [Date] <= DATEADD(DAY,N.Num,#SDATE)
ORDER BY [Date] DESC) SUB
WHERE N.Num <= #DAYS
--
SQL Fiddle
It's possible, but neither pretty nor very performant at scale:
In addition to your_table, you'll need to create a second table/view dates containing every date you'd ever like to appear in the output of this query. For your example it would need to contain at least 2013-11-01 through 2013-12-01.
SELECT m.date, y.value
FROM your_table y
INNER JOIN (
SELECT md.date, MAX(my.date) AS max_date
FROM dates md
INNER JOIN your_table my ON md.date >= my.date
GROUP BY md.date
) m
ON y.date = m.max_date

MSSQL - Create records for entries that don't exist in the last 7 days

I have an ASP.NET website with a C# back-end using MSSQL SQL Server 2008 for its content.
I have written the following stored procedure which checks for any records within the last 7 days and then returns what it finds.
ALTER PROCEDURE [dbuser].[GetResponses]
(
#QUEST_ID int
)
AS
SELECT DateAdded, SUM(Responses) AS responseCount
FROM ActiveResponses
WHERE #QUEST_ID = QuestionnaireID AND DateAdded >= dateadd(day,datediff(day,0,GetDate())- 6,0)
GROUP BY DateAdded
RETURN
My problem here is that if no record exists for any of those last 7 days then my method over on the website back-end side will fail as it required 7 records. For example:
Lets say I have the following records in my table
-DateAdded--------Responses
2012-02-12 4
2012-02-11 5
2012-02-10 8
2012-02-08 7
2012-02-07 3
Notice that there are no records for both 2012-02-13(today) and 2012-02-09
How can I create an SQL statement that checks the last 7 days for the number of responses and if no records are found for any one of those days it creates a record with a response of 0 in the correct position?
This is a good application of a numbers table (ex: http://www.projectdmx.com/tsql/tblnumbers.aspx)
Assuming you have a numbers table dbo.Nums that has at least 6 numbers in it, you can try the following:
CREATE TABLE #Dates
(
[Date] DATETIME
)
INSERT INTO #Dates
(
[Date]
)
SELECT
DATEADD(DD, DATEDIFF(DD, 0, GETDATE()) - ([n] - 1), 0)
FROM
[dbo].[Nums] WITH (NOLOCK)
WHERE
[n] < 7
SELECT
[Date],
ISNULL(SUM([Responses]), 0) AS [responseCount]
FROM
#Dates AS d
LEFT OUTER JOIN
ActiveResponses AS a
ON
a.[DateAdded] = d.[Date]
WHERE
#QUEST_ID = QuestionnaireID
ORDER BY
[Date] ASC
This demonstrates getting summary data for each day in a week, even if some of the days have no data:
declare #Data as table ( DateAdded date, Responses int )
insert into #Data ( DateAdded, Responses ) values ( '2/10/2012', 5 ), ( '2/13/2012', 9 )
; with James as (
select cast( SysDateTime() as date ) as StartOfDay, 7 as DaysLeft
union all
select DateAdd( d, -1, StartOfDay ), DaysLeft - 1
from James
where DaysLeft > 1
)
select J.StartOfDay, DateAdd( ms, -3, cast( DateAdd( day, 1, J.StartOfDay ) as DateTime ) ) as EndOfDay, Coalesce( D.Responses, 0 ) as Responses
from James as J left outer join
#Data as D on D.DateAdded = J.StartOfDay
order by J.StartOfDay desc
Left as an exercise is mating this with your questionnaire data.
Note that the time closest to midnight represented by DateTime values is 3ms before midnight. You can use the StartOfDay and EndOfDay values to drop any DateAdded into the correct date.
Declare a table variable with the last seven dates and include it with your query:
ALTER PROCEDURE [dbuser].[GetResponses]
(
#QUEST_ID int
)
AS
DECLARE #i INT=0;
DECLARE #today DATE=getdate();
DECLARE #last7 TABLE(DateAdded DATE);
WHILE #i>-7 BEGIN
INSERT INTO #last7 VALUES (DATEADD(DAY,#i,#today));
SET #i -= 1;
END
;WITH a AS (
SELECT ar.DateAdded, count(ar.Responses) as responseCount
FROM ActiveResponses ar
INNER JOIN #last7 z ON z.DateAdded=ar.DateAdded
WHERE #QUEST_ID = ar.QuestionnaireID
GROUP BY ar.DateAdded
)
SELECT DateAdded=ISNULL(a.DateAdded,z.DateAdded)
, responseCount=ISNULL(a.responseCount,0)
FROM #last7 z
LEFT JOIN a ON a.DateAdded=z.DateAdded;
RETURN;
GO
Results:
DateAdded responseCount
---------- -------------
2012-02-13 0
2012-02-12 4
2012-02-11 5
2012-02-10 8
2012-02-09 0
2012-02-08 7
2012-02-07 3

Resources