In the table below, how to insert rows with the first and last date of years between the START_DATE and END_DATE column?
EMPID
EMPNAME
START_DATE
END_DATE
1001
Shivansh
2015-09-01
2018-03-31
1004
Mayank
2019-04-01
2020-06-30
The output should look as follows:
EMPID
EMPNAME
START_DATE
END_DATE
1001
Shivansh
2015-09-01
2015-12-31
1001
Shivansh
2016-01-01
2016-12-31
1001
Shivansh
2017-01-01
2017-12-31
1001
Shivansh
2018-01-01
2018-03-31
1004
Mayank
2019-04-01
2019-12-31
1004
Mayank
2020-01-01
2020-06-30
This has to be implemented using loops as Azure Synapse Analytics doesn't support Recursive common table expressions
This approach uses a numbers table and a number of date functions which are available in Azure Synapse Analytics dedicated SQL pools, including DATEFROMPARTS, DATEDIFF and YEAR.
NB This is not a recursive query. There is a loop used in the creation of the numbers table but this is done only once. Once the numbers table exists it can be used for similar scenarios, eg converting recursive CTEs to set-based approaches compatible with Azure Synapse Analytics.
DATEFROMPARTS is used to construct the first day of the year in the calculated records. I then use DATEADD to add one year, then take away one day, to get the last day of the year. DATEDIFF with year is used to determine the gap in years between the two dates and therefore the number of records that need to be added. I then UNION the original and calculated records for the full result.
IF OBJECT_ID('tempdb..#tmp') IS NOT NULL
DROP TABLE #tmp
GO
CREATE TABLE #tmp (
empId INT NOT NULL,
empName VARCHAR(50) NOT NULL,
start_date DATE NOT NULL,
end_date DATE NULL
)
GO
-- Setup test data
INSERT INTO #tmp ( empId, empName, start_date, end_date )
SELECT 1001, 'Shivansh', '2015-09-01', '2018-03-31'
UNION ALL
SELECT 1004, 'Mayank', '2019-04-01', '2020-06-30'
GO
;WITH cte AS (
SELECT *,
DATEFROMPARTS( YEAR(start_date) + n.number, 1, 1 ) newStart
FROM #tmp t
CROSS JOIN dbo.numbers n
WHERE n.number <= DATEDIFF( year, start_date, end_date )
)
SELECT 'o' s, empId, empName, start_date,
CASE
WHEN YEAR(start_date) = YEAR(end_date) THEN end_date
ELSE DATEFROMPARTS( YEAR(start_date), 12, 13 )
END end_date
FROM #tmp
UNION ALL
SELECT 'c', empId, empName,
newStart AS start_date,
CASE
WHEN YEAR(end_date) = YEAR(newStart) THEN end_date
ELSE DATEADD( day, -1, DATEADD( year, 1, newStart ) )
END newEnd
FROM cte
ORDER BY empId, start_date
My results:
I've added the o and c to indicate original and calculated rows but you can remove that column if you like. If you do not have a numbers table already then the script I used to create this one is here. This code has been tested on an Azure Synapse Analytics dedicated SQL pool, version Microsoft Azure SQL Data Warehouse - 10.0.15554.0 Dec 10 2020 03:11:10.
I found the workaround using a loop. The steps followed are as follows:
Create a temporary table to hold initial values.
CREATE TABLE #EMP
(
EMPID VARCHAR(10),
EMPNAME VARCHAR(10),
START_DATE DATE,
END_DATE DATE
);
Insert initial values.
INSERT INTO #EMP
SELECT '1001', 'Shivansh', '2015-09-01', '2018-03-31';
INSERT INTO #EMP
SELECT '1004', 'Mayank', '2019-04-01', '2020-06-30';
Create the required table.
CREATE TABLE #NEWEMP
(
EMPID VARCHAR(10),
EMPNAME VARCHAR(10),
START_DATE DATE,
END_DATE DATE
);
Insert the first year date (i.e. if the START_DATE for a tuple is 2015-09-01 insert 2015-09-01 for START_DATE and 2015-12-31 for the END_DATE).
INSERT INTO #NEWEMP
SELECT EMPID, EMPNAME, START_DATE, DATEFROMPARTS(YEAR(START_DATE), 12, 31) FROM #EMP;
Similarly, Insert the last year date.
INSERT INTO #NEWEMP
SELECT EMPID, EMPNAME, DATEFROMPARTS(YEAR(END_DATE), 1, 1), END_DATE FROM #EMP;
Run a while loop till the maximum value of the difference between START_DATE and END_DATE column.
DECLARE #counter INT = 1;
DECLARE #len INT = (SELECT MAX(DATEDIFF(YEAR, START_DATE, END_DATE)) FROM #EMP);
WHILE #counter < #len
BEGIN
INSERT INTO #NEWEMP
SELECT EMPID, EMPNAME, DATEFROMPARTS(YEAR(START_DATE) + #counter, 1, 1), DATEFROMPARTS(YEAR(START_DATE) + #counter, 12, 31) FROM #EMP
WHERE #counter < DATEDIFF(YEAR, START_DATE, END_DATE);
SET #counter += 1;
END
Query the output.
SELECT * FROM #NEWEMP ORDER BY START_DATE;
Here is the Query, written in Azure Synapse Analytics.
The required output is.
Related
I get provided a list of users and their home zip code every month. However, not every user provides a zip code for every month so my monthly tables are never the same size.
What I want to do is create one master table that has a record for every month for every user starting with the first month. Then if a user in the first month doesn't appear in the second month they should still get a record for the second month with the zip code assigned based on the prior month.
For example, I have two tables that look like this:
UserNumber Month ZIP
1 201701 12345
2 201701 30032
3 201701 01432
Etc.
UserNumber Month ZIP
1 201702 12345
3 201702 01433
4 201702 30032
Etc.
You can see that some ZIP codes will change (user 3 "moved") which is ok. But user 2 doesn't have a record for 201702. But my new master table should have a record for them where the ZIP code from 201701 is used. So the master table should look like this:
UserNumber Month ZIP
1 201701 12345
1 201702 12345
2 201701 30032
2 201702 30032
3 201701 01432
Etc.
As mentioned, there is a record for user 2 for 201702 using the same zip code where we had a record. Sometimes there will be multiple missing months so I want to grab the most recent record that is less than the current month.
I have tried creating multiple temporary tables based on the table intersects and then appending them together and that worked. But with 30+ months of data that was going to get very complicated and tedious so I'm hoping there is a better way. And this master table would have to be updated each month as well.
I would appreciate any suggestions!
Currently the data is in S3 which I access using Hive so a HiveQL solution would be ideal so I don't have to import all this data into SSMS but if it's easier to do this in SSMS using SQL I can make that work as well.
Something like this sounds like it would work, at least once you get the initial set built. This does make the assumption that the process is updated every month so that there can't be gaps in the series:
insert into Master (userid, month, zip)
select
coalesce(u.userid, m.userid),
coalesce(u.month, convert(char(6), dateadd(month, 1, m.month + '01'), 112),
coalesce(u.zip, m.zip)
from ZipUpdate u full outer join Master m
on m.userid = u.userid and m.month =
convert(char(6), dateadd(month, -1, u.month + '01'), 112);
If you want to fill in the gaps you could start with an empty table and just run this 30+ times in a loop. If the table names are predictable then something like this could generate the entire script.
declare #sql varchar(8000);
declare #dt date = '20170101';
while #dt < cast('20190901' as date)
begin
set #sql = 'insert into Master (userid, month, zip)
select
coalesce(u.userid, m.userid),
''' + convert(char(6), #dt, 112) + ''',
coalesce(u.zip, m.zip)
from '
/* change this expression as needed */
+ 'ZipUpdate'
+ convert(varchar(3), datediff(month, '20170101', #dt) + 1)
+ ' u full outer join Master m
on m.userid = u.userid and m.month = ''' +
+ convert(char(6), dateadd(month, -1, #dt), 112) + '''
where u.month = ''' + convert(char(6), #dt, 112) + ''';'
set #dt = dateadd(month, 1, #dt);
select #sql;
end
The following solution is suitable for your question:
begin tran
create table #tbl1 (UserNumber int, [Month] int, ZIP char(5));
create table #tbl2 (UserNumber int, [Month] int, ZIP char(5));
create table #tbl3 (UserNumber int, [Month] int, ZIP char(5));
insert into #tbl1 (UserNumber, [Month], ZIP)
select 1, 201701, '12345' union all
select 2, 201701, '30032' union all
select 3, 201701, '01432';
insert into #tbl2 (UserNumber, [Month], ZIP)
select 1, 201702, '12345' union all
select 3, 201702, '01433' union all
select 4, 201702, '30032';
insert into #tbl3 (UserNumber, [Month], ZIP)
select 3, 201703, '01435' union all
select 4, 201703, '30032';
create table #full (UserNumber int, [Month] int, ZIP char(5));
insert into #full (UserNumber, [Month], ZIP)
select UserNumber, [Month], ZIP from #tbl1
union all
select UserNumber, [Month], ZIP from #tbl2
union all
select UserNumber, [Month], ZIP from #tbl3;
CREATE UNIQUE CLUSTERED INDEX [CI_Full] ON #full (UserNumber asc, [Month] asc);
create table #month ([Month] int);
insert into #month ([Month])
select [Month]
from #full
group by [Month];
CREATE UNIQUE CLUSTERED INDEX [CI_Month] ON #month ([Month] asc);
create table #start_usernumber (UserNumber int, [Month] int);
insert into #start_usernumber (UserNumber, [Month])
select UserNumber, min([Month])
from #full
group by UserNumber;
CREATE UNIQUE CLUSTERED INDEX [CI_StartUserNumber] ON #start_usernumber (UserNumber asc, [Month] asc);
select su.UserNumber,
m.[Month],
case when(f.ZIP is null) then (select top(1) f0.ZIP from #full as f0 where f0.UserNumber=su.UserNumber and f0.[Month]<m.[Month] and f0.ZIP is not null order by f0.[Month] desc) else f.ZIP end as ZIP
from #start_usernumber as su
inner join #month as m on su.[Month]<=m.[Month]
left join #full as f on m.[Month]=f.[Month] and su.UserNumber=f.UserNumber
order by su.UserNumber, m.[Month];
rollback tran
Result:
Given a table with a single row for each day of the month, how can I query it to get the row for the last day of each month?
Try adapting the following query. The SELECT statement within the IN clause choses the dates for the outer query to return.
SELECT *
FROM myTable
WHERE DateColumn IN
(
SELECT MAX(DateColumn)
FROM myTable
GROUP BY YEAR(Datecolumn), MONTH(DateColumn)
)
Try to make use of below query:
DECLARE #Dates Table (ID INT, dt DATE)
INSERT INTO #Dates VALUES
(1,'2017-02-01'),
(2,'2017-02-03'),
(3,'2017-02-04'),
(4,'2017-03-03'),
(5,'2017-04-03'),
(6,'2017-04-04')
SELECT MAX(dt) AS LastDay FROM #Dates GROUP BY DATEPART(MONTH,dt)
OUTPUT
LastDay
2017-02-04
2017-03-03
2017-04-04
OR
SELECT DATEPART(MONTH,dt) AS [MONTH],MAX(DATEPART(DAY,dt)) AS LastDay FROM #Dates GROUP BY DATEPART(MONTH,dt)
MONTH LastDay
2 4
3 3
4 4
You need to select from your table where the YourDateColumn field of the record equals the last date of the month YourDateColumn belongs to:
SELECT CAST(DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0, YourDateColumn )+1,0)) AS DATE)
I am using SQL Server 2016 and I need to create a table (RoomInventory) in my database that will contain the following set of data which will be repeated for the period 01 January 2016 to 31 December 2016.
Here is how I want the final table to appear:
Date RoomType Property Inventory
2016-01-01 SUP JS 20
2016-01-01 DLX JS 15
2016-01-01 FAS FB 6
2016-01-02 SUP JS 20
2016-01-02 DLX JS 15
2016-01-02 FAS FB 6
-------------------------------------------
-------------------------------------------
2016-12-31 SUP JS 20
2016-12-31 DLX JS 15
2016-12-31 FAS FB 6
I know how to create the Table and how to insert data using the INSERT INTO syntax but I have no clue as to how to write the proper syntax that will fill the DATE column with the dates required.
Use a recursive CTE:
;WITH
cte AS
(
SELECT CAST('2016-01-01' AS date) AS [Date]
UNION ALL
SELECT DATEADD(DAY, 1, [Date])
FROM cte
WHERE [Date] < '2016-12-31'
)
SELECT *
FROM cte
CROSS JOIN RoomInventory
OPTION (MAXRECURSION 0)
I like the CTE option more for the Calendar... here's my go at it.
DECLARE #Start DATE, #End DATE
SET #Start = '2016-01-01'
SET #End = '2016-12-31'
IF OBJECT_ID('tempdb..#date') IS NOT NULL
DROP TABLE #date
CREATE TABLE #date (date_id SMALLINT IDENTITY(1,1) PRIMARY KEY,
date_val DATE)
WHILE #Start<=#End
BEGIN
INSERT INTO #date (date_val)
SELECT #Start
SET #Start=DATEADD(DD,1,#Start)
END
IF OBJECT_ID('tempdb..#roomInventory') IS NOT NULL
DROP TABLE #roomInventory
CREATE TABLE #roomInventory (RoomType VARCHAR(5), Property VARCHAR(5), Inventory INT)
INSERT INTO #roomInventory (RoomType, Property,Inventory)
VALUES ('SUP','JS',20),
('DLX','JS',15),
('FAS','FB',6)
SELECT date_val, RoomType, Property, Inventory
FROM #date
CROSS APPLY #roomInventory
ORDER BY date_val, RoomType
Another option is to use your CalendarTable and
Select B.Date,RoomType,Property,Inventory
From RoomDefTable A
Cross Join CalendarTable B
Where B.Date between '2016-01-01' and '2016-12-31'
Order By B.Date
I am trying to get order counts for everyday this year in 3pm to 3pm intervals
Select cast(order_datetime as date) ,count(*)
from order_table
where order_datetime> '01/01/2015 15:00'
group by ?????
I normally group by
cast(order_datetime as date) ---for 12am to 12am
but I want 3pm to 3pm.
This might be solution looking for i have taken a sample data and a table variable #t to solve it col1 in the table refers to Your problem
SET DATEFORMAT DMY
DECLARE #t TABLE
(
Id int,
Name varchar(11),
[Col1] DateTime
)
INSERT INTO #t
VALUES(1,'ABC','22/12/2012 03:45:00 PM'),(2,'SD','22/12/2012 03:01:00 PM'),(3,'SDSA','22/12/2012 02:01:00 PM'),(4,'ASDF','22/12/2012 03:30:00 PM'),(5,'ASWER','22/12/2012 02:30:00 PM')
,(11,'NARI','21/12/2012 03:40:00 PM')
SELECT CASE WHEN CAST(COL1 AS TIME) >'15:00:00' THEN CAST(DATEADD(DAY,1,Col1) AS date) ELSE CAST(Col1 AS DATE) END AS ORDERS,COUNT(*) AS COUNT
FROM #t
GROUP BY CASE WHEN CAST(COL1 AS TIME) >'15:00:00' THEN CAST(DATEADD(DAY,1,Col1) AS date) ELSE CAST(Col1 AS DATE) END
I have a SQL Server 2005 database which contains a table called Memberships.
The table schema is:
PersonID int, Surname nvarchar(30), FirstName nvarchar(30), Description nvarchar(100), StartDate datetime, EndDate datetime
I'm currently working on a grid feature which shows a break-down of memberships by person. One of the requirements is to split membership rows where there is an intersection of date ranges. The intersection must be bound by the Surname and FirstName, ie splits only occur with membership records of the same Surname and FirstName.
Example table data:
18 Smith John Poker Club 01/01/2009 NULL
18 Smith John Library 05/01/2009 18/01/2009
18 Smith John Gym 10/01/2009 28/01/2009
26 Adams Jane Pilates 03/01/2009 16/02/2009
Expected result set:
18 Smith John Poker Club 01/01/2009 04/01/2009
18 Smith John Poker Club / Library 05/01/2009 09/01/2009
18 Smith John Poker Club / Library / Gym 10/01/2009 18/01/2009
18 Smith John Poker Club / Gym 19/01/2009 28/01/2009
18 Smith John Poker Club 29/01/2009 NULL
26 Adams Jane Pilates 03/01/2009 16/02/2009
Does anyone have any idea how I could write a stored procedure that will return a result set which has the break-down described above.
The problem you are going to have with this problem is that as the data set grows, the solutions to solve it with TSQL won't scale well. The below uses a series of temporary tables built on the fly to solve the problem. It splits each date range entry into its respective days using a numbers table. This is where it won't scale, primarily due to your open ranged NULL values which appear to be inifinity, so you have to swap in a fixed date far into the future that limits the range of conversion to a feasible length of time. You could likely see better performance by building a table of days or a calendar table with appropriate indexing for optimized rendering of each day.
Once the ranges are split, the descriptions are merged using XML PATH so that each day in the range series has all of the descriptions listed for it. Row Numbering by PersonID and Date allows for the first and last row of each range to be found using two NOT EXISTS checks to find instances where a previous row doesn't exist for a matching PersonID and Description set, or where the next row doesn't exist for a matching PersonID and Description set.
This result set is then renumbered using ROW_NUMBER so that they can be paired up to build the final results.
/*
SET DATEFORMAT dmy
USE tempdb;
GO
CREATE TABLE Schedule
( PersonID int,
Surname nvarchar(30),
FirstName nvarchar(30),
Description nvarchar(100),
StartDate datetime,
EndDate datetime)
GO
INSERT INTO Schedule VALUES (18, 'Smith', 'John', 'Poker Club', '01/01/2009', NULL)
INSERT INTO Schedule VALUES (18, 'Smith', 'John', 'Library', '05/01/2009', '18/01/2009')
INSERT INTO Schedule VALUES (18, 'Smith', 'John', 'Gym', '10/01/2009', '28/01/2009')
INSERT INTO Schedule VALUES (26, 'Adams', 'Jane', 'Pilates', '03/01/2009', '16/02/2009')
GO
*/
SELECT
PersonID,
Description,
theDate
INTO #SplitRanges
FROM Schedule, (SELECT DATEADD(dd, number, '01/01/2008') AS theDate
FROM master..spt_values
WHERE type = N'P') AS DayTab
WHERE theDate >= StartDate
AND theDate <= isnull(EndDate, '31/12/2012')
SELECT
ROW_NUMBER() OVER (ORDER BY PersonID, theDate) AS rowid,
PersonID,
theDate,
STUFF((
SELECT '/' + Description
FROM #SplitRanges AS s
WHERE s.PersonID = sr.PersonID
AND s.theDate = sr.theDate
FOR XML PATH('')
), 1, 1,'') AS Descriptions
INTO #MergedDescriptions
FROM #SplitRanges AS sr
GROUP BY PersonID, theDate
SELECT
ROW_NUMBER() OVER (ORDER BY PersonID, theDate) AS ID,
*
INTO #InterimResults
FROM
(
SELECT *
FROM #MergedDescriptions AS t1
WHERE NOT EXISTS
(SELECT 1
FROM #MergedDescriptions AS t2
WHERE t1.PersonID = t2.PersonID
AND t1.RowID - 1 = t2.RowID
AND t1.Descriptions = t2.Descriptions)
UNION ALL
SELECT *
FROM #MergedDescriptions AS t1
WHERE NOT EXISTS
(SELECT 1
FROM #MergedDescriptions AS t2
WHERE t1.PersonID = t2.PersonID
AND t1.RowID = t2.RowID - 1
AND t1.Descriptions = t2.Descriptions)
) AS t
SELECT DISTINCT
PersonID,
Surname,
FirstName
INTO #DistinctPerson
FROM Schedule
SELECT
t1.PersonID,
dp.Surname,
dp.FirstName,
t1.Descriptions,
t1.theDate AS StartDate,
CASE
WHEN t2.theDate = '31/12/2012' THEN NULL
ELSE t2.theDate
END AS EndDate
FROM #DistinctPerson AS dp
JOIN #InterimResults AS t1
ON t1.PersonID = dp.PersonID
JOIN #InterimResults AS t2
ON t2.PersonID = t1.PersonID
AND t1.ID + 1 = t2.ID
AND t1.Descriptions = t2.Descriptions
DROP TABLE #SplitRanges
DROP TABLE #MergedDescriptions
DROP TABLE #DistinctPerson
DROP TABLE #InterimResults
/*
DROP TABLE Schedule
*/
The above solution will also handle gaps between additional Descriptions as well, so if you were to add another Description for PersonID 18 leaving a gap:
INSERT INTO Schedule VALUES (18, 'Smith', 'John', 'Gym', '10/02/2009', '28/02/2009')
It will fill the gap appropriately. As pointed out in the comments, you shouldn't have name information in this table, it should be normalized out to a Persons Table that can be JOIN'd to in the final result. I simulated this other table by using a SELECT DISTINCT to build a temp table to create that JOIN.
Try this
SET DATEFORMAT dmy
DECLARE #Membership TABLE(
PersonID int,
Surname nvarchar(16),
FirstName nvarchar(16),
Description nvarchar(16),
StartDate datetime,
EndDate datetime)
INSERT INTO #Membership VALUES (18, 'Smith', 'John', 'Poker Club', '01/01/2009', NULL)
INSERT INTO #Membership VALUES (18, 'Smith', 'John','Library', '05/01/2009', '18/01/2009')
INSERT INTO #Membership VALUES (18, 'Smith', 'John','Gym', '10/01/2009', '28/01/2009')
INSERT INTO #Membership VALUES (26, 'Adams', 'Jane','Pilates', '03/01/2009', '16/02/2009')
--Program Starts
declare #enddate datetime
--Measuring extreme condition when all the enddates are null(i.e. all the memberships for all members are in progress)
-- in such a case taking any arbitary date e.g. '31/12/2009' here else add 1 more day to the highest enddate
select #enddate = case when max(enddate) is null then '31/12/2009' else max(enddate) + 1 end from #Membership
--Fill the null enddates
; with fillNullEndDates_cte as
(
select
row_number() over(partition by PersonId order by PersonId) RowNum
,PersonId
,Surname
,FirstName
,Description
,StartDate
,isnull(EndDate,#enddate) EndDate
from #Membership
)
--Generate a date calender
, generateCalender_cte as
(
select
1 as CalenderRows
,min(startdate) DateValue
from #Membership
union all
select
CalenderRows+1
,DateValue + 1
from generateCalender_cte
where DateValue + 1 <= #enddate
)
--Generate Missing Dates based on Membership
,datesBasedOnMemberships_cte as
(
select
t.RowNum
,t.PersonId
,t.Surname
,t.FirstName
,t.Description
, d.DateValue
,d.CalenderRows
from generateCalender_cte d
join fillNullEndDates_cte t ON d.DateValue between t.startdate and t.enddate
)
--Generate Dscription Based On Membership Dates
, descriptionBasedOnMembershipDates_cte as
(
select
PersonID
,Surname
,FirstName
,stuff((
select '/' + Description
from datesBasedOnMemberships_cte d1
where d1.PersonID = d2.PersonID
and d1.DateValue = d2.DateValue
for xml path('')
), 1, 1,'') as Description
, DateValue
,CalenderRows
from datesBasedOnMemberships_cte d2
group by PersonID, Surname,FirstName,DateValue,CalenderRows
)
--Grouping based on membership dates
,groupByMembershipDates_cte as
(
select d.*,
CalenderRows - row_number() over(partition by Description order by PersonID, DateValue) AS [Group]
from descriptionBasedOnMembershipDates_cte d
)
select PersonId
,Surname
,FirstName
,Description
,convert(varchar(10), convert(datetime, min(DateValue)), 103) as StartDate
,case when max(DateValue)= #enddate then null else convert(varchar(10), convert(datetime, max(DateValue)), 103) end as EndDate
from groupByMembershipDates_cte
group by [Group],PersonId,Surname,FirstName,Description
order by PersonId,StartDate
option(maxrecursion 0)
[Only many, many years later.]
I created a stored procedure that will align and break segments by a partition within a single table, and then you can use those aligned breaks to pivot the description into a ragged column using a subquery and XML PATH.
See if the below help:
Documentation: https://github.com/Quebe/SQL-Algorithms/blob/master/Temporal/Date%20Segment%20Manipulation/DateSegments_AlignWithinTable.md
Stored Procedure: https://github.com/Quebe/SQL-Algorithms/blob/master/Temporal/Date%20Segment%20Manipulation/DateSegments_AlignWithinTable.sql
For example, your call might look like:
EXEC dbo.DateSegments_AlignWithinTable
#tableName = 'tableName',
#keyFieldList = 'PersonID',
#nonKeyFieldList = 'Description',
#effectivveDateFieldName = 'StartDate',
#terminationDateFieldName = 'EndDate'
You will want to capture the result (which is a table) into another table or temporary table (assuming it is called "AlignedDataTable" in below example). Then, you can pivot using a subquery.
SELECT
PersonID, StartDate, EndDate,
SUBSTRING ((SELECT ',' + [Description] FROM AlignedDataTable AS innerTable
WHERE
innerTable.PersonID = AlignedDataTable.PersonID
AND (innerTable.StartDate = AlignedDataTable.StartDate)
AND (innerTable.EndDate = AlignedDataTable.EndDate)
ORDER BY id
FOR XML PATH ('')), 2, 999999999999999) AS IdList
FROM AlignedDataTable
GROUP BY PersonID, StartDate, EndDate
ORDER BY PersonID, StartDate