Calculate the time difference in the same row dynamically - sql

Is there any way to calculate the time difference in SQL between rows within the same column based on the 'DOWN' and 'UP' values like this:
There are 3 scenarios(that I'm aware of):
Yellow, Orange and Green: there is a state_id 2(down) and after that a state_id 5(up), so the time difference needs to be calculated between the two rows;
Blue: there are multiple state_id 2(down) and after that one state_id 5(up), so the time difference needs to be calculated between the first row and last row;
Red: there is only a state_id 2(down) because it is still down with any update, so the time difference needs to be calculated till the end of the month.
I hope you can help me out.

Was first considering to use LAG for this.
But using a cummulative SUM, and the window version of MIN works also for more than 2 DOWN's:
-- test reference data
declare #State table (id int, state varchar(4));
insert into #State (id, state) values
(2,'DOWN'),
(5,'UP')
-- test data, using a table variable
declare #AlertState table (alert_id int identity(1,1), host_id int, state_time datetime, state_id int);
insert into #AlertState (host_id, state_time, state_id) values
(119, GetDate()-0.32, 2),
(119, GetDate()-0.31, 5),
(119, GetDate()-0.24, 2),
(119, GetDate()-0.23, 2),
(119, GetDate()-0.22, 2),
(119, GetDate()-0.21, 5),
(119, GetDate()-0.15, 5),
(119, GetDate()-0.11, 2);
-- The query
select alert_id, host_id, state_time, state_id,
diff_min = (
case
when state_id = 5 then
datediff(minute, min(state_time) over (partition by host_id, stategroup), state_time)
when state_id = 2 and stategroup is null then
datediff(minute, state_time, cast(EOMONTH(GetDate()) as datetime)+1)
end),
s.state
from (
select alert_id, host_id, state_time, state_id,
sum(case state_id when 5 then 1 end) over (partition by host_id order by state_time desc) as stategroup
from #AlertState
where state_id in (2,5)
) q
left join #State s on s.id = q.state_id
order by state_time, alert_id;

The way I did this before is
Select a.state_time as downtime,
(
select min(inner.state_time) from tablename downentry where
inner.state_time > outer.state_time and downentry.state='UP'
) as uptime
from tablename upentry
where state = 'DOWN'
Then you need to find the datediff between them, and if uptime is null, the datediff between downtime and 'endofmonth'
It's potentially quite poor performing, so I always wrote the answer out to a data warehouse, but think it gives the results you're asking for.

SQL2012+
You could try following solution:
SELECT y.group_id, host_id = MIN(host_id), start_time = MIN(state_time), end_time = MAX(state_time), diff_minute = DATEDIFF(MINUTE, MIN(state_time), MAX(state_time))
FROM (
SELECT *, group_id = SUM(x.new_group_start) OVER(ORDER BY x.host_id, x.state_time)
FROM (
SELECT *, new_group_start = IIF(a.state_id = 'DOWN' AND ISNULL(LAG(a.state_id) OVER(ORDER BY a.host_id, a.state_time), 'UP') = 'UP', 1, 0)
FROM #Alerts a
) x
) y
GROUP BY y.group_id
ORDER BY y.group_id
Demo

Related

Select 75% of records to rename, based on column sum

I have a scenario where I need to rename a value in one column, based on another column's total. Example table below with basic math, to express concept. I'd like to change the value in 'Condition' column to "Used" for the rows that make up 70% of the 'Revenue' column (which in this example would be 7 rows). The other 30% would be renamed to "New" (the remaining 3 rows). No other specific logic required.
I found that the approach mentioned here works for selecting the percentage of rows required
Select Rows who's Sum Value = 80% of the Total
I suppose I could create two temporary tables, rename the column fields in each respective table, and then join together. Curious if there is an easier way?
Current Table:
Source
Condition
Revenue
A
Old
1
B
New
1
C
Old
1
D
New
1
E
Old
1
F
New
1
G
Old
1
H
New
1
I
Old
1
J
New
1
New Table:
Source
Condition
Revenue
A
Used
1
B
Used
1
C
Used
1
D
Used
1
E
Used
1
F
Used
1
G
Used
1
H
New
1
I
New
1
J
New
1
You could do this with two updates. The first would update the entire table. The second would update the first 70%.
First we need sample data in a table. I used a table variable here but you would use your actual table.
declare #Something table
(
Source char(1)
, Condition varchar(10)
, Revenue int
)
insert #Something values
('A', 'Old', 1)
, ('B', 'New', 1)
, ('C', 'Old', 1)
, ('D', 'New', 1)
, ('E', 'Old', 1)
, ('F', 'New', 1)
, ('G', 'Old', 1)
, ('H', 'New', 1)
, ('I', 'Old', 1)
, ('J', 'New', 1)
select *
from #Something;
Next simply update the entire table.
update #Something
set Condition = 'New';
Last step is to update the first 70%. An easy to do this is to use a cte to select the first 70% and then update the cte.
with Top70 as
(
select top 70 percent *
from #Something
order by Source
)
update Top70
set Condition = 'Used';
Here is the final output.
select *
from #Something;
--EDIT--
Now understanding we need a running total you could do something like this.
select *
, case when sum(Revenue) over(order by Source) > (sum(Revenue) over() * .7) then 'New' else 'Old' end
from #Something
You can select/mark the 70% and 30% records using this query :
with cte as (
SELECT *, SUM(revenue) OVER(ORDER BY source) AS cumulative_revenue, SUM(revenue) OVER() as total
FROM mytable t
)
select Source, iif((cumulative_revenue + 0.0) /total <= 0.7, 'Used', 'New') as Condition, revenue, cumulative_revenue, (cumulative_revenue + 0.0) /total as perc
from cte
Demo here
You could chain a couple of CTEs to run the UPDATE
DROP TABLE IF EXISTS #t
CREATE TABLE #t([Source] VARCHAR(10), [Condition] VARCHAR(10), Revenue INT)
INSERT INTO #t([Source], [Condition], [Revenue])
values
('A', 'Old', 1)
,('B', 'New', 1)
,('C', 'Old', 1)
,('D', 'New', 1)
,('E', 'Old', 1)
,('F', 'New', 1)
,('G', 'Old', 1)
,('H', 'New', 1)
,('I', 'Old', 1)
,('J', 'New', 1)
;WITH cte AS (
SELECT *, SUM( Revenue) OVER (ORDER BY Source) ACC
FROM #t
), cte2 as(
SELECT MAX(acc)*1. TotalRevenue FROM cte
)
UPDATE cte
SET Condition = CASE WHEN Acc / TotalRevenue <= .7 THEN 'Used' ELSE 'New' END
FROM cte
CROSS APPLY (SELECT TotalRevenue FROM cte2) ca
SELECT * FROM #t

Select first occurrence of value after last occurrence of other value in SQL

I have a requirement to find the current operation of a part. The table I have to get this information from lists operation statuses of complete (1) or 0. So the table typically looks like:
ID Operation Status
1 100 1
2 200 1
3 250 1
4 300 0
5 350 0
So in this case Operation 300 is the current op which I get using MIN(Operation) WHERE Status = 0.
However, some cases have appeared where some operations are skipped which would look like:
ID Operation Status
1 100 1
2 200 0
3 250 1
4 300 0
5 350 0
So in this case the current operation is still Operation 300 but MIN(Operation) doesn't work. What I need is the first occurrence of the row where Status = 0 that follows the last occurrence of a Status = 1 row. How could I achieve this?
Edit: Also have to consider the case where all operations are Status 0, where the correct result would be the first row (Operation 100)
This will give you the entire row to work with:
DECLARE #MyTable TABLE (
ID INT,
Operation INT,
Status BIT
);
INSERT INTO #MyTable VALUES
(1, 100, 1)
,(2, 200, 0)
,(3, 250, 1)
,(4, 300, 0)
,(5, 350, 0)
;
WITH MaxOperation AS (
SELECT MAX(x.Operation) AS MaxOperation
FROM #MyTable x
WHERE x.Status = 1
)
SELECT TOP 1 t.*
FROM #MyTable t
CROSS APPLY (SELECT MaxOperation FROM MaxOperation) x
WHERE t.Operation > x.MaxOperation
OR x.MaxOperation IS NULL
ORDER BY t.Operation
This will result in:
ID Operation Status
----------- ----------- ------
4 300 0
It will also produce this if all the Status values are 0:
ID Operation Status
----------- ----------- ------
1 100 0
I'm sure there is a clever window function way to do it, but in vanilla sql this is the idea
SELECT MIN(Operation)
FROM SOME_TABLE
WHERE Operation >
( SELECT MAX(Operation)
FROM SOME_TABLE
WHERE status = 1
)
As indicated by user Error_2646, a good way would be something like
select
min(ID)
from
[YourTable]
where
ID > (select max(ID) from [YourTable] where Status = 1)
I hope this answer will give you the correct answer. If you can add the expected output in a image it is more easy to identify what you need. Please add schema and data when, so that it is easy for user to put their solutions.
Schema and data I used:
(
ID INT
,operation INT
,Status INT
)
insert into Occurances values(1,100,1)
insert into Occurances values(2,200,0)
insert into Occurances values(1,250,1)
insert into Occurances values(1,300,0)
insert into Occurances values(1,350,0)
SELECT *
FROM
(
SELECT
Rank() OVER ( ORDER BY operation) AS [rank]
,MIN([operation]) AS [min]
,id
,[status]
FROM Occurances
WHERE [Status]= 0
GROUP BY id
,[status]
,operation
UNION
SELECT
Rank() OVER ( ORDER BY operation DESC) AS [rank]
,MAX([operation]) AS [min]
,id
,[status]
FROM Occurances
WHERE [Status]= 1
GROUP BY id
,[status]
,operation
) AS A
WHERE A.[rank]= 1
This is the answer I am getting:
You can do this very efficiently with a window function:
SELECT TOP (1) *
FROM (
SELECT *, LEAD(Status) OVER (ORDER BY Operation DESC) AS PreviousStatus
FROM myTable
)
WHERE Status = 0 AND PreviousStatus = 1
ORDER BY Operation DESC
Try this:
DECLARE #Table TABLE
(
ID int
, Operation int
, [Status] bit
)
;
INSERT INTO #Table (ID, Operation, [Status])
VALUES
(1, 100, 1)
, (2, 200, 0)
, (3, 250, 1)
, (4, 300, 1)
, (5, 350, 0)
;
SELECT TOP 1 T.*
FROM #Table T
WHERE T.[Status] = 0
AND T.ID > (
SELECT TOP 1 T.ID
FROM #Table T
WHERE T.[Status] = 1
ORDER BY ID DESC
)
ORDER BY ID

