SQL Server - Selecting periods without changes in data - sql

What I am trying to do is to select periods of time where the rest of data in the table was stable based on one column and check was there a change in second column value in this period.
Table:
create table #stable_periods
(
[Date] date,
[Car_Reg] nvarchar(10),
[Internal_Damages] int,
[External_Damages] int
)
insert into #stable_periods
values ('2015-08-19', 'ABC123', 10, 10),
('2015-08-18', 'ABC123', 9, 10),
('2015-08-17', 'ABC123', 8, 9),
('2015-08-16', 'ABC123', 9, 9),
('2015-08-15', 'ABC123', 10, 10),
('2015-08-14', 'ABC123', 10, 10),
('2015-08-19', 'ABC456', 5, 3),
('2015-08-18', 'ABC456', 5, 4),
('2015-08-17', 'ABC456', 8, 4),
('2015-08-16', 'ABC456', 9, 4),
('2015-08-15', 'ABC456', 10, 10),
('2015-01-01', 'ABC123', 1, 1),
('2015-01-01', 'ABC456', NULL, NULL);
--select * from #stable_periods
-- Unfortunately I can’t post pictures yet but you get the point of how the table looks like
What I would like to receive is
Car_Reg FromDate ToDate External_Damages Have internal damages changed in this period?
ABC123 2015-08-18 2015-08-19 10 Yes
ABC123 2015-08-16 2015-08-17 9 Yes
ABC123 2015-08-14 2015-08-15 10 No
ABC123 2015-01-01 2015-01-01 1 No
ABC456 2015-08-19 2015-08-19 3 No
ABC456 2015-08-16 2015-08-18 4 Yes
ABC456 2015-08-15 2015-08-15 10 No
ABC456 2015-01-01 2015-01-01 NULL NULL
Basically to build period frames where [External_Damages] were constant and check did the [Internal_Damages] change in the same period (doesn't matter how many times).
I spend a lot of time trying but I am afraid that my level of abstraction thinking in much to low...
Will be great to see any suggestions.
Thanks,
Bartosz

I believe this is a form of Islands Problem.
Here is a solution using ROW_NUMBER and GROUP BY:
SQL Fiddle
WITH CTE AS(
SELECT *,
RN = DATEADD(DAY, - ROW_NUMBER() OVER(PARTITION BY Car_reg, External_Damages ORDER BY [Date]), [Date])
FROM #stable_periods
)
SELECT
Car_Reg,
FromDate = MIN([Date]),
ToDate = MAX([Date]) ,
External_Damages,
Change =
CASE
WHEN MAX(External_Damages) IS NULL THEN NULL
WHEN COUNT(DISTINCT Internal_Damages) > 1 THEN 'Yes'
ELSE 'No'
END
FROM CTE c
GROUP BY Car_Reg, External_Damages, RN
ORDER BY Car_Reg, ToDate DESC

Related

Function that returns MAX OR MIN dates based on ID count

I have a task in SQL Server where I need to return the RESULT_DATE column using ID, PRODUCT_ID and DATE columns. Task criteria:
If DATE column is filled once for each PRODUCT_ID then I need to return the only date (like for PRODUCT_ID 1 and 3). Let`s say its MIN date.
If DATE column is filled more than one time (like for PRODUCT_ID 2) then I need to return the next filled DATE row.
Data:
CREATE TABLE #temp (
ID INT,
PRODUCT_ID INT,
[DATE] DATETIME
)
INSERT #temp (ID, PRODUCT_ID, DATE) VALUES
(1, 1, '2008-04-24 00:00:00.000'),
(2, 1, NULL),
(3, 2, '2015-12-09 00:00:00.000'),
(4, 2, NULL),
(5, 2, NULL),
(6, 2, '2022-01-01 13:06:45.253'),
(7, 2, NULL),
(8, 2, '2022-01-19 13:06:45.253'),
(9, 3, '2018-04-25 00:00:00.000'),
(10,3, NULL),
(11,3, NULL)
ID
PRODUCT_ID
DATE
RESULT_DATE
1
1
2008-04-24 00:00:00.000
2008-04-24 00:00:00.000
2
1
NULL
2008-04-24 00:00:00.000
3
2
2015-12-09 00:00:00.000
2022-01-01 13:06:45.253
4
2
NULL
2022-01-01 13:06:45.253
5
2
NULL
2022-01-01 13:06:45.253
6
2
2022-01-01 13:06:45.253
2022-01-19 13:06:45.253
7
2
NULL
2022-01-19 13:06:45.253
8
2
2022-01-19 13:06:45.253
2022-01-19 13:06:45.253
9
3
2018-04-25 00:00:00.000
2018-04-25 00:00:00.000
10
3
NULL
2018-04-25 00:00:00.000
11
3
NULL
2018-04-25 00:00:00.000
I have tried different techniques, for example using LEAD and LAG SQL function combinations. The latest script: (However, still not working)
SELECT
COALESCE(DATE,
CAST(
SUBSTRING(
MAX(CAST(DATE AS BINARY(4)) + CAST(DATE AS BINARY(4))) OVER ( PARTITION BY PRODUCT_ID ORDER BY DATE ROWS UNBOUNDED PRECEDING)
,5,4)
AS INT)
) AS RESULT_DATE,
*
FROM TABLE
You can use a CTE, Select all rows with a non-NULL Date giving each a row_number, then use a second CTE to fetch all rows from the first CTE equivalent to the date with the largest row number per product_id that is less than 3. Finally join this CTE to the original table to supply the 2nd Date to each row:
Set Up
CREATE TABLE #temp (
ID INT,
PRODUCT_ID INT,
MyDATE DATETIME
)
INSERT #temp (ID, PRODUCT_ID, MyDate)
VALUES
(1, 1, '2008-04-24 00:00:00.000'),
(2, 1, NULL),
(3, 2, '2015-12-09 00:00:00.000'),
(4, 2, NULL),
(5, 2, NULL),
(6, 2, '2022-01-01 13:06:45.253'),
(7, 2, NULL),
(8, 2, '2022-01-19 13:06:45.253'),
(9, 3, '2018-04-25 00:00:00.000'),
(10,3, NULL),
(11,3, NULL);
Query:
;WITH CTE
AS
(
SELECT ID, Product_ID, MyDate,
ROW_NUMBER() OVER (PARTITION BY Product_ID ORDER BY Id) AS rn
from #temp
WHERE MyDate IS NOT NULL
),
CTE2
AS
(
SELECT *
FROM CTE C1
WHERE C1.rn < 3
AND
C1.rn =
(SELECT MAX(rn) FROM CTE WHERE Product_Id = C1.Product_Id AND rn<3)
)
SELECT T.Id, T.Product_Id, T.MyDate, C.MyDate As Result_date
FROM #temp T
INNER JOIN CTE2 C
ON T.Product_Id = C.Product_Id
ORDER BY T.Id;
Results:
Id Product_Id MyDate Result_Date
1 1 2008-04-24 00:00:00.000 2008-04-24 00:00:00.000
2 1 NULL 2008-04-24 00:00:00.000
3 2 2015-12-09 00:00:00.000 2022-01-01 13:06:45.253
4 2 NULL 2022-01-01 13:06:45.253
5 2 NULL 2022-01-01 13:06:45.253
6 2 2022-01-01 13:06:45.253 2022-01-01 13:06:45.253
7 2 NULL 2022-01-01 13:06:45.253
8 2 2022-01-19 13:06:45.253 2022-01-01 13:06:45.253
9 3 2018-04-25 00:00:00.000 2018-04-25 00:00:00.000
10 3 NULL 2018-04-25 00:00:00.000
11 3 NULL 2018-04-25 00:00:00.000

Group by on range of dates

I've read some topics about group by sequence but I could not figure out an solution for my problem.
I have a table (the name is ViewHistory) like this.
Tme Value
2020-07-22 09:30:00 1
2020-07-22 09:31:00 2
2020-07-22 09:32:00 3
2020-07-22 09:33:00 4
2020-07-22 09:34:00 5
2020-07-22 09:35:00 6
.
.
.
The data can grow indefinitely.
In this table, there are many records with 1 min TimeFrame.
I want to group on range of dataTime with timeFrame 2 min and Sum(value).
like this output:
TimeFrame SumData
09:30 1
09:32 5 -- sum of range 09:31_09:32
09:34 9 -- sum of range 09:33_09:34
.
.
.
How can I do this automatically, instead of using a:
WHERE Tme BETWEEN ('2020-07-22 09:31:00' AND '2020-07-22 09:32:00') and etc.
I am sure there is a simpler way, but its not coming to me right now.
declare #Test table (tme datetime2, [value] int)
insert into #Test (tme, [value])
values
('2020-07-22 09:30:00', 1),
('2020-07-22 09:31:00', 2),
('2020-07-22 09:32:00', 3),
('2020-07-22 09:33:00', 4),
('2020-07-22 09:34:00', 5),
('2020-07-22 09:35:00', 6);
with cte as (
select convert(date, tme) [date], datepart(hour, tme) [hour], datepart(minute,dateadd(minute, 1,tme)) / 2 [minute], sum([value]) [value]
from #Test
group by convert(date, tme), datepart(hour, tme), datepart(minute,dateadd(minute, 1,tme)) / 2
)
select convert(varchar(2),[hour]) + ':' + convert(varchar(2), [minute] * 2) [time], [value]
-- , dateadd(minute, [minute] * 2, dateadd(hour, [hour], convert(datetime2, [date]))) -- Entire date if desired
from cte;
Which gives:
time
value
9:30
1
9:32
5
9:34
9
9:36
6

SQL - Setting Value From Hierarchical Children

I am writing an application which gets task data from a project planning MS SQL table (let's call the table tasks). For simplicity the table fields can be thought of as follows:
task_id, parent_id, name, start_date, end_date
All parent tasks have NULL as start and end dates. Only the children (with no children of their own) have a start and end date.
I want to get the tasks data and in the process set the start date of each parent based upon the earliest start date of all the parent's children and recursive grandchildren and set the end date to be the latest end date of all the children and recursive grandchildren. Is this possible please?
I assume from your question that you use Sql Server. I think this is what you want. It is done with recursive common table expression. It begins with leaf children and goes up to top most parents:
DECLARE #t TABLE(id INT, pid INT, sd DATE, ed DATE)
INSERT INTO #t VALUES
(1, NULL, NULL, NULL),
(2, 1, NULL, NULL),
(3, 2, '20150201', '20150215'),
(4, 2, '20150101', '20150201'),
(5, 1, NULL, NULL),
(6, 5, '20150301', '20150401'),
(7, 1, NULL, NULL),
(8, 7, NULL, NULL),
(9, 8, '20140101', '20141230'),
(10, 8, '20140102', '20141231')
;WITH cte AS(
SELECT * FROM #t WHERE sd IS NOT NULL
UNION ALL
SELECT t.id, t.pid, c.sd, c.ed FROM #t t
JOIN cte c ON c.pid = t.id
)
SELECT id, pid, MIN(sd) AS sd, MAX(ed) AS ed
FROM cte
GROUP BY id, pid
ORDER BY id
Output:
id pid sd ed
1 NULL 2014-01-01 2015-04-01
2 1 2015-01-01 2015-02-15
3 2 2015-02-01 2015-02-15
4 2 2015-01-01 2015-02-01
5 1 2015-03-01 2015-04-01
6 5 2015-03-01 2015-04-01
7 1 2014-01-01 2014-12-31
8 7 2014-01-01 2014-12-31
9 8 2014-01-01 2014-12-30
10 8 2014-01-02 2014-12-31

Count Days using Dense Row function

I have a table which contains data for all the action performed on a particular object. The table below appears something as follows:
ActionId ProductName ProductPart ActionDate ActionStatusId
1 Bike abc123 3/24/2013 12:00:00 -4:00 7
2 Bike abc123 3/25/2013 12:00:00 -4:00 3
3 Bike abc123 3/25/2013 15:00:00 -4:00 1
4 Bike abc123 3/26/2013 16:00:00 -4:00 3
5 Bike abc123 3/26/2013 16:00:00 -4:00 3
6 Bike abc123 4/26/2013 15:00:00 -4:00 3
7 Bicycle def432 4/27/2013 12:00:00 -4:00 1
8 Bicycle def432 4/26/2013 12:00:00 -4:00 4
9 Bicycle def432 4/27/2013 12:00:00 -4:00 3
10 Bicycle def432 4/28/2013 12:00:00 -4:00 1
Now i need to get productname, productpart, laststatusid (only if it is 3 or 1), [No of days since statusid = 3]
So basically if last statusid based on last actiondate is not 3 or 1 i don't need that data, which i am able to get using row_number function.
But after that i need to count no.of days if statusid = 3. I don't need to count days if the last actionstatusid = 1.
But i have a problem in achieving it, because if the last statusid = 3 then i need to count no.of days not from that instance but the instance when it went to that status till date.
So, for productname Bike i should be getting following result:
ProductName ProductPart ActionStatusId [No. of Days Since Statusid = 3]
Bike abc123 3 34 (i.e. getdate() - 3/26/2013) as it went to statusid = 3 since 3/26/2013 and not taking just last actiondate
Bicycle dec432 1 -
I tried using row_number,dense_rank function but able to achieve it. Is there a way to achieve it?
Also, i am working with sql 2012.
Possible this be helpful for you -
DECLARE #temp TABLE
(
ActionId INT
, ProductName VARCHAR(50)
, ProductPart VARCHAR(50)
, ActionDate DATETIME
, ActionStatusId TINYINT
)
INSERT INTO #temp (ActionId, ProductName, ProductPart, ActionDate, ActionStatusId)
VALUES
(1, 'Bike', 'abc123', '20130324 12:00:00', 7),
(2, 'Bike', 'abc123', '20130325 12:00:00', 3),
(3, 'Bike', 'abc123', '20130325 15:00:00', 1),
(4, 'Bike', 'abc123', '20130326 16:00:00', 3),
(5, 'Bike', 'abc123', '20130326 16:00:00', 3),
(6, 'Bike', 'abc123', '20130426 15:00:00', 3),
(7, 'Bicycle', 'def432', '20130427 12:00:00', 1),
(8, 'Bicycle', 'def432', '20130426 12:00:00', 4),
(9, 'Bicycle', 'def432', '20130427 12:00:00', 3),
(10, 'Bicycle', 'def432', '20130428 12:00:00', 1)
DECLARE #Date DATE = GETDATE()
SELECT
ProductName
, ProductPart
, ActionStatusId
, CASE WHEN ActionStatusId = 3
THEN MAX(DATEDIFF(DAY, ActionDate, #Date))
ELSE 0
END
FROM #temp
WHERE ActionStatusId IN (1, 3)
GROUP BY
ProductName
, ProductPart
, ActionStatusId
Output:
ProductName ProductPart ActionStatusId Count
------------- ------------ -------------- -----------
Bicycle def432 1 0
Bicycle def432 3 2
Bike abc123 1 0
Bike abc123 3 35

How to find the difference between two dates in same column?

I have a table SO_STATUS that writes a record for each status change for a service order (we'll call the Service_Order_ID "Job_ID"). Job_ID references SERVICE_ORDER table. When the service order is initialized, a record is written for that status type of "open" (StatusType 2) which shows the datetime. Then another record is written in the status table for when it is "in progress" (StatusType 1). And also when the service order is "closed", another record written in the status table (StatusType 3). There are also other status types that may happen, but these are the most common. The data in the SO_STATUS table looks like this:
id Date Job_ID StatusTypeID EmployeeID
1 2012-01-01 09:05:00.000 51 2 5
2 2012-01-01 10:00:00.000 52 2 12
3 2012-01-01 10:01:00.000 51 1 5
4 2012-01-01 12:15:00.000 53 2 8
5 2012-01-01 12:16:00.000 51 3 5
6 2012-01-01 13:00:00.000 52 1 12
7 2012-01-01 14:00:00.000 52 3 12
8 2012-01-01 14:15:00.000 53 1 8
9 2012-01-01 15:00:00.000 54 2 11
10 2012-01-01 16:30:00.000 53 3 8
11 2012-01-01 15:00:00.000 54 1 11
12 2012-01-01 16:30:00.000 54 3 11
I need to be able to find the time elapsed between each status change of each Job_ID. Essentially, the duration of time spent from open to close for the job.
Output would look something like (EmployeeName would be referenced from the EMPLOYEE table):
Job_ID Duration EmployeeName
51 03:11:00 Kyle
52 04:00:00 Chris
53 04:15:00 Fred
54 01:30:00 John
How would I go about getting this type of output? Thank you.
Why dont you use:
SELECT DATEDIFF (anyparticularunit, ' 2012-01-01 09:05:00.000', ' 2012-01-01 15:00:00.000')
Go through following link for datediff:
http://msdn.microsoft.com/en-us/library/ms189794.aspx
Also follow this link to get different exmples:
http://www.sqlteam.com/forums/topic.asp?TOPIC_ID=56126
Hope you will put further where conditions.
This this one -
SET NOCOUNT ON;
DECLARE #duration TABLE
(
id BIGINT IDENTITY
, [date] DATETIME
, job_id INT
, [status] VARCHAR(10)
, employee_id INT
)
INSERT INTO #duration ([date], job_id, [status], employee_id)
VALUES
('2012-01-01 09:05:00.000', 51, 'open', 5),
('2012-01-01 10:00:00.000', 52, 'open', 12),
('2012-01-01 10:01:00.000', 51, 'inprogress', 5),
('2012-01-01 12:15:00.000', 53, 'open', 8),
('2012-01-01 12:16:00.000', 51, 'closed', 5),
('2012-01-01 13:00:00.000', 52, 'inprogress', 12),
('2012-01-01 14:00:00.000', 52, 'closed', 12),
('2012-01-01 14:15:00.000', 53, 'inprogress', 8),
('2012-01-01 15:00:00.000', 54, 'open', 11),
('2012-01-01 16:30:00.000', 53, 'closed', 8),
('2012-01-01 15:00:00.000', 54, 'inprogress', 11),
('2012-01-01 16:30:00.000', 54, 'closed', 11)
SELECT
job_id
, employee_id
, work_time = CONVERT(VARCHAR(12), MAX([date]) - MIN([date]), 114)
FROM #duration
GROUP BY job_id, employee_id
You can use DATEDIFF to return the count (signed integer) of the specified datepart boundaries crossed between the specified startdate and enddate (see http://msdn.microsoft.com/en-us/library/ms189794.aspx)
SELECT Job_ID,
DATEDIFF(day, (SELECT MIN(Date) FROM YOUTABLE WHERE Job_ID=k.Job_ID),(SELECT MAX(Date) FROM YOUTABLE WHERE Job_ID=k.Job_ID)),
(SELECT EmployeeName FROM EmployeeTABLE WHERE EmployeeID=k.EmployeeID)) FROM YOUTABLE k
If your database is Oracle, you can do like this
SELECT DISTINCT JOB_ID, MAX(DATE) OVER(PARTITION BY JOB_ID)-MIN(DATE) OVER(PARTITION BY JOB_ID) AS Duration FROM TA JOIN TB .....
I have created some custom code to create dat and time difference, using datediff function and dividing with certain numbers to generate hours, minutes and seconds:
SELECT
Job_ID,
CAST(DATEDIFF(second, MIN(Date), MAX(Date)) / 3600 AS VARCHAR)
+ ':' + CAST((DATEDIFF(second, MIN(Date), MAX(Date)) % 3600) / 60 AS VARCHAR)
+ ':' + CAST(((DATEDIFF(second, MIN(Date), MAX(Date)) % 3600) % 60) AS VARCHAR)
FROM YOUTABLE
GROUP BY Job_ID
Try query given below:
Select t1.Job_ID,
Convert(varchar(5),DateDiff(HH,Min(t1.JobDate),tbl.MaxDate))+' : '+convert(varchar(5),DateDiff(s,Min(t1.JobDate),tbl.MaxDate) % 3600/60)+' : '+Convert(varchar(5),DateDiff(s,Min(t1.JobDate),
tbl.MaxDate) % 60) MinDate,t1.EmployeeName From SO_STATUS t1
Inner join (Select Max(JobDate) MaxDate, job_id From SO_STATUS Group By Job_Id)tbl on t1.Job_ID=tbl.Job_ID
Inner Join EMPLOYEE e On e.EmployeeID=t1.EmployeeID
Group By t1.EmployeeName,tbl.MaxDate,t1.Job_ID
Order By t1.Job_ID
Difference between two dates of different tables which has datetime format.
SELECT t1.Column_Names,
CONVERT(varchar(10),t1.CreatedOn,103)
AS CreatedOn FROM table1 t1 INNER JOIN table2 t2
ON t1.id = t2.id
WHERE CAST (t1.CreatedOn as Date)
BETWEEN #fromdate and #todate.
i have taken t1.CreatedOn as my table attribute which holds date.
#fromdate and #todate to pass dates.