calculate attendance and mark absentees for each day MS SQL - sql-server

this is a table for attendance logs:
CREATE TABLE [dbo].[DeviceLogs] (
[DeviceLogId] INT NOT NULL,
[UserId] NVARCHAR (50) NOT NULL,
[LogDate] DATETIME NOT NULL,
);
this is my employee table:
CREATE TABLE [dbo].[Employees] (
[EmployeeId] INT IDENTITY (1, 1) NOT NULL,
[EmployeeName] NVARCHAR (50) NULL,
[UserId] NVARCHAR (50) NOT NULL,
[Gender] NVARCHAR (255) NULL,
[DepartmentId] NVARCHAR (255) NULL,
[Designation] NVARCHAR (255) NULL,
[CategoryId] INT NULL,
[DOJ] DATETIME NULL,
[DOR] DATETIME NULL,
[Status] NVARCHAR (255) NULL,
[DOB] DATETIME NULL,
);
We will insert attendance logs in the first table.
Now i want to calculate attendance for each employee in each day.
SELECT [userid] AS identit,
CONVERT(VARCHAR, userid),
Min(logdate) AS lowtime,
Max(logdate) AS hightime,
CONVERT(VARCHAR, Max(logdate) - Min(logdate), 108) AS dur,
CASE
WHEN CONVERT(VARCHAR, Max(logdate) - Min(logdate), 108) IS NOT NULL
THEN
'present'
ELSE 'Absent'
END AS Status
FROM [dbo].[devicelogs]
GROUP BY [userid]
ORDER BY userid DESC
This is the MS SQl query which i'm using.
But it is giving me result only if the person has attendance logs.
I want to calculate attendance even in the person is absent.
I want it to repeat it for everyday.
Please help me here.

If am not wrong there should be a User table which contains the list of all the users.
You have to Left/Right outer join the User table with devicelogs table.
Something like this.
SELECT U.[userid] AS identit,
CONVERT(VARCHAR, U.userid),
Min(logdate) AS lowtime,
Max(logdate) AS hightime,
CONVERT(VARCHAR, Max(logdate) - Min(logdate), 108) AS dur,
CASE
WHEN d.userid IS NOT NULL THEN 'present'
ELSE 'Absent'
END AS Status
FROM [users] U -- Replace with the table that contains all the users
LEFT OUTER JOIN [dbo].[devicelogs] D
ON U.userid = D.userid
GROUP BY [userid]
ORDER BY U.userid DESC

To get daily reports / lists it's usually best to have a date table that contains all the possible dates you'll ever need, for example 1.1.2000 - 31.12.2099. Using it you can easily create a query that groups by that date, like this:
SELECT
e.userid,
d.[date],
l.first,
l.last,
case when l.first is null then 'Absent' else 'Present' end as status
FROM
[dbo].[Employees] e
cross join [dbo].[Dates] d
outer apply (
select min(logdate) as first, max(logdate) as last
from [dbo].[devicelogs] l
where l.userid = e.userid and
l.logdate >= d.[date] and l.logdate < dateadd(day, 1, d.[date]
) l
where d.[date] >= '20150501' and d.[date] < getdate()
GROUP BY e.userid, d.[date]
ORDER BY e.userid desc, d.[date] desc
You could do it of course with devicelog too, but if there's ever a single date without any logs you'll miss that completely.
Didn't try this, hopefully there's no errors :)

First of all you must clarify why the log table may have some users with no log date because the logdate is set to NOT NULL and you are quering log table and expecting to get some users with no logdate.
The algorithm is : When a employee is not in the log table set him as absent and put the duration equal to 0 . Otherwhise calculate the duration and set him as present . Plus get his frist date of log and last date of log.
1) Get a table showing users with null dates
select E.UserID as UserID,L.LogDate as logdate from employee E
full outer join
DeviceLogs L
on E.UserID=L.UserID
2) apply your code to this table

Related

SQL Server Joining record based on max 7 days old

