I have this tables Holiday(Id,FK(EmployeeId),StartDate,EndDate) and table Employee(Id,FullName,etc...)
I want to know the number of days that each employee have
I was trying something like this :
SELECT Employee.Id, SUM(DATEDIFF(day,Holiday.StartDate,Holiday.EndDate) + 1)
FROM Employee
LEFT JOIN Holiday ON Holiday.EmployeeId=Employee.Id
GROUP BY Employee.id
i know this doesn't work because, to sum thati would need to group by Holiday.Id since i will have many rows in the Holiday table for the same EmployeeId
how can i accomplish this?
thanks for the help
Or, using a workingday calculation I found here: https://www.sqlshack.com/how-to-calculate-work-days-and-hours-in-sql-server you could do the following:
CREATE FUNCTION workingdays ( #DateFrom Date, #DateTo Date) RETURNS INT AS
BEGIN
DECLARE #TotDays INT= DATEDIFF(DAY, #DateFrom, #DateTo) + 1;
DECLARE #TotWeeks INT= DATEDIFF(WEEK, #DateFrom, #DateTo) * 2;
DECLARE #IsSunday INT= CASE
WHEN DATENAME(WEEKDAY, #DateFrom) = 'Sunday'
THEN 1
ELSE 0
END;
DECLARE #IsSaturday INT= CASE
WHEN DATENAME(WEEKDAY, #DateTo) = 'Saturday'
THEN 1
ELSE 0
END;
DECLARE #TotWorkingDays INT= #TotDays - #TotWeeks - #IsSunday + #IsSaturday;
RETURN #TotWorkingDays;
END
GO
create table Employee (Id int identity, name varchar(64), Primary Key (Id));
create table Holiday (EmployeeId int, StartDate date, EndDate date);
insert into Employee VALUES ('Harry Potter'),('Hermiony Granger'),('Ron Weasly'),('Ginny Weasley');
insert into Holiday VALUES (1,'2020-02-12','2020-02-18'),(1,'2020-04-02','2020-04-07'),(1,'2020-08-21','2020-09-05'),
(2,'2020-01-04','2020-01-13'),(2,'2020-03-17','2020-03-23'),(2,'2020-05-29','2020-06-7');
SELECT Employee.Id, SUM(dbo.workingdays(Holiday.StartDate,Holiday.EndDate))
FROM Employee
LEFT JOIN Holiday ON Holiday.EmployeeId=Employee.Id
GROUP BY Employee.id
This will still only be a crude estimation as is does not account for public holidays.
DEMO: https://rextester.com/QKGUT41272
Hi i think you can build query using CTE like this :
Resource : CTE Microsoft : https://learn.microsoft.com/fr-fr/sql/t-sql/queries/with-common-table-expression-transact-sql?view=sql-server-ver15
Or using select in clause form
EDIT : Or it work with your current query to
create Table #EMPLOYEE
(
EMP_ID INT,
EMP_NAME varchar(128)
)
create Table #HOLIDAYS
(
HL_ID INT,
HL_EMP varchar(128),
StartDate DATE,
EndDate DATE
)
Insert into #EMPLOYEE
(
EMP_ID,
EMP_NAME
)
SELECT 1, 'Toto'
UNION ALL
SELECT 2,'Dupont'
UNION ALL
SELECT 3,'Titi'
UNION ALL
SELECT 4,'Tata'
Insert into #HOLIDAYS
(
HL_ID,
HL_EMP,
StartDate,
EndDate
)
SELECT '1', '1',GETDATE(),DATEADD(day,4,GETDATE())
UNION ALL
SELECT '2','1',DATEADD(day,-7,GETDATE()), DATEADD(day,-5,GETDATE())
UNION ALL
SELECT '3','2',DATEADD(day,4,GETDATE()),DATEADD(day,15,GETDATE())
-- USING CTE EXEMPLE
;WITH MyCteAPP AS (
SELECT EMP_ID, EMP_NAME, HL_ID, ISNULL(StartDate,GETDATE()) AS 'StartDate', ISNULL(EndDate,GETDATE()) AS 'EndDate'
FROM #EMPLOYEE
LEFT JOIN #HOLIDAYS ON EMP_ID = HL_EMP
)
SELECT EMP_ID, EMP_NAME, SUM(DATEDIFF(day,StartDate,EndDate)) AS 'NbDay'
FROM MyCteAPP
GROUP BY EMP_ID,EMP_NAME
ORDER BY EMP_ID
-- USING SELECT IN FROM EXEMPLE
SELECT EMP_ID, EMP_NAME, SUM(DATEDIFF(day,StartDate,EndDate)) AS 'NbDay'
FROM (SELECT EMP_ID, EMP_NAME, HL_ID, ISNULL(StartDate,GETDATE()) AS 'StartDate', ISNULL(EndDate,GETDATE()) AS 'EndDate'
FROM #EMPLOYEE
LEFT JOIN #HOLIDAYS ON EMP_ID = HL_EMP
) AS SUBQUERY
GROUP BY EMP_ID,EMP_NAME
ORDER BY EMP_ID
--USING CURRENT QUERY
SELECT EMP_ID, EMP_NAME, SUM(DATEDIFF(day,ISNULL(StartDate,GETDATE()),ISNULL(EndDate,GETDATE()))) AS 'NbDay'
FROM #EMPLOYEE
LEFT JOIN #HOLIDAYS ON EMP_ID = HL_EMP
GROUP BY EMP_ID,EMP_NAME
ORDER BY EMP_ID
DROP TABLE #EMPLOYEE
DROP TABLE #HOLIDAYS
RESULT :
I have tried with sub query. Please use below query. It will be helpful.
SELECT Id, SUM(leave) AS leave
FROM
(
SELECT Employee.Id, DATEDIFF(dd,Holiday.StartDate,Holiday.EndDate) as leave
FROM Employee
LEFT JOIN Holiday ON Holiday.EmployeeId=Employee.Id
)a
GROUP BY ID
Related
I have Request table with 3 records having structure: Id, DateFrom, DateTo
Id DateFrom DateTo
1 15/01/2019 15/01/2019
2 21/01/2019 28/01/2019
3 04/02/2019 09/02/2019
And I want an output like this:
Id Date
1 15/01/2019
2 21/01/2019
2 22/01/2019
2 23/01/2019
2 24/01/2019
2 25/01/2019
2 26/01/2019
2 27/01/2019
2 28/01/2019
3 04/02/2019
3 05/02/2019
3 06/02/2019
3 07/02/2019
3 08/02/2019
3 09/02/2019
I have created a table valued function to display the series of date based DateFrom and DateTo.
CREATE FUNCTION [dbo].[tvfhrms_Calendar_DateRange](#DateFrom date, #DateTo date)
RETURNS #DateOfTheYear Table(Level int,SysDate date)
AS
BEGIN
WITH AllDays
AS (
SELECT [Level] = 1
,[Date] = #DateFrom
UNION ALL
SELECT [Level] = [Level] + 1
,[Date] = DATEADD(DAY, 1, [Date])
FROM AllDays
WHERE [Date] < #DateTo
)
INSERT #DateOfTheYear
SELECT [Level]
,[SysDate]=[Date]
FROM AllDays OPTION (MAXRECURSION 0)
RETURN
END
Then when used in select query,
SELECT sysdate from [dbo].[tvfhrms_Calendar_DateRange]('2019-01-10', '2019-02-09')
This will give the results of the sequence of Datefrom to DateTo.
How can I integrate this to my table so that I can have the output as my expectations?
You can use APPLY :
SELECT tt.*
FROM table t CROSS APPLY
(SELECT tt.*
FROM [dbo].[tvfhrms_Calendar_DateRange] (t.datefrom, t.dateto) AS tt
) tt;
No need to have extra table with dates. Note that my dates in different format.
DECLARE #t TABLE (Id INT, DateFrom DATE, DateTo DATE)
INSERT INTO #t VALUES
(1,'01/15/2019','01/15/2019'),
(2,'01/21/2019','01/28/2019'),
(3,'02/04/2019','02/09/2019')
;WITH cte as (
SELECT ID, [Date] = DateFrom FROM #t
UNION ALL
SELECT t.ID, DATEADD(DAY,1,[Date]) FROM #t as t
INNER JOIN cte ON t.ID = cte.ID and cte.[Date] < t.DateTo
)
SELECT * FROM cte
ORDER BY ID
I have a salary table like this:
declare #t table (OrderedID int, EmpID int, EffDate date, Salary money)
insert into #t
values
(1,1234,'20150101',100)
,(2,1234,'20160101',100)
,(3,1234,'20170101',100)
,(4,1234,'20180101',300)
,(1,2351,'20150101',100)
I am trying to get an initial effective date on each row:
First 3 rows have 1/1/2015
4th row has new value 1/1/2018
Here is what I tried with a case and a lag but i can't figure out how to reference the prior value of the column I am creating.
case when OrderedID = 1 then EFFDaTe
when Salary != LAG(Salary,1) then EFFDaTe
else lag(SalaryEFFDT,1) over (order by 1)
end as SalaryEFFDT
Thanks for your help.
As you haven't provided the expected output, I think this is what you want:
declare #t table (OrderedID int, EmpID int, EffDate date, Salary money)
insert into #t
values
(1,1234,'20150101',100)
,(2,1234,'20160101',100)
,(3,1234,'20170101',100)
,(4,1234,'20180101',300)
,(1,2351,'20150101',100)
,(5,1234,'20190101',100)
;with cte as
(Select *, OrderedId - Row_Number() over (partition by EmpId,Salary order by OrderedID) as grp
from #t)
, cte1 as
(Select EmpID, grp, min(effDate) as effDate from cte c group by EmpID, grp)
Select OrderedID, t.EmpID, t.EffDate, t.Salary, c.effDate as computeddate
from cte t join cte1 c on t.EmpID = c.EmpID and t.grp = c.grp
order by OrderedID
So you are trying to get the first effective date for each EmpID? the code below should do that. If that is not your desired output can you put what the output should look like?
declare #t table (OrderedID int, EmpID int, EffDate date, Salary money)
insert into #t
values
(1,1234,'20150101',100)
,(2,1234,'20160101',100)
,(3,1234,'20170101',100)
,(4,1234,'20180101',300)
,(1,2351,'20140101',100)
,(2,2351,'20150101',100)
Select
T.*,FE.FirstEff
From #t T
inner join (Select EmpID,MIN(EffDate) as FirstEff from #t group by
The second set is if you need the first time they have that salary, however you will have issues if someone gets a raise and then a demotion.
Select
T.*,FE.FirstEff
From #t T
inner join (Select EmpID,Salary,MIN(EffDate) as FirstEff from #t group by EmpID,Salary) FE on FE.EmpID = T.EmpID
and FE.Salary = T.Salary
I have these three tables .
CREATE TABLE Employees (
EmpID INT IDENTITY,
EmpName VARCHAR(100)
)
GO
INSERT INTO Employees(EmpName)
SELECT 'John Torres' UNION ALL
SELECT 'Irina Williams'
SELECT * FROM Employees
GO
CREATE TABLE PayrollWeek(
WeekID INT IDENTITY,
EmpID INT,
WeekStart DATETIME,
WeekEnd DATETIME
)
GO
INSERT INTO PayrollWeek(EmpID,WeekStart,WeekEnd)
SELECT 1,'11-20-2011','11-26-2011' UNION ALL
SELECT 2,'11-27-2011','12-03-2011' UNION ALL
SELECT 1,'11-27-2011','12-03-2011'
SELECT * FROM PayrollWeek
GO
CREATE TABLE EmployeeVisits(
ID INT,
EmpID INT,
VisitDate DATETIME,
StartTime VARCHAR(5),
EndTime VARCHAR(5),
EarningCode VARCHAR(100)
)
GO
INSERT INTO EmployeeVisits(ID,EmpID,VisitDate,StartTime,EndTime,EarningCode)
SELECT 1,1,'11-20-2011','10:00','12:00','Sat-Sun1' UNION ALL
SELECT 2,1,'11-21-2011','13:30','16:00','Mon-Fri1' UNION ALL
SELECT 3,1,'11-22-2011','14:00','15:00','Mon-Fri1' UNION ALL
SELECT 4,1,'11-24-2011','10:00','14:00','Mon-Fri1' UNION ALL
SELECT 5,1,'11-25-2011','13:30','16:00','Mon-Fri1' UNION ALL
SELECT 6,1,'11-26-2011','14:00','15:00','Sat-Sun1' UNION ALL
SELECT 7,2,'11-27-2011','09:00','11:00','Sat-Sun1' UNION ALL
SELECT 8,2,'11-28-2011','07:00','12:00','Mon-Fri1' UNION ALL
SELECT 9,2,'11-29-2011','09:00','11:00','Mon-Fri1' UNION ALL
SELECT 10,2,'12-03-2011','07:00','12:00','Sat-Sun1'
Expected Result is this
RecordType EmpId EmpName WeekStart Weekend EarningCode Hour
H 1 John Torres 11/20/2011 11/26/2011
D Sat-Sun1 3
D Mon-Fri1 10
H 2 Irina Williams 11/27/2011 12/3/2011
D Sat-Sun1 7
D Mon-Fri1 7
Here is my answer I try this using Union, Group By .
SELECT 'H' AS RecordType
,emp.EmpID
,EmpName
,Cast(MIN(WeekStart) AS VARCHAR(106)) AS StartTime
,cast(Max(VisitDate) AS VARCHAR(106)) AS EndTime
,'' AS EarningCode
,'' AS TimeDiff
FROM EmployeeVisits E
JOIN PayrollWeek P ON e.EmpID = P.EmpID
JOIN Employees Emp ON Emp.EmpID = e.EmpID
GROUP BY emp.EmpID
,EmpName
UNION
SELECT 'D' AS RecordType
,EmpID
,'' AS EmpName
,'' AS StartTime
,'' AS EndTime
,EarningCode
,cast(sum(dateDiff(MINUTE, StartTime, EndTime)) / 60 AS VARCHAR(100)) AS TimeDiff
FROM EmployeeVisits
GROUP BY EmpID
,EarningCode
ORDER BY EmpID
,RecordType DESC
I need to concatenate rows with a date and a code into a date range
Table with two columns that are a composite primary key (date and a code )
Date Code
1/1/2011 A
1/2/2011 A
1/3/2011 A
1/1/2011 B
1/2/2011 B
2/1/2011 A
2/2/2011 A
2/27/2011 A
2/28/2011 A
3/1/2011 A
3/2/2011 A
3/3/2011 A
3/4/2011 A
Needs to be converted to
Start Date End Date Code
1/1/2011 1/3/2011 A
2/1/2011 2/2/2011 A
1/1/2011 1/2/2011 B
2/27/2011 3/4/2011 A
Is there any other way or is a cursor loop the only way?
declare #T table
(
[Date] date,
Code char(1)
)
insert into #T values
('1/1/2011','A'),
('1/2/2011','A'),
('1/3/2011','A'),
('1/1/2011','B'),
('1/2/2011','B'),
('3/1/2011','A'),
('3/2/2011','A'),
('3/3/2011','A'),
('3/4/2011','A')
;with C as
(
select *,
datediff(day, 0, [Date]) - row_number() over(partition by Code
order by [Date]) as rn
from #T
)
select min([Date]) as StartDate,
max([Date]) as EndDate,
Code
from C
group by Code, rn
sql server 2000 has it limitations. Rewrote the solution to make it more readable.
declare #t table
(
[Date] datetime,
Code char(1)
)
insert into #T values
('1/1/2011','A'),
('1/2/2011','A'),
('1/3/2011','A'),
('1/1/2011','B'),
('1/2/2011','B'),
('3/1/2011','A'),
('3/2/2011','A'),
('3/3/2011','A'),
('3/4/2011','A')
select a.code, a.date, min(b.date)
from
(
select *
from #t t
where not exists (select 1 from #t where t.code = code and t.date -1 = date)
) a
join
(
select *
from #t t
where not exists (select 1 from #t where t.code = code and t.date = date -1)
) b
on a.code = b.code and a.date <= b.date
group by a.code, a.date
Using a DatePart function for month will get you the "groups" you want
SELECT Min(Date) as StartDate, Max(Date) as EndDate, Code
FROM ThisTable Group By DatePart(m, Date), Code
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