Repeat previous value of the column in SQL

I want to know if is possible to repeat the value of the current row until I found another one and then repeat that
WHEN DATE=201903 always is going to be the value of CODE and I want to repeat that value until I found a different value an repeat that.
CREATE TABLE ForgeRock
(
[id] VARCHAR(13),
[code] VARCHAR(57),
[date] VARCHAR(13),
[code2] VARCHAR(7)
);
INSERT INTO ForgeRock ([id], [code], [date], [code2])
VALUES (1, 21, 201903, 0),
(1, 21, 201902, 0),
(1, 21, 201901, 0),
(1, 21, 201812, 0),
(1, 21, 201811, 0),
(1, 21, 201810, 22),
(1, 21, 201809, 0),
(1, 21, 201808, 0),
(1, 21, 201807, 0);
SELECT
*,
result = (CASE WHEN date = 201903 THEN code
WHEN date <> 201903 AND code2 = 0 THEN code
ELSE code2
END)
FROM
ForgeRock
but the number 22 just repeat once and I want that from the moment 22 appears use that number always something like this
Yes, you can do this. The ideal way would be to use LAG() with IGNORE NULLs, but SQL Server does not support that. So here is another method:
select fr.*,
(case when grp = 0 then code else max(code2) over (partition by grp) end) as result
from (select fr.*,
sum(case when code2 <> 0 then 1 else 0 end) over (order by date desc) as grp
from ForgeRock fr
) fr
order by date desc;
This assigns a "group" to rows for each code2 value. Every new code2 generates a new group. Then, we can use max() over this group to spread the value over all the rows in the group.
Finally, the outer query chooses between code and code2.
Here is a db<>fiddle.