The query that I've build does not fit the solution. I've tried many queries but can't get the result I want.
I have the following table
CREATE TABLE [dbo].[test](
[ID] [int] IDENTITY(1,1) NOT NULL,
[ATC] [varchar](15) NOT NULL,
[PID] [varchar](7) NOT NULL,
[NAME] [varchar](50) NOT NULL,
[REG_DATE] [datetime] NULL,
[ACTIVE] [bit] NOT NULL CONSTRAINT [DF_test_ACTIVE] DEFAULT ((1))
) ON [PRIMARY]
Example data
INSERT INTO [dbo].[test] ([ATC],[PID],[NAME],[REG_DATE],[ACTIVE])
VALUES ('A01','123456','TEST1','2016-08-31 00:00:00.000',0);
INSERT INTO [dbo].[test] ([ATC],[PID],[NAME],[REG_DATE],[ACTIVE])
VALUES ('A01','123456','TEST2','2016-09-01 00:00:00.000',0);
INSERT INTO [dbo].[test] ([ATC],[PID],[NAME],[REG_DATE],[ACTIVE])
VALUES ('A01','123456','TEST3','2016-09-02 00:00:00.000',0);
INSERT INTO [dbo].[test] ([ATC],[PID],[NAME],[REG_DATE],[ACTIVE])
VALUES ('A01','123456','TEST4','2016-09-03 00:00:00.000',0);
INSERT INTO [dbo].[test] ([ATC],[PID],[NAME],[REG_DATE],[ACTIVE])
VALUES('A01','123456','TEST5','2016-09-06 00:00:00.000',1);
Example Query:
;WITH CTE AS
(
SELECT ROW_NUMBER() OVER (PARTITION BY ATC, PID ORDER BY REG_DATE ASC) AS ROWNUM,* FROM [dbo].[test]
WHERE ACTIVE=0
)
SELECT DATEDIFF(DAY, d2.REG_DATE, d1.REG_DATE),d1.NAME, d1.REG_DATE AS ACTIVE_REC_DATE, d2.REG_DATE AS NOTACTIVE_REC_DATE, d1.ACTIVE, d2.ACTIVE FROM [dbo].[test] as d1
LEFT JOIN CTE d2 ON d2.ATC = d1.ATC AND d2.PID = d1.PID
AND DATEDIFF(DAY, d2.REG_DATE, d1.REG_DATE) <= 7
WHERE d1.ACTIVE=1 AND d1.PID=123456;
Wanted result:
The record with column ACTIVE True (1) should contain (if the record exists) the REG_DATE of the not active previous record max 7 days old. Like:
(No column name) NAME ACTIVE_REC_DATE NOTACTIVE_REC_DATE ACTIVE ACTIVE
3 TEST5 2016-09-06 00:00:00.000 2016-08-31 00:00:00.000 1 0
Currently the query result contains multiple records because the are more records that will fall in the 7 days period. I need to join 1 record that will be the max 7 day old one.
I've used the ROW_NUMBER() with over partition by so I can use and identify the first record because I will be sorting ascending. This doesn't work when there is no previous records available or the previous records are older then 7 days
When there is no records to join is ignored can use INNER JOIN or date columns at null
I hope I'm clear with my explanation.
SELECT DATEDIFF(DAY, d2.REG_DATE, d1.REG_DATE),d1.NAME, d1.REG_DATE AS ACTIVE_REC_DATE, d2.REG_DATE AS NOTACTIVE_REC_DATE, d1.ACTIVE, d2.ACTIVE FROM [dbo].[test] as d1
OUTER APPLY
(SELECT TOP 1 T1.* FROM dbo.test t1 WHERE t1.ATC = d1.ATC AND t1.PID = d1.PID and DATEDIFF(DAY, t1.REG_DATE, d1.REG_DATE) <= 7 order by t1.REG_DATE desc) d2
WHERE d1.ACTIVE=1 AND d1.PID=123456;
You can return a single row for a PID by adding a filter on ROWNUM to your left join:
;WITH CTE AS
(
SELECT ROW_NUMBER() OVER (PARTITION BY ATC, PID ORDER BY REG_DATE ASC) AS ROWNUM,* FROM [dbo].[test]
WHERE ACTIVE=0
)
SELECT DATEDIFF(DAY, d2.REG_DATE, d1.REG_DATE),d1.NAME, d1.REG_DATE AS ACTIVE_REC_DATE, d2.REG_DATE AS NOTACTIVE_REC_DATE, d1.ACTIVE, d2.ACTIVE FROM [dbo].[test] as d1
LEFT JOIN CTE d2 ON d2.ATC = d1.ATC AND d2.PID = d1.PID
AND DATEDIFF(DAY, d2.REG_DATE, d1.REG_DATE) <= 7
AND d2.ROWNUM = 1
WHERE d1.ACTIVE=1 AND d1.PID=123456;
It's not clear from the question how the query should behave when no previous records from the last seven days exist.

Find all rows that have changed and write old-Newvalue to different table in SQL Server 2012

