I have a table that has account number, end of month valid from and end of month valid to columns.
What I need is a table that has account number and a column that has all the end of month dates of when the account was live, inclusive of end of month valid to. The Current Table looks like this
New table will need to be like this
I have tried using a calendar table and an CTE table type query but have had no success.
Any help would be great.
This can be achieved using Using multiple comma separated CTEs in a statement
Query
with t0 (i) AS (select 0 union all select 0 union all select 0 union all select 0 union all select 0 union all select 0),
t1 (i) AS (select 0 from t0 a inner join t0 b on a.i = b.i),
n (i) AS (select row_number()over(order by i) from t1),
Account_details (Account_number,valid_from,valid_to,mth,Live_date)As(
select Account_number,valid_from,valid_to, datediff(month,valid_from,valid_to ) mth, valid_from"Live_date"
from tbl1
union all
select Account_number,valid_from,valid_to, datediff(month,valid_from,valid_to ) mth, EOMONTH (dateadd(month,n.i,valid_from)) "Live_date"
from tbl1
inner join n on 1=1 and n.i between 1 and datediff(month,valid_from,valid_to )
)
select *
from Account_details
where Account_details.Account_number =1
order by Account_details.Account_number
Output
CTE Table t0, t1 and n will generate numbers. This is a best way to generate rows without any data.
Then the CTE table Account_details is used to pull data from the table.
Based on sql on the msdn thread how to get month end date between two dates.
DECLARE #Old AS Table (AccountNumber INT, ValidFrom DATE, ValidTo DATE)
DECLARE #New AS Table (AccountNumber INT, LiveDate DATE)
INSERT INTO #old
SELECT 1, '20130630', '20131130' UNION ALL
SELECT 2, '20130630', '20131231' UNION ALL
SELECT 3, '20120430', '20120531' UNION ALL
SELECT 4, '20170331', '20171130'
SELECT TOP 100 * FROM #old
DECLARE #AccountNumber INT, #ValidFrom DATE, #ValidTo DATE
DECLARE #Cursor CURSOR
SET #Cursor = CURSOR FOR
SELECT AccountNumber, ValidFrom, ValidTo
FROM #old
OPEN #Cursor
FETCH NEXT INTO #Cursor FROM #AccountNumber, #ValidFrom, #ValidTo
WHILE ##FETCH_STATUS = 0
BEGIN
;WITH cteEndMonthDates (MonthEndDate)
AS
(
SELECT eomonth(#ValidFrom) AS MonthEndDate
UNION ALL
SELECT eomonth( dateadd(day, 1, MonthEndDate)) AS MonthEndDate
FROM cteEndMonthDates
WHERE MonthEndDate < eomonth(#ValidTo)
)
INSERT INTO #new (AccountNumber, LiveDate)
SELECT #AccountNumber, MonthEndDate
FROM cteEndMonthDates
FETCH NEXT FROM #Cursor INTO #AccountNumber, #ValidFrom, #ValidTo
END
CLOSE #Cursor
DEALLOCATE #Cursor
SELECT * FROM #New
Edit: Or without the cursor
DECLARE #Old AS Table (AccountNumber INT, ValidFrom DATE, ValidTo DATE)
DECLARE #New AS Table (AccountNumber INT, LiveDate DATE)
INSERT INTO #old
SELECT 1, '20130630', '20131130' UNION ALL
SELECT 2, '20130630', '20131231' UNION ALL
SELECT 3, '20120430', '20120531' UNION ALL
SELECT 4, '20170331', '20171130' UNION ALL
SELECT 5, '20180430', '20190131' UNION ALL
SELECT 6, '20160430', '20180531'
SELECT TOP 100 * FROM #old
;WITH cteEndMonthDates (AccountNumber, MonthEndDate)
AS
(
SELECT AccountNumber, eomonth(ValidFrom) AS MonthEndDate
FROM #Old
UNION ALL
SELECT x.AccountNumber, eomonth( dateadd(day, 1, MonthEndDate)) AS MonthEndDate
FROM cteEndMonthDates x
JOIN #Old o ON o.AccountNumber = x.AccountNumber
WHERE MonthEndDate < eomonth(ValidTo)
)
SELECT AccountNumber, MonthEndDate
FROM cteEndMonthDates
order by AccountNumber, MonthEndDate
This should work.
;WITH Span AS (
SELECT
AccountNumber,
ValidFrom AS Valid
FROM dbo.Input
UNION ALL
SELECT
AccountNumber,
DATEADD(DAY, 1, Span.Valid) AS Valid
FROM Span
WHERE DATEADD(DAY, 1, Span.Valid) <= (SELECT ValidTo FROM dbo.Input WHERE AccountNumber = Span.AccountNumber)
)
SELECT * FROM Span
ORDER BY Span.AccountNumber, Span.Valid
OPTION (MAXRECURSION 0);
I need to split date ranges that overlap. I have a primary table (I've called it Employment for this example), and I need to return all Begin-End date ranges for a person from this table. I also have multiple sub tables (represented by Car and Food), and I want to return the value that was active in the sub tables during the times given in the main tables. This will involve splitting the main table date ranges when a sub table item changes.
I don't want to return sub table information for dates not in the main tables.
DECLARE #Employment TABLE
( Person_ID INT, Employment VARCHAR(50), Begin_Date DATE, End_Date DATE )
DECLARE #Car TABLE
( Person_ID INT, Car VARCHAR(50), Begin_Date DATE, End_Date DATE )
DECLARE #Food TABLE
( Person_ID INT, Food VARCHAR(50), Begin_Date DATE, End_Date DATE )
INSERT INTO #Employment ( [Person_ID], [Employment], [Begin_Date], [End_Date] )
VALUES ( 123 , 'ACME' , '1986-01-01' , '1990-12-31' )
, ( 123 , 'Office Corp' , '1995-05-15' , '1998-10-03' )
, ( 123 , 'Job 3' , '1998-10-04' , '2999-12-31' )
INSERT INTO #Car ( [Person_ID] , [Car] , [Begin_Date] , [End_Date] )
VALUES ( 123, 'Red Car', '1986-05-01', '1997-06-23' )
, ( 123, 'Blue Car', '1997-07-03', '2999-12-31' )
INSERT INTO #Food ( [Person_ID], [Food], [Begin_Date], [End_Date] )
VALUES ( 123, 'Eggs', '1997-01-01', '1997-03-09' )
, ( 123, 'Donuts', '2001-02-23', '2001-02-25' )
For the above data, the results should be:
Person_ID Employment Food Car Begin_Date End_Date
123 ACME 1986-01-01 1986-04-30
123 ACME Red Car 1986-05-01 1990-12-31
123 Office Corp Red Car 1995-05-15 1996-12-31
123 Office Corp Eggs Red Car 1997-01-01 1997-03-09
123 Office Corp Red Car 1997-03-10 1997-06-23
123 Office Corp 1997-06-24 1997-07-02
123 Office Corp Blue Car 1997-07-03 1998-10-03
123 Job 3 Blue Car 1998-10-04 2001-02-22
123 Job 3 Donuts Blue Car 2001-02-23 2001-02-25
123 Job 3 Blue Car 2001-02-26 2999-12-31
The first row is his time working for ACME, where he didn't have a car or a weird food obsession. In the second row, he purchased a car, and still worked at ACME. In the third row, he changed jobs to Office Corp, but still has the Red Car. Note how we're not returning data during his unemployment gap, even though he had the Red Car. We only want to know what was in the Car and Food tables during the times there are values in the Employment table.
I found a solution for SQL Server 2012 that uses the LEAD/LAG functions to accomplish this, but I'm stuck with 2008 R2.
To change the 2012 solution from that blog to work with 2008, you need to replace the LEAD in the following
with
ValidDates as …
,
ValidDateRanges1 as
(
select EmployeeNo, Date as ValidFrom, lead(Date,1) over (partition by EmployeeNo order by Date) ValidTo
from ValidDates
)
There are a number of ways to do this, but one example is a self join to the same table + 1 row (which is effectively what a lead does). One way to do this is to put a rownumber on the previous table (so it is easy to find the next row) by adding another intermediate CTE (eg ValidDatesWithRowno). Then do a left outer join to that table where EmployeeNo is the same and rowno = rowno + 1, and use that value to replace the lead. If you wanted a lead 2, you would join to rowno + 2, etc. So the 2008 version would look something like
with
ValidDates as …
,
ValidDatesWithRowno as --This is the ValidDates + a RowNo for easy self joining below
(
select EmployeeNo, Date, ROW_NUMBER() OVER (ORDER BY EmployeeNo, Date) as RowNo from ValidDates
)
,
ValidDateRanges1 as
(
select VD.EmployeeNo, VD.Date as ValidFrom, VDLead1.Date as ValidTo
from ValidDatesWithRowno VD
left outer join ValidDatesWithRowno VDLead1 on VDLead1.EmployeeNo = VD.EmployeeNo
and VDLead1.RowNo = VD.RowNo + 1
)
The rest of the solution described looks like it will work like you want on 2008.
Here is the answer I came up with. It works, but it's not very pretty.
It goes it two waves, first splitting any overlapping Employment/Car dates, then running the same SQL a second time add the Food dates and split any overlaps again.
DECLARE #Employment TABLE
( Person_ID INT, Employment VARCHAR(50), Begin_Date DATE, End_Date DATE )
DECLARE #Car TABLE
( Person_ID INT, Car VARCHAR(50), Begin_Date DATE, End_Date DATE )
DECLARE #Food TABLE
( Person_ID INT, Food VARCHAR(50), Begin_Date DATE, End_Date DATE )
INSERT INTO #Employment ( [Person_ID], [Employment], [Begin_Date], [End_Date] )
VALUES ( 123 , 'ACME' , '1986-01-01' , '1990-12-31' )
, ( 123 , 'Office Corp' , '1995-05-15' , '1998-10-03' )
, ( 123 , 'Job 3' , '1998-10-04' , '2999-12-31' )
INSERT INTO #Car ( [Person_ID] , [Car] , [Begin_Date] , [End_Date] )
VALUES ( 123, 'Red Car', '1986-05-01', '1997-06-23' )
, ( 123, 'Blue Car', '1997-07-03', '2999-12-31' )
INSERT INTO #Food ( [Person_ID], [Food], [Begin_Date], [End_Date] )
VALUES ( 123, 'Eggs', '1997-01-01', '1997-03-09' )
, ( 123, 'Donuts', '2001-02-23', '2001-02-25' )
DECLARE #Person_ID INT = 123;
--A table to hold date ranges that need to be merged together
DECLARE #DatesToMerge TABLE
(
ID INT,
Person_ID INT,
Date_Type VARCHAR(10),
Begin_Date DATETIME,
End_Date DATETIME
)
INSERT INTO #DatesToMerge
SELECT ROW_NUMBER() OVER(ORDER BY [Car])
, Person_ID
, 'Car'
, Begin_Date
, End_Date
FROM #Car
WHERE Person_ID = #Person_ID
INSERT INTO #DatesToMerge
SELECT ROW_NUMBER() OVER(ORDER BY [Employment])
, Person_ID
, 'Employment'
, Begin_Date
, End_Date
FROM #Employment
WHERE Person_ID = #Person_ID;
--A table to hold the merged #Employment and Car records
DECLARE #EmploymentAndCar TABLE
(
RowNumber INT,
Person_ID INT,
Begin_Date DATETIME,
End_Date DATETIME
)
;
WITH CarCTE AS
(--This CTE grabs just the Car rows so we can compare and split dates from them
SELECT ID,
Person_ID,
Date_Type,
Begin_Date,
End_Date
FROM #DatesToMerge
WHERE Date_Type = 'Car'
),
NewRowsCTE AS
( --This CTE creates just new rows starting after the Car dates for each #Employment date range
SELECT a.ID,
a.Person_ID,
a.Date_Type,
DATEADD(DAY,1,b.End_Date) AS Begin_Date,
a.End_Date
FROM #DatesToMerge a
INNER JOIN CarCTE b
ON a.Begin_Date <= b.Begin_Date
AND a.End_Date > b.Begin_Date
AND a.End_Date > b.End_Date -- This is needed because if both the Car and #Employment end on the same date, there is split row after
),
UnionCTE AS
( -- This CTE merges the new rows with the existing ones
SELECT ID,
Person_ID,
Date_Type,
Begin_Date,
End_Date
FROM #DatesToMerge
UNION ALL
SELECT ID,
Person_ID,
Date_Type,
Begin_Date,
End_Date
FROM NewRowsCTE
),
FixEndDateCTE AS
(
SELECT CONVERT (CHAR,c.ID)+CONVERT (CHAR,c.Begin_Date) AS FixID,
MIN(d.Begin_Date) AS Begin_Date
FROM UnionCTE c
LEFT OUTER JOIN CarCTE d
ON c.Begin_Date < d.Begin_Date
AND c.End_Date >= d.Begin_Date
WHERE c.Date_Type <> 'Car'
GROUP BY CONVERT (CHAR,c.ID)+CONVERT (CHAR,c.Begin_Date)
),
Finalize AS
(
SELECT ROW_NUMBER() OVER (ORDER BY e.Begin_Date) AS RowNumber,
e.Person_ID,
e.Begin_Date,
CASE WHEN f.Begin_Date IS NULL THEN e.End_Date
ELSE DATEADD (DAY,-1,f.Begin_Date)
END AS EndDate
FROM UnionCTE e
LEFT OUTER JOIN FixEndDateCTE f
ON (CONVERT (CHAR,e.ID)+CONVERT (CHAR,e.Begin_Date)) = f.FixID
)
INSERT INTO #EmploymentAndCar ( RowNumber, Person_ID, Begin_Date, End_Date )
SELECT F.RowNumber
, F.Person_ID
, F.Begin_Date
, F.EndDate
FROM Finalize F
INNER JOIN #Employment Employment
ON F.Begin_Date BETWEEN Employment.Begin_Date AND Employment.End_Date AND Employment.Person_ID = #Person_ID
ORDER BY F.Begin_Date
--------------------------------------------------------------------------------------------------
--Now that the Employment and Car dates have been merged, empty the DatesToMerge table
DELETE FROM #DatesToMerge;
--Reload the DatesToMerge table with the newly-merged Employment and Car records,
--and the Food records that still need to be merged
INSERT INTO #DatesToMerge
SELECT RowNumber
, Person_ID
, 'PtBCar'
, Begin_Date
, End_Date
FROM #EmploymentAndCar
WHERE Person_ID = #Person_ID
INSERT INTO #DatesToMerge
SELECT ROW_NUMBER() OVER(ORDER BY [Food])
, Person_ID
, 'Food'
, Begin_Date
, End_Date
FROM #Food
WHERE Person_ID = #Person_ID
;
WITH CarCTE AS
(--This CTE grabs just the Food rows so we can compare and split dates from them
SELECT ID,
Person_ID,
Date_Type,
Begin_Date,
End_Date
FROM #DatesToMerge
WHERE Date_Type = 'Food'
),
NewRowsCTE AS
( --This CTE creates just new rows starting after the Food dates for each Employment date range
SELECT a.ID,
a.Person_ID,
a.Date_Type,
DATEADD(DAY,1,b.End_Date) AS Begin_Date,
a.End_Date
FROM #DatesToMerge a
INNER JOIN CarCTE b
ON a.Begin_Date <= b.Begin_Date
AND a.End_Date > b.Begin_Date
AND a.End_Date > b.End_Date -- This is needed because if both the Food and Car/Employment end on the same date, there is split row after
),
UnionCTE AS
( -- This CTE merges the new rows with the existing ones
SELECT ID,
Person_ID,
Date_Type,
Begin_Date,
End_Date
FROM #DatesToMerge
UNION ALL
SELECT ID,
Person_ID,
Date_Type,
Begin_Date,
End_Date
FROM NewRowsCTE
),
FixEndDateCTE AS
(
SELECT CONVERT (CHAR,c.ID)+CONVERT (CHAR,c.Begin_Date) AS FixID,
MIN(d.Begin_Date) AS Begin_Date
FROM UnionCTE c
LEFT OUTER JOIN CarCTE d
ON c.Begin_Date < d.Begin_Date
AND c.End_Date >= d.Begin_Date
WHERE c.Date_Type <> 'Food'
GROUP BY CONVERT (CHAR,c.ID)+CONVERT (CHAR,c.Begin_Date)
),
Finalize AS
(
SELECT ROW_NUMBER() OVER (ORDER BY e.Begin_Date) AS RowNumber,
e.Person_ID,
e.Begin_Date,
CASE WHEN f.Begin_Date IS NULL THEN e.End_Date
ELSE DATEADD (DAY,-1,f.Begin_Date)
END AS EndDate
FROM UnionCTE e
LEFT OUTER JOIN FixEndDateCTE f
ON (CONVERT (CHAR,e.ID)+CONVERT (CHAR,e.Begin_Date)) = f.FixID
)
SELECT DISTINCT
F.Person_ID
, Employment
, Car
, Food
, F.Begin_Date
, F.EndDate
FROM Finalize F
INNER JOIN #Employment Employment
ON F.Begin_Date BETWEEN Employment.Begin_Date AND Employment.End_Date AND Employment.Person_ID = #Person_ID
LEFT JOIN #Car Car
ON Car.[Begin_Date] <= F.Begin_Date
AND Car.[End_Date] >= F.[EndDate]
AND Car.Person_ID = #Person_ID
LEFT JOIN #Food Food
ON Food.[Begin_Date] <= F.[Begin_Date]
AND Food.[End_Date] >= F.[EndDate]
AND Food.Person_ID = #Person_ID
ORDER BY F.Begin_Date
If anyone has a more elegant solution, I will be happy to accept their answer.
I have 2 columns 1) id (int) and 2) Date. I want to select id on max date. Grouping results in returning both id's/more than one id. Instead i only want to retrieve id on max date .
I am sure there is easier way to do this however below should work fine.
-- create sample data
create table #temp(ID int, courseID int, end_date datetime)
go
insert into #temp
select 1 , 11 , getdate()
union
select 1, 12, getdate()-20
union
select 1, 13, getdate()-40
union
select 2, 13, getdate()-70
union
select 2, 14, getdate()-80
-- create temp table to calculate correct date
select id, max(end_date) as correctDate
into #temp2
from #temp
group by id
-- final desired outup
select #temp2.id , #temp.courseID
from #temp2
inner join #temp
on #temp2.id = #temp.id
and #temp2.correctDate = #temp.end_date
-- drop temp tables
drop table #temp
drop table #temp2
give me a shout if you have any questions
Simpler alternative
-- create sample data
create table #temp(id int, courseID int, end_date datetime)
go
insert into #temp
select
1 , 11 , getdate()
union
select
1, 12, getdate()-20
union
select
1, 13, getdate()-40
union
select
2, 13, getdate()-70
union
select
2, 14, getdate()-80
SELECT * FROM(
SELECT DENSE_RANK() OVER(PARTITION BY id ORDER BY end_date DESC ) sira, id,courseID,end_date FROM #temp
) t WHERE sira = 1
-- drop temp tables
drop table #temp
drop table #temp2
I have this table called Table1 as follows:
UserID Date
1 01/01/09
1 14/01/09
1 25/01/09
1 01/02/09
1 15/02/09
2 02/02/09
2 15/02/09
I am trying to return a result that counts the number of times between the MIN(Date) and 30 days after the MIN(Date) which is DATEADD(day,30,MIN(DATE)). So it would look something like this:
UserID Count
1 3
2 2
This code below is wrong but it expresses what I am trying to achieve:
SELECT COUNT(1) AS Count
FROM Table1
GROUP BY UserID
WHERE Date BETWEEN MIN(Date) AND DATEADD(day,30,MIN(DATE))
SELECT a.UserID, COUNT(a.UserID) AS [Count]
FROM Table1 AS a
INNER JOIN
(
SELECT UserID, MIN([Date]) AS MinDate
FROM Table1
GROUP BY UserID
) AS b
ON a.UserID = b.UserID
WHERE [Date] BETWEEN MinDate AND DATEADD(day, 30, MinDate)
GROUP BY a.UserID
Try this
DECLARE #table TABLE(
UserID INT,
DDate DATETIME
)
INSERT INTO #table (UserID,DDate) SELECT 1, '01 Jan 2009'
INSERT INTO #table (UserID,DDate) SELECT 1, '14 Jan 2009'
INSERT INTO #table (UserID,DDate) SELECT 1, '25 Jan 2009'
INSERT INTO #table (UserID,DDate) SELECT 1, '01 Feb 2009'
INSERT INTO #table (UserID,DDate) SELECT 1, '15 Feb 2009'
INSERT INTO #table (UserID,DDate) SELECT 2, '02 Feb 2009'
INSERT INTO #table (UserID,DDate) SELECT 2, '15 Feb 2009'
SELECT t.UserID,
COUNT(t.UserID)
FROM #table t INNER JOIN
(
SELECT UserID,
MinDate,
DATEADD(dd, 30, MinDate) MinDataAdd30
FROM (
SELECT UserID,
MIN(DDate) MinDate
FROM #table
GROUP BY UserID
) MINDates
) DateRange ON t.UserID = DateRange.UserID
WHERE t.DDate BETWEEN DateRange.MinDate AND DateRange.MinDataAdd30
GROUP BY t.UserID
I think you'll need to use a subquery to get the minimum date. I've shown it below
as a separate query into a variable as I'd probably turn this into a table-valued function.
DECLARE #STARTDATE DATETIME
SELECT #STARTDATE = MIN(DATE) FROM Table1
SELECT COUNT(1) AS Count
FROM Table1
GROUP BY UserID
WHERE Date BETWEEN #STARTDATE AND DATEADD(day,30,#STARTDATE)
I would do it like this:
select a.UserID, count(case when DDate - MinDate <= 30 then 1 end) as Count
from (
select UserID, min(DDate) MinDate
from Table1
group by UserID
) a
inner join Table1 t on a.UserID = t.UserID
group by a.UserID