SQL Joining Query How To

CREATE TABLE interview (uniqueID int identity(1,1),
date datetime,
recordtype int,
amount numeric(18, 4))
INSERT INTO interview values('6/30/13', 1, 27.95)
INSERT INTO interview values('5/20/13', 1, 21.85)
INSERT INTO interview values('5/22/13', 2, 27.90)
INSERT INTO interview values('12/11/12', 2, 23.95)
INSERT INTO interview values('6/13/13', 3, 24.90)
INSERT INTO interview values('6/30/13', 2, 27.95)
INSERT INTO interview values('5/20/13', 2, 21.85)
INSERT INTO interview values('5/22/13', 1, 27.90)
INSERT INTO interview values('12/11/12',1, 23.95)
INSERT INTO interview values('6/13/13', 3, 24.90)
INSERT INTO interview values('6/30/13', 3, 27.95)
INSERT INTO interview values('5/20/13', 3, 21.85)
INSERT INTO interview values('5/22/13', 2, 27.90)
INSERT INTO interview values('12/11/12', 1, 23.95)
INSERT INTO interview values('6/13/13', 1, 24.90)
How to get the following result? What would the query look like?
I was only able to get a partial to work, but my answer is not correct. I need to
somehow join the queries.
select distinct date, count(RecordType)as Count_unique1
from interview
where RecordType = '1'
group by date
select distinct date, count(RecordType)as Count_unique2
from interview
where RecordType = '2'
group by date
select distinct date, count(RecordType)as Count_unique3
from interview
where RecordType = '3'
group by date
select
date,
sum(case when RecordType = '1' then 1 else 0 end) as Count_unique1,
sum(case when RecordType = '2' then 1 else 0 end) as Count_unique2,
sum(case when RecordType = '3' then 1 else 0 end) as Count_unique3
from interview
group by date
Also in MSSQL you can use PIVOT
SELECT date, [1] AS Count_unique1,
[2] AS Count_unique2,
[3] AS Count_unique3
FROM (SELECT date,recordtype,amount FROM interview) p
PIVOT
(
COUNT (amount)
FOR recordtype IN ([1], [2], [3])
) AS pvt
ORDER BY pvt.date;
SQLFiddle demo
If RecordType is 1,2 and 3 in all cases this will be enough.
select date,
sum(RecordType = '1') as Count_unique1,
sum(RecordType = '2') as Count_unique2,
sum(RecordType = '3') as Count_unique3
from interview
group by date

