How to calculate time difference on column value change? - sql-server

I am trying to write a query to get the accumulated time difference with respect to a value change in a column in SQL Server 2012.
I am trying to gather some analytics on how much time a task was pending on another user, in this task there are 2 participants Role = 0 is the implementer and Role = 1 is the reviewer of the task. Through the duration of the task the implementer and the the reviewer can perform activities on the task multiple times. The aim is to to get the total time it was pending for the reviewer and the implementer.
To re create a snapshot of the data please see the example below
CREATE TABLE ActivityTable
([Id] [int] IDENTITY(1,1) NOT NULL,
[RoleId] [int] NULL,
[ActivityDate] [DATETIME] NULL)
INSERT INTO [ActivityTable] VALUES (1, '2018-10-19 13:00:19.840')
INSERT INTO [ActivityTable] VALUES (1, '2018-10-19 13:00:18.073')
INSERT INTO [ActivityTable] VALUES (1, '2018-10-19 12:59:48.417')
INSERT INTO [ActivityTable] VALUES (0, '2018-10-15 13:48:00.557')
INSERT INTO [ActivityTable] VALUES (0, '2018-10-15 12:56:25.567')
INSERT INTO [ActivityTable] VALUES (0, '2018-10-15 12:56:09.967')
INSERT INTO [ActivityTable] VALUES (0, '2018-10-15 12:55:26.500')
INSERT INTO [ActivityTable] VALUES (0, '2018-10-15 12:53:17.997')
INSERT INTO [ActivityTable] VALUES (1, '2018-10-15 12:36:17.967')
INSERT INTO [ActivityTable] VALUES (1, '2018-10-15 12:35:38.497')
INSERT INTO [ActivityTable] VALUES (1, '2018-10-15 12:33:05.860')
INSERT INTO [ActivityTable] VALUES (1, '2018-10-15 12:32:07.793')
INSERT INTO [ActivityTable] VALUES (1, '2018-10-15 12:32:00.010')
INSERT INTO [ActivityTable] VALUES (0, '2018-10-15 12:18:18.417')
INSERT INTO [ActivityTable] VALUES (0, '2018-10-15 12:17:16.370')
INSERT INTO [ActivityTable] VALUES (0, '2018-10-15 12:11:48.590')
INSERT INTO [ActivityTable] VALUES (0, '2018-10-15 11:58:38.557')
INSERT INTO [ActivityTable] VALUES (0, '2018-10-15 11:56:23.820')`
So the total time for each transition would look like
RoleInfo Start Time End Time Duration Minutes
0 2018-10-15 11:56:23.820 2018-10-15 12:32:00.010 37
1 2018-10-15 12:32:00.010 2018-10-15 12:53:17.997 22
0 2018-10-15 12:53:17.997 2018-10-19 12:59:48.417 5767
1 2018-10-19 12:59:48.417 2018-10-19 13:00:19.840 1
and the final result expected is aggregation of pending times
RoleInfo Duration in Minutes
0 5804
1 23

refer to comments within the query.
execute each inner query by itself to see the result for better understanding
select RoleId, Duration = sum(datediff(minute, StartTime, EndTime))
from
(
-- perform a group by RoleId + grp
-- min() and max() on ActivityDate will gives you based on RoleId
-- however you wanted the StartTime of next RoleId. For this LEAD() OVER() is used
select RoleId,
StartTime = min(ActivityDate),
EndTime = coalesce(lead(min(ActivityDate)) over (order by min(ActivityDate)),
max(ActivityDate))
from
(
-- identify the group. each group is continuous same RoleId value
select *, grp = Id - dense_rank() over (partition by RoleId
order by ActivityDate desc)
from ActivityTable
) a
group by RoleId, grp
) b
group by RoleId
by the way, i think the expected result that you posted is wrong
/* RESULT
RoleId Duration
0 5802
1 22
*/

Related

How to re write while loop using cte

I have two tables, one with Events, the other with episodes.
An Episode has a start date and end date, the event has a single date.
Both Episodes and Events have one of six Types.
Currently I'm using some fuzzy logic to run an update script on the Events table to set it's ID field to the matching Episode. It does this by checking for the Event date between the Episode start and end, both having the same Type, as well as some other links like same User etc.
Since the Events can sit outside of the Episode, or have a different Type, what I do is loop through a sequence of expanding date ranges (StartDate-1, -2 etc) and also cycle through each Type looking for a match.
I've been reading that while loops aren't very efficient, so was wondering if there was a way to rewrite this nested loop into a CTE function.
I'm using SQL Server 2012.
Event List is just a temp table that has all the possible Types with an order to loop through.
My loop currently is:
WHILE #CurrBefore <= #Before and #CurrentAfter <= #After
BEGIN
SET #Row = 0
WHILE #Row <= #MaxRow
BEGIN
UPDATE P
SET P.ID = E.ID
FROM Event P
OUTER APPLY (SELECT TOP 1 E.Id, E.Type
FROM Episode E
WHERE E.User = P.User AND
E.Type = CASE WHEN #Row=0 THEN P.Event ELSE (SELECT Event FROM #EventList WHERE RN = #Row) END AND
P.Date BETWEEN E.StartDate-#CurrentBefore AND E.EndDate+#CurrentAfter
ORDER BY P.Date) E
WHERE P.ID = 0
INCREMENT #ROW CODE
END
INCREMENT #BEFORE/AFTER CODE
END
Sample Data:
IF OBJECT_ID('tempdb..#EventList') IS NOT NULL
BEGIN
DROP TABLE #EventList
CREATE TABLE #EventList(Event Varchar(50), RN INT);
INSERT INTO #EventList SELECT 'A', 1
INSERT INTO #EventList SELECT 'B', 2
INSERT INTO #EventList SELECT 'C', 3
INSERT INTO #EventList SELECT 'D', 4
INSERT INTO #EventList SELECT 'E', 5
INSERT INTO #EventList SELECT 'F', 6
END
CREATE TABLE dbo.Episode ([ID] INT, [Start] DateTime, [End] DateTime, [Type] varchar(1), [User] INT)
INSERT INTO [dbo].Episode ([ID], [Start], [End], [Type],[User])
VALUES
(1, '2018-07-01 10:00', '2018-07-02 14:00', 'A',10),
(2, '2018-07-05 6:00', '2018-07-06 13:00', 'A',11),
(3, '2018-07-03 9:00', '2018-07-04 8:00', 'B',10),
(4, '2018-07-02 15:00', '2018-07-03 7:00', 'B',12),
(5, '2018-07-01 1:00', '2018-07-02 8:00', 'C',13),
(6, '2018-07-01 6:00', '2018-07-01 8:00', 'D',11)
CREATE TABLE dbo.Event ([ID] INT, [Date] DateTime, [Type] varchar(1), [User] INT)
INSERT INTO [dbo].Event ([ID], [Date], [Type],[User])
VALUES
(0, '2018-07-01 12:00', 'A',10),
(0, '2018-07-05 15:00', 'A',11),
(0, '2018-07-03 13:00', 'C',10),
(0, '2018-07-10 9:00', 'B',12),
(0, '2018-07-01 5:00', 'C',10),
(0, '2018-07-01 10:00', 'D',11)
Expected result, Event now looks like this:
1 2018-07-01 12:00:00.000 A 10
2 2018-07-05 15:00:00.000 A 11
3 2018-07-03 13:00:00.000 C 10
0 2018-07-10 09:00:00.000 B 12
1 2018-07-01 05:00:00.000 C 10
6 2018-07-01 10:00:00.000 D 11
I don't know, if I fully got the logic, but this might help to get you running:
USE master;
GO
CREATE DATABASE TestDB
GO
USE TestDB;
GO
CREATE TABLE dbo.Episode ([ID] INT, [Start] DateTime, [End] DateTime, [Type] varchar(1), [User] INT)
INSERT INTO [dbo].Episode ([ID], [Start], [End], [Type],[User])
VALUES
(1, '2018-07-01 10:00', '2018-07-02 14:00', 'A',10),
(2, '2018-07-05 6:00', '2018-07-06 13:00', 'A',11),
(3, '2018-07-03 9:00', '2018-07-04 8:00', 'B',10),
(4, '2018-07-02 15:00', '2018-07-03 7:00', 'B',12),
(5, '2018-07-01 1:00', '2018-07-02 8:00', 'C',13),
(6, '2018-07-01 6:00', '2018-07-01 8:00', 'D',11)
CREATE TABLE dbo.[Event] ([ID] INT, [Date] DateTime, [Type] varchar(1), [User] INT)
INSERT INTO [dbo].[Event] ([ID], [Date], [Type],[User])
VALUES
(0, '2018-07-01 12:00', 'A',10),
(0, '2018-07-05 15:00', 'A',11),
(0, '2018-07-03 13:00', 'C',10),
(0, '2018-07-10 9:00', 'B',12),
(0, '2018-07-01 5:00', 'C',10),
(0, '2018-07-01 10:00', 'D',11)
GO
CREATE TABLE #EventList(Event Varchar(50), RN INT);
INSERT INTO #EventList VALUES ('A', 1),('B', 2),('C', 3),('D', 4),('E', 5),('F', 6);
WITH mathingEpisodes AS
(
SELECT ev.ID AS evID
,ev.[Date] AS evDate
,ev.[Type] AS evType
,ev.[User] AS evUser
,e1.RN AS evRN
,ep.ID AS epID
,ep.[Type] AS epType
,e2.RN AS epRN
FROM [Event] ev
LEFT JOIN Episode ep ON ev.[User]=ep.[User] AND ev.[Date] >= ep.[Start] AND ev.[Date] < ep.[End]
LEFT JOIN #EventList e1 ON ev.[Type]=e1.[Event]
LEFT JOIN #EventList e2 ON ep.[Type]=e2.[Event]
)
SELECT COALESCE(epID,Closest.ID) AS FittingEpisodeID
,me.evDate
,evType
,evUser
FROM mathingEpisodes me
OUTER APPLY(SELECT TOP 1 *
FROM Episode ep
CROSS APPLY(SELECT ABS(DATEDIFF(SECOND,me.evDate,ep.[Start])) AS DiffToStart
,ABS(DATEDIFF(SECOND,me.evDate,ep.[End])) AS DiffToEnd) Diffs
CROSS APPLY(SELECT CASE WHEN DiffToStart<DiffToEnd THEN DiffToStart ELSE DiffToEnd END AS Smaller) Diffs2
WHERE ep.[User] = me.evUser
AND me.epID IS NULL
ORDER BY Diffs2.Smaller
) Closest
ORDER BY evDate;
GO
USE master;
GO
DROP DATABASE TestDB;
GO
DROP TABLE #EventList
GO
The result
1 2018-01-07 05:00:00.000 C 10
6 2018-01-07 10:00:00.000 D 11
1 2018-01-07 12:00:00.000 A 10
3 2018-03-07 13:00:00.000 C 10
2 2018-05-07 15:00:00.000 A 11
4 2018-10-07 09:00:00.000 B 12
Some explanation
In the first cte I try to find fitting episodes (same user and date within range).
The second cte will compute the closest Episode for the same user in all cases, where the first cte did not succeed.
The only difference for this sample is the event for userId=12. My logic will bind this to the closest episode of this user (ID=4), while your expected output shows a zero in this place.
Anyway, my solution is fully set-based, therefore faster than a loop, and should be rather close to your needs. Try to adapt it...
UPDATE Some more thoughts...
I did not get the ghist of your #EventList... I bound the results into the set (you can make it visible by using SELECT * instead of the explicit column list. But this is - assumably - not what you meant...

Count should return only if both times are present

I want to calculate the number of days employee was present. If he/she doesn't have timeout then it should not consider their present.
CREATE TABLE Attendance(
[EmpCode] INT
,[TimeIn] DATETIME
,[TimeOut] DATETIME
)
INSERT INTO Attendance VALUES (12, '2018-08-01 09:00:00.000', '2018-08-01 17:36:00.000');
INSERT INTO Attendance VALUES (12, '2018-08-02 09:00:00.000', NULL);
INSERT INTO Attendance VALUES (12, '2018-08-03 09:25:00.000', '2018-08-03 16:56:00.000');
INSERT INTO Attendance VALUES (12, '2018-08-04 09:13:00.000', NULL);
INSERT INTO Attendance VALUES (12, '2018-08-06 09:00:00.000', '2018-08-07 18:15:00.000');
INSERT INTO Attendance VALUES (12, '2018-08-07 09:27:00.000', NULL);
My query is returning 6 days but it should return 3 days as only 3 days have both time in and time out.
SELECT
COUNT(CAST(COALESCE([TimeIn], [TimeOut]) AS DATE))
FROM [dbo].[Attendance]
WHERE
CAST(COALESCE([TimeIn], [TimeOut]) AS DATE) BETWEEN '2018-08-01' AND '2018-08-07'
just add a WHERE condition checking for TimeOut is not null
SELECT
COUNT(CAST(COALESCE([TimeIn], [TimeOut]) AS DATE))
FROM
[dbo].[Attendance]
WHERE
[TimeOut] is not null
and
CAST(COALESCE([TimeIn], [TimeOut]) AS DATE) BETWEEN '2018-08-01' AND '2018-08-07'

how to get flag values based on conditions in sql server

I have a question about SQL Server: how to get flag values based on pid?
If name values A and Name value B corresponding id values is 1 then flag 1 other wise 0
CREATE TABLE [dbo].[TABLECHECK]
(
[ID] [INT] NULL,
[NAME] [VARCHAR](50) NULL,
[PID] [INT] NULL
) ON [PRIMARY]
GO
INSERT INTO [dbo].[TABLECHECK] ([ID], [NAME], [PID])
VALUES (1, N'A', 1), (0, N'B', 1), (1, N'A', 2),
(1, N'B', 2), (0, N'A', 3), (0, N'B', 3)
Based on this data, I want to get an output like this:
PID | Flag
-----+-------
1 | 0
2 | 1
3 | 0
My query:
select
pid, count(id),
case
when name in ('a', 'b') and id = 1
then 1
else 0
end
from
[TABLECHECK]
group by
pid
This query is resulting in an error.
Please tell me how to achieve this task in SQL Server.
If A & B always exists per PID, you can do this
SELECT PID,
Flag = CASE WHEN MIN(ID) = 1 THEN 1 ELSE 0 END
FROM TABLECHECK
GROUP BY PID
Since you are using SQL Server 2012, you can use MIN(), IIF() and GROUP BY
SELECT PID,
IIF(MIN(ID) = 1, 1, 0) Flag
FROM TABLECHECK
GROUP BY PID;

How do you validate that range doesn't overlap in a list of data?

I have a list of data :
Id StartAge EndAge Amount
1 0 2 50
2 2 5 100
3 5 10 150
4 6 9 160
I have to set Amount for various age group.
The age group >0 and <=2 need to pay 50
The age group >2 and <=5 need to pay 100
The age group >5 and <=10 need to pay 150
But
The age group >6 and <=9 need to pay 160 is an invalid input because >6 and <=9 already exist on 150 amount range.
I have to validate such kind of invalid input before inserting my data as a bulk.Once 5-10 range gets inserted anything that is within this range should not be accepted by system. For example: In above list, user should be allowed to insert 10-15 age group but any of the following should be checked as invalid.
6-9
6-11
3-5
5-7
If Invalid Input exists on my list I don't need to insert the list.
You could try to insert your data to the temporary table first.
DECLARE #TempData TABLE
(
[Id] TINYINT
,[StartAge] TINYINT
,[EndAge] TINYINT
,[Amount] TINYINT
);
INSERT INTO #TempData ([Id], [StartAge], [EndAge], [Amount])
VALUES (1, 0, 2, 50)
,(2, 2, 5, 100)
,(3, 5, 10, 150)
,(4, 6, 9, 160);
Then, this data will be transferred to your target table using INSERT INTO... SELECT... statement.
INSERT INTO <your target table>
SELECT * FROM #TempData s
WHERE
NOT EXISTS (
SELECT 1
FROM #TempData t
WHERE
t.[Id] < s.[Id]
AND s.[StartAge] < t.[EndAge]
AND s.[EndAge] > t.[StartAge]
);
I've created a demo here
We can use recursive CTE to find how records are chained by end age and start age pairs:
DECLARE #DataSource TABLE
(
[Id] TINYINT
,[StartAge] TINYINT
,[EndAge] TINYINT
,[Amount] TINYINT
);
INSERT INTO #DataSource ([Id], [StartAge], [EndAge], [Amount])
VALUES (1, 0, 2, 50)
,(2, 2, 5, 100)
,(3, 5, 10, 150)
,(4, 6, 9, 160)
,(5, 6, 11, 160)
,(6, 3, 5, 160)
,(7, 5, 7, 160)
,(9, 10, 15, 20)
,(8, 7, 15, 20);
WITH PreDataSource AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY [StartAge] ORDER BY [id]) as [pos]
FROM #DataSource
), DataSource AS
(
SELECT [Id], [StartAge], [EndAge], [Amount], [pos]
FROM PreDataSource
WHERE [id] = 1
UNION ALL
SELECT R.[Id], R.[StartAge], R.[EndAge], R.[Amount], R.[pos]
FROM DataSource A
INNER JOIN PreDataSource R
ON A.[Id] < R.[Id]
AND A.[EndAge] = R.[StartAge]
AND R.[pos] =1
)
SELECT [Id], [StartAge], [EndAge], [Amount]
FROM DataSource;
This is giving us, the following output:
Note, that before this, we are using the following statement to prepare the data:
SELECT *, ROW_NUMBER() OVER (PARTITION BY [StartAge] ORDER BY [id]) as [pos]
FROM #DataSource;
The idea is to find records with same start age and to calculated which one is inserted first. Then, in the CTE we are getting only the first.
Assuming you are bulk inserting the mentioned data into a temp table(#tmp) or table variable (#tmp).
If you are working on sql server 2012 try the below.
select *
from(select *,lag(endage,1,0)over(order by endage) as [col1]
from #tmp)tmp
where startage>=col1 and endage>col1
The result of this query should be inserted into your main table.

SSAS Measure average related to range values

I have Sales data provided weekly and Lookup data provided quarterly.
In the SSAS data cube I have pre-calculated average of sales data for each period of time and what I need to do is to get related record from LookupTable for next calculations, where: LookupTable.Min < Sales Average < LookupTable.Max
Example:
Sales = 297 + 33 + 311 = 641
SalesAverage = 213.66
LookupRecordShrinkageIndicator = Min < SalesAverage < Max = 0 < 213.66 < 9000 = 0.007
CREATE TABLE dbo.SalesData
(
Id int,
Sales decimal(18, 2) )
CREATE TABLE dbo.LookupTable
(
Id int,
Min int,
Max int,
Shrinkage decimal(10, 5),
Wages decimal(10, 5),
Waste decimal(10, 5)
)
INSERT [dbo].[SalesData] ([Id], [Sales]) VALUES (1, 297)
INSERT [dbo].[SalesData] ([Id], [Sales]) VALUES (2, 33)
INSERT [dbo].[SalesData] ([Id], [Sales]) VALUES (3, 311)
INSERT [dbo].[LookupTable] ([Id], [Min], [Max], [Shrinkage], [Wages], [Waste]) VALUES (1, 0, 9000, 0.00700, 0.12700, 0.00300)
INSERT [dbo].[LookupTable] ([Id], [Min], [Max], [Shrinkage], [Wages], [Waste]) VALUES (2, 9000, 9250, 0.00700, 0.12700, 0.00300)
INSERT [dbo].[LookupTable] ([Id], [Min], [Max], [Shrinkage], [Wages], [Waste]) VALUES (3, 9250, 9500, 0.00700, 0.12300, 0.00300)
I need to create calculated member based on sales average which contains indicators from lookup table for next calculations.
To solve this issue I had to use my LookupTable as dimension and as measures, let's see how I did this.
Create dimension based on LookupTable:
Add Lookup measures do the cube and add Lookup dimension to the cube as well.
Create Fact relationship between Lookup dimension and Lookup measures group
That's all:
Let's see mdx example:
SELECT
{
FILTER([Lookup Table].[Id].AllMembers , [Measures].[Min] <= 213 AND [Measures].[Max] > 213 )
}
ON COLUMNS,
{
[Measures].[Shrinkage - Lookup Table], [Measures].[Wages - Lookup Table], [Measures].[Waste - Lookup Table]
} ON ROWS
FROM
[MyCube]
And result:
I hope this example will be useful

Resources