Need to write to a table all the rows where values have changed between 2 datacuts.
This must be done in sql and not using any third party tools.
I can find the difference between 2 datacuts easily by using "Except".
I have not tried chksum but added a column just in case.
What I am struggling with and need your help is
How do pull all the data out from my findings into my #Changes table?
WANTED RESULT
EmployeeId ColumnName OldValue NewValue
3 MaritalStatus Single Married
3 Surname Malone Evans
10 MaritalStatus Single Married
SETUP TEST DATA
Dummy data set up (2 Employees with Id(3,10) have changes) if you notice
employee id(3) has 2 columns changes.
IF OBJECT_ID('tempdb..#Employee') IS NOT NULL DROP TABLE #Employee
GO
IF OBJECT_ID('tempdb..#Changes') IS NOT NULL DROP TABLE #Changes
GO
CREATE TABLE #Employee
(
[Id] [int] NOT NULL,
EmployeeNo INT NOT NULL,
[DataCut] [int] NULL,
[Name] [varchar](50) NULL,
[Surname] [varchar](50) NULL,
[Gender] [varchar](10) NULL,
[MaritalStatus] [varchar](10) NULL,
[Chksum] [int] NULL,
CONSTRAINT [PK_#Employee]
PRIMARY KEY CLUSTERED ([Id] ASC)
) ON [PRIMARY]
CREATE TABLE #Changes
(
[EmployeeNo] [int] ,
[ColumnName] [varchar](50) NULL,
[OldValue] [varchar](50) NULL,
[NewValue] [varchar](50) NULL
)
INSERT INTO #Employee([Id], EmployeeNo,[DataCut], [Name], [Surname], [Gender], [MaritalStatus],[Chksum])
SELECT 1, 1,1, N'Jo', N'Bloggs', N'Male', N'Single', NULL UNION ALL
SELECT 2, 2,1, N'Mark', N'Smith', N'Male', N'Single', NULL UNION ALL
SELECT 3, 3,1, N'Jenny', N'Malone', N'Female', N'Single', NULL UNION ALL
SELECT 4, 4,1, N'Mario', N'Rossi', N'Male', N'Single', NULL UNION ALL
SELECT 5, 5,1, N'Richard', N'Jones', N'Male', N'Single', NULL UNION ALL
SELECT 6, 1,2, N'Jo', N'Bloggs', N'Male', N'Single', NULL UNION ALL
SELECT 7, 2,2, N'Mark', N'Smith', N'Male', N'Single', NULL UNION ALL
SELECT 8, 3,2, N'Jenny', N'Evans', N'Female', N'Married', NULL UNION ALL
SELECT 9, 4,2, N'Mario', N'Rossi', N'Male', N'Single', NULL UNION ALL
SELECT 10,5,2, N'Richard', N'Jones', N'Male', N'Married', NULL
--Find all the Rows that have changed between 2 datacuts using EXCEPT
SELECT EmployeeNo,Name, Surname, Gender, MaritalStatus
FROM #Employee
WHERE DataCut=1
EXCEPT
SELECT EmployeeNo,Name, Surname, Gender, MaritalStatus
FROM #Employee
WHERE DataCut=2
UNION
--do the opposite so that we get all the rows.
SELECT EmployeeNo,Name, Surname, Gender, MaritalStatus
FROM #Employee
WHERE DataCut=2
EXCEPT
SELECT EmployeeNo,Name, Surname, Gender, MaritalStatus
FROM #Employee
WHERE DataCut=1
--HOW DO I FILL MY #CHANGES TABLES TO MATCH MY WANTED RESULT?
DROP TABLE #Changes
DROP TABLE #Employee
You can use UNPIVOT:
;WITH UnpivotedTable AS (
SELECT Id, EmployeeNo, DataCut, Val, Col
FROM
(SELECT Id, EmployeeNo, DataCut, CAST(Name AS VARCHAR(50)) AS Name,
CAST(Surname AS VARCHAR(50)) AS Surname,
CAST(Gender AS VARCHAR(50)) AS Gender,
CAST(MaritalStatus AS VARCHAR(50)) AS MaritalStatus
FROM #Employee) AS src
UNPIVOT
(Val FOR Col IN
(Name, Surname, Gender, MaritalStatus)) AS unpvt
)
SELECT t1.Id As EmployeeId,
t1.Col AS ColumnName,
t1.Val AS OldValue,
t2.Val AS NewValue
FROM UnpivotedTable AS t1
INNER JOIN UnpivotedTable AS t2
ON t1.EmployeeNo = t2.EmployeeNo AND t1.Col = t2.Col AND
t1.DataCut = 1 AND t2.DataCut = 2
WHERE t1.Val <> t2.Val
Demo here
Explanation:
Here's an excerpt of the data returned by the CTE (for EmployeeNo = 1):
Id EmployeeNo DataCut Val Col
---------------------------------------------
1 1 1 Jo Name
1 1 1 Bloggs Surname
1 1 1 Male Gender
1 1 1 Single MaritalStatus
6 1 2 Jo Name
6 1 2 Bloggs Surname
6 1 2 Male Gender
6 1 2 Single MaritalStatus
Using the above table expression we can easily get the expected result performing an INNER JOIN operation: we just have to compared 'old' (DataCut = 1) vs new (DataCut = 2) values for the same EmployeeNo and Col.

low query execution time SQL server

I have this query in sql server database
SELECT [Id]
,[CreatedBy]
,[CreatedDate]
,[ModifiedBy]
,[ModifiedDate]
,[IsDeleted]
,[IsActive]
,[Type]
,[RelaseDate]
,[Prefix]
,[SubTitle]
,[Title]
,[Status]
,[Sequence]
,[Value]
,[Content]
,[Author]
,[Summery]
,[EndDate]
,[ViewedTime]
,[DefaultCategorieId]
,[URLTitle]
,[AlowComments]
,[HideImage]
,[ExternalLink]
FROM [SalesItem].[dbo].[Items]
where Type='7a38bd0c-222f-4308-8dce-f7a2014d7d79' and IsDeleted <> 1
order by [CreatedDate] desc
OFFSET 10 ROWS
FETCH NEXT 20 ROWS ONLY;
the items has about 200,000 record the execution time for this query is about 00:80:30
Is there any way to faster the query. because the same table may have many different queries which they take longer execution time
Considering [Id] as PK with identity enabled and NO index on CreatedDate, You can sort by [ID]
As ordering by both columns will be same. (Hope you are not updating CreatedDate later)
This query should be quicker
;with cte
as
(
SELECT ROW_NUMBER() over (order by [Id]) as rowid,
,[Id]
,[CreatedBy]
,[CreatedDate]
,[ModifiedBy]
,[ModifiedDate]
,[IsDeleted]
,[IsActive]
,[Type]
,[RelaseDate]
,[Prefix]
,[SubTitle]
,[Title]
,[Status]
,[Sequence]
,[Value]
,[Content]
,[Author]
,[Summery]
,[EndDate]
,[ViewedTime]
,[DefaultCategorieId]
,[URLTitle]
,[AlowComments]
,[HideImage]
,[ExternalLink]
FROM [SalesItem].[dbo].[Items]
where Type='7a38bd0c-222f-4308-8dce-f7a2014d7d79' and IsDeleted <> 1
)
select * from cte where rowid between 11 and 20
Please let me know the execution time this takes

How To Query Order By Desc StartDate But NULLS First

sQL FIDDLE
CREATE TABLE [STUDENT_MASTER]
(
[User_ID] [int] IDENTITY (1, 1) NOT NULL CONSTRAINT STUDENT_MASTER_P_KEY PRIMARY KEY,
[Name] [varchar] (50),
[START_DATE] [varchar] (50),
[PRIORITY] [varchar] (50)
)
INSERT INTO STUDENT_MASTER
VALUES('JOHN','2013-08-16','4')
INSERT INTO STUDENT_MASTER
VALUES('JACK','2013-08-10','')
INSERT INTO STUDENT_MASTER
VALUES('MACK','','1')
INSERT INTO STUDENT_MASTER
VALUES('ACK','2013-08-15','2')
//SQL QUERY
SELECT ROW_NUMBER() OVER
(ORDER BY CASE
WHEN STUDENT_MASTER.START_DATE IS NULL THEN 1
WHEN STUDENT_MASTER.PRIORITY IS NULL THEN 1
ELSE 0 END,STUDENT_MASTER.START_DATE DESC ,STUDENT_MASTER.PRIORITY DESC
)AS RowNumber,STUDENT_MASTER.START_DATE
FROM STUDENT_MASTER
HOW TO QUERY ORDER BY DESC START DATE AND NULL VALUE BE FIRST
DEMO
ORDER BY
CASE WHEN START_DATE = '' THEN 0 ELSE 1 END ASC,
START_DATE DESC
You could use ISNULL, but your column does not have null values - it has empty strings intead.
And another problem - because your START_DATE column is VARCHAR, not DATETIME it will perform string, alphabetical sort instead of datetime sorting.
Don't simply order by StartDate, order by StartDate or a default value if it's null.
ORDER BY ISNULL(StartDate, '9999-12-31') DESC
Oh... and have your date be an actual datetime. Otherwise you'll just be doing an alphabetical sort.
Considering that is for purpose your date as string, this would be my solution for the problem
SELECT
start_date
FROM
STUDENT_MASTER
ORDER BY
LEN(start_date),
start_date DESC
Follows the SQL Fiddle

Date Range Intersection Splitting in SQL

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

Resources