ms sql server 2012 - deducing active rows for a time in the past using effective from and effective to dates

I have a table containing rows of pricing.
Each row has 3 timestamps, insert_date, effective_from and effective_to. (example row below)
I have a query that will show me instances where rows where the price has been wrongly entered as zero.
What I'd like to be able to do, is know what the price field was BEFORE the last update (I didn't want these to be updated to zero)
The query I have now is:
SELECT *
FROM company..database_pricing_fixed
WHERE price = 0
AND status = 1
ORDER BY insert_date DESC
the table structure looks like this:
POSTCODE_KEY EXP_ID POSTCODE SOURCE_POSTCODE_DISTRICT DEST_POSTCODE_DISTRICT PRICE INSERT_DATE EFFECTIVE_FROM EFFECTIVE_TO ACCOUNT_ID STATUS
W11_TW6 3XA 75 TW6 3XA W11 NULL 0 24/12/2013 01:32 24/12/2013 01:31 24/12/2013 03:41 32523 1
With SQL Server 2012, you can use LEAD() to get the second closest price if the latest one inserted is 0;
WITH cte AS (
SELECT POSTCODE_KEY, EXP_ID, POSTCODE, SOURCE_POSTCODE_DISTRICT,
DEST_POSTCODE_DISTRICT,
CASE WHEN PRICE = 0
THEN LEAD(PRICE) OVER (PARTITION BY POSTCODE_KEY
ORDER BY INSERT_DATE DESC)
ELSE PRICE
END PRICE,
INSERT_DATE, EFFECTIVE_FROM, EFFECTIVE_TO, ACCOUNT_ID,STATUS,
ROW_NUMBER() OVER (PARTITION BY POSTCODE_KEY ORDER BY INSERT_DATE DESC) rn
FROM DATABASE_pricing_fixed
)
SELECT * FROM cte WHERE rn=1
An SQLfiddle to test with.
I modified the DDL in Joachim's SQL Fiddle a bit and utilized this structure to find a SQL Server 2008 solution.
A_PRICE and B_PRICE are the original PRICE columns in each table of the LEFT JOIN. CALCULATED_PRICE is the price you want to use and is the result of determining if table A.PRICE is 0 and falling back to B.PRICE if it is. It furthermore falls back to 0 if a.PRICE is 0 and there is no preceding insert to reference. You can get rid of A_PRICE and B_PRICE.
SQL Fiddle
SEQ_A and SEQ_B are debugging values I used in my coding to help visualize the dataset. You can get rid of those.
Using this DDL:
CREATE TABLE DATABASE_pricing_fixed
([POSTCODE_KEY] varchar(11), [EXP_ID] int, [POSTCODE] varchar(7), [SOURCE_POSTCODE_DISTRICT] varchar(3), [DEST_POSTCODE_DISTRICT] varchar(4), [PRICE] int, [INSERT_DATE] varchar(16), [EFFECTIVE_FROM] varchar(16), [EFFECTIVE_TO] varchar(16), [ACCOUNT_ID] int, [STATUS] int)
;
INSERT INTO DATABASE_pricing_fixed
([POSTCODE_KEY], [EXP_ID], [POSTCODE], [SOURCE_POSTCODE_DISTRICT], [DEST_POSTCODE_DISTRICT], [PRICE], [INSERT_DATE], [EFFECTIVE_FROM], [EFFECTIVE_TO], [ACCOUNT_ID], [STATUS])
VALUES
('W11_TW6 3XA', 75, 'TW6 3XA', 'W11', NULL, 0, '24/12/2013 01:34', '24/12/2013 01:31', '24/12/2013 03:41', 32523, 1)
;
INSERT INTO DATABASE_pricing_fixed
([POSTCODE_KEY], [EXP_ID], [POSTCODE], [SOURCE_POSTCODE_DISTRICT], [DEST_POSTCODE_DISTRICT], [PRICE], [INSERT_DATE], [EFFECTIVE_FROM], [EFFECTIVE_TO], [ACCOUNT_ID], [STATUS])
VALUES
('W11_TW6 3XB', 75, 'TW6 3XA', 'W11', NULL, 47, '22/12/2013 01:32', '24/12/2013 01:31', '24/12/2013 03:41', 32523, 1)
;
INSERT INTO DATABASE_pricing_fixed
([POSTCODE_KEY], [EXP_ID], [POSTCODE], [SOURCE_POSTCODE_DISTRICT], [DEST_POSTCODE_DISTRICT], [PRICE], [INSERT_DATE], [EFFECTIVE_FROM], [EFFECTIVE_TO], [ACCOUNT_ID], [STATUS])
VALUES
('W11_TW6 3XC', 75, 'TW6 3XA', 'W11', NULL, 33, '24/12/2013 01:32', '24/12/2013 01:31', '24/12/2013 03:41', 32523, 1)
;
Solution:
WITH d1 AS (
SELECT
*
,Seq = ROW_NUMBER() OVER (PARTITION BY POSTCODE ORDER BY INSERT_DATE)
FROM DATABASE_pricing_fixed
)
SELECT a.POSTCODE_KEY
,a.EXP_ID
,a.POSTCODE
,a.SOURCE_POSTCODE_DISTRICT
,a.DEST_POSTCODE_DISTRICT
,A_PRICE = a.PRICE
,B_PRICE = b.PRICE
,CALCULATED_PRICE = COALESCE(NULLIF(a.PRICE, 0), b.PRICE, 0)
,a.INSERT_DATE
,a.EFFECTIVE_FROM
,a.EFFECTIVE_TO
,a.ACCOUNT_ID
,a.STATUS
,A_SEQ = a.Seq
,B_SEQ = b.Seq
FROM d1 a
LEFT JOIN d1 b
ON a.PRICE = 0
AND b.POSTCODE = a.POSTCODE
AND a.Seq - b.Seq = 1