MIN MAX query with a twist - sql

I need to get the MIN and MAX dates for volume but I need to group it based on volume and not all the volume of same amount....
Basically, I have daily volume and dates for those daily volume. I need to be able to get the MIN Date as "to" and MAX date as "from" for a set of volume.
Note that the volume can traverse dates and then break and then have a new set of dates for the same volume.
Hopefully the screenshots below do a better job explaining than I can. I know how to do this via code.. but was wondering if the same was possible with SQL.
Please note that the SQL will be called from within an application and I can't insert into a temp table to get my end result data set...
Here is the raw data that I am querying using select *:
Here is what I ultimately want:
The query that I am running gives me the MIN and MAX for all the occurrences of the volume 1100. I want it split based on the break between dates as shown in the End result screenshot....
Here is my SQL:
SELECT daily_volume, MIN(volume_date) AS min_date, MAX(volume_date) AS max_date, ins_num
FROM daily_volume
WHERE ins_num = 3854439
GROUP BY daily_volume, ins_num

The following was written for sql server, but it should work for other databases (sqlfiddle):
with DatesMinMax as
(
select
volume_date,
daily_volume,
isnull
(
(
select top 1 d2.volume_date
from daily_volume d2
where d.volume_date > d2.volume_date and d.daily_volume <> d2.daily_volume
order by d2.volume_date desc
)
, '1753-01-01'
) as min_date,
isnull
(
(
select top 1 d2.volume_date
from daily_volume d2
where d.volume_date < d2.volume_date and d.daily_volume <> d2.daily_volume
order by d2.volume_date
)
, '9999-12-31'
) as max_date
from daily_volume d
),
DatesFromTo as
(
select d1.daily_volume as qty,
(select min(d2.volume_date)
from DatesMinMax d2
where d2.volume_date > d1.min_date and d2.volume_date < d1.max_date
) as [from],
(select max(d2.volume_date)
from DatesMinMax d2
where d2.volume_date > d1.min_date and d2.volume_date < d1.max_date
) as [to]
from DatesMinMax d1
)
select distinct
qty,
[from],
[to]
from DatesFromTo
order by [from]
DatesMinMax is used to get the first date that had a different volume than the current volume
DatesFromTo is used to get the date range for each row

Related

Calculate time span between two specific statuses on the database for each ID

I have a table on the database that contains statuses updated on each vehicle I have, I want to calculate how many days each vehicle spends time between two specific statuses 'Maintenance' and 'Read'.
My table looks something like this
and I want to result to be like this, only show the number of days a vehicle spends in maintenance before becoming ready on a specific day
The code I written looks like this
drop table if exists #temps1
select
VehicleId,
json_value(VehiclesHistoryStatusID.text,'$.en') as VehiclesHistoryStatus,
VehiclesHistory.CreationTime,
datediff(day, VehiclesHistory.CreationTime ,
lead(VehiclesHistory.CreationTime ) over (order by VehiclesHistory.CreationTime ) ) as days,
lag(json_value(VehiclesHistoryStatusID.text,'$.en')) over (order by VehiclesHistory.CreationTime) as PrevStatus,
case
when (lag(json_value(VehiclesHistoryStatusID.text,'$.en')) over (order by VehiclesHistory.CreationTime) <> json_value(VehiclesHistoryStatusID.text,'$.en')) THEN datediff(day, VehiclesHistory.CreationTime , (lag(VehiclesHistory.CreationTime ) over (order by VehiclesHistory.CreationTime ))) else 0 end as testing
into #temps1
from fleet.VehicleHistory VehiclesHistory
left join Fleet.Lookups as VehiclesHistoryStatusID on VehiclesHistoryStatusID.Id = VehiclesHistory.StatusId
where (year(VehiclesHistory.CreationTime) > 2021 and (VehiclesHistory.StatusId = 140 Or VehiclesHistory.StatusId = 144) )
group by VehiclesHistory.VehicleId ,VehiclesHistory.CreationTime , VehiclesHistoryStatusID.text
order by VehicleId desc
drop table if exists #temps2
select * into #temps2 from #temps1 where testing <> 0
select * from #temps2
Try this
SELECT innerQ.VehichleID,innerQ.CreationDate,innerQ.Status
,SUM(DATEDIFF(DAY,innerQ.PrevMaintenance,innerQ.CreationDate)) AS DayDuration
FROM
(
SELECT t1.VehichleID,t1.CreationDate,t1.Status,
(SELECT top(1) t2.CreationDate FROM dbo.Test t2
WHERE t1.VehichleID=t2.VehichleID
AND t2.CreationDate<t1.CreationDate
AND t2.Status='Maintenance'
ORDER BY t2.CreationDate Desc) AS PrevMaintenance
FROM
dbo.Test t1 WHERE t1.Status='Ready'
) innerQ
WHERE innerQ.PrevMaintenance IS NOT NULL
GROUP BY innerQ.VehichleID,innerQ.CreationDate,innerQ.Status
In this query first we are finding the most recent 'maintenance' date before each 'ready' date in the inner most query (if exists). Then calculate the time span with DATEDIFF and sum all this spans for each vehicle.

SQL Rowwise comparison between groups

Question
The following is a snippet of my data:
Create Table Emps(person VARCHAR(50), started DATE, stopped DATE);
Insert Into Emps Values
('p1','2015-10-10','2016-10-10'),
('p1','2016-10-11','2017-10-11'),
('p1','2017-10-12','2018-10-13'),
('p2','2019-11-13','2019-11-13'),
('p2','2019-11-14','2020-10-14'),
('p3','2020-07-15','2021-08-15'),
('p3','2021-08-16','2022-08-16');
db<>fiddle.
I want to use T-SQL to get a count of how many persons fulfil the following criteria at least once - multiples should also count as one:
For a person:
One of the dates in 'started' (say s1) is larger than at least one of the dates in 'ended' (say e1)
s1 and e1 are in the same year, to be set manually - e.g. '2021-01-01' until '2022-01-01'
Example expected response
If I put the date range '2016-01-01' until '2017-01-01' somewhere in a WHERE / HAVING clause, the output should be 1 as only p1 has both a start date and an end date that fall in 2016 where the start date is larger than the end date:
s1 = '2016-10-11', and e1 = '2016-10-10'.
Why can't I do this myself
The reason I'm stuck is that I don't know how to do this rowwise comparison between groups. The question requires comparing values across columns (start with end) across rows, within a person ID.
Use conditional aggregation to get the maximum start date and the minimum stop date in the given range.
select person
from emps
group by person
having max(case when started >= '2016-01-01' and started < '2017-01-01'
then started end) >
min(case when stopped >= '2016-01-01' and stopped < '2017-01-01'
then stopped end);
Demo: https://dbfiddle.uk/?rdbms=sqlserver_2019&fiddle=45adb153fcac9ce72708f1283cac7833
I would choose to use a self-outer-join with an exists correlation, it should be pretty much the most performant, all things being equal.
select Count(*)
from emps e
where exists (
select * from emps e2
where e2.person = e.person
and e2.stopped > e.started
and e.started between '20160101' and '20170101'
and e2.started between '20160101' and '20170101'
);
You said you plan to set the dates manually, so this works where we set the start date in one CTE, and the end date in another CTE. Then we calculate the min/max for each, and use that criteria in the query where statement.
with min_max_start as (
select person,
min(started) as min_start, --obsolete
max(started) as max_start
from emps
where started >= '2016-01-01'
group by person
),
min_max_end as (
select person,
min(stopped) as min_stop,
max(stopped) as max_stop --obsolete
from emps
where stopped < '2017-01-01'
group by person
)
select count(distinct e.person)
from emps e
join min_max_start mms
on e.person = mms.person
join min_max_end mme
on e.person = mme.person
where mms.max_start> mme.min_stop
Output: 1
Try the following:
With CTE as
(
Select D.person, D.started, T.stopped,
case
when Year(D.started) = Year(T.stopped) and D.started > T.stopped
then 1
else 0
end as chk
From
(Select person, started From Emps Where started >= '2016-01-01') D
Join
(Select person, stopped From Emps Where stopped <= '2017-01-01') T
On D.person = T.person
)
Select Count(Distinct person) as CNT
From CTE
Where chk = 1;
To get the employee list who met the criteria use the following on the CTE instead of the above Select Count... query:
Select person, started, stopped
From CTE
Where chk = 1;
See a demo from db<>fiddle.

SQL for looping the date and summarize data

I need to summarize by customer having specific product from 2018-1-1 until today if cdate is within activationdt and next_activationdt. When next_activationdt is empty that means it is still in use and not cancelled or changed.
For example 2018-1-1, Mary and Alan are using product A.
I could not figure out a good way of running all the dates together.
I need to run the code on SQL workbench pulling data from AWS Redshift database.
You need a way to generate the dates. Perhaps you have a calendar table, or you can use something like this:
with dates as (
select dateadd(day, row_number() over (), '2018-01-01'::date) as dte
from t
limit 365 -- assumes table has at least 365 rows
)
select d.dte,
sum( (product = 'A')::int ) as num_a,
sum( (product = 'B')::int ) as num_b,
sum( (product = 'C')::int ) as num_c,
sum( (product = 'D')::int ) as num_d
from dates d left join
t
on d.dte >= t.activationdate and
(d.dte < t.nextactivationdate or t.nextactivationdate is null)
group by d.dte
order by d.dte;

First date when certain condition was met

I'm trying to find a first date when a condition was met. So the logic is below:
use
[AdventureWorksDW2012]
go
;WITH sales AS (
select d.OrderDateKey,
SalesAmount = SUM(d.SalesAmount)
from [dbo].[FactInternetSales] d
group by d.OrderDateKey
having SUM(d.SalesAmount)>10000
)
select FirstOrderDateKey = MIN(OrderDateKey)
from sales
The only problem is that in my data is too complex and too huge to calculate value for each date and then choose the min date when the condition is met. Is there any quick way of finding first date when Internet sales amount exceeded 10000? Is there some kind of loop required?
You can do it in single statement also. The performance can be improved with this if you have proper index on orderdatekey.
select MIN(s.OrderDateKey) as FirstOrderDateKey
from [dbo].[FactInternetSales] d
group by d.OrderDateKey
having SUM(d.SalesAmount)>10000
SELECT TOP 1
d.OrderDateKey
,S.RunningTotal
FROM [dbo].[FactInternetSales] d
CROSS APPLY ( SELECT SUM(SalesAmount) AS RunningTotal
FROM [dbo].[FactInternetSales]
WHERE OrderDateKey <= d.OrderDateKey
) S
WHERE S.RunningTotal < -- Condition
ORDER BY d.OrderDateKey DESC

How to find the row where the sum of all values in a column reaches a specified value?

Given data in a table with the following schema:
CREATE TABLE purchases (timestamp DATETIME, quantity INT)
I would like to find the point in time (i.e. the timestamp of the row) where the sum of the values in the quantity column passed a certain threshold value.
This is in MS SQL Server, and ideally I'd like to avoid using a cursor if possible.
SELECT timestamp, SUM(quantity)
FROM purchases
GROUP BY timestamp
HAVING SUM(quantity) > someValue
Or if it is a Running Sum
SELECT a1.timestamp
FROM purchases a1, purchases a2
WHERE a1.quantity >= a2.quantity or (a1.quantity=a2.quantity and a1.timestamp = a2.timestamp)
GROUP BY a1.timestamp, a1.quantity
having SUM(a2.quantity) >= someValue
ORDER BY a1.timestamp ASC
LIMIT 1
You could get the smallest timestamp where the sum of the previous values is larger than the threshold:
select min(timestamp)
from purchases p
where (
select sum(x.quantity)
from purchases x
where x.timestamp < p.timestamp
) > #threshold
However, this is not a very efficient query, so it might be better to use a cursor after all.
In SQL Server 2005+ you could try this:
;WITH numbered AS (
SELECT
timestamp,
quantity,
rownum = ROW_NUMBER() OVER (ORDER BY timestamp)
FROM purchases
),
recursive AS (
SELECT
timestamp,
quantity,
rownum,
runningsum = quantity,
passed = CASE WHEN n.quantity < #threshold THEN 0 ELSE 1 END
FROM numbered
UNION ALL
SELECT
n.timestamp,
n.quantity,
n.rownum,
runningsum = n.quantity + r.runningsum,
passed = CASE WHEN n.quantity + r.runningsum < #threshold THEN 0 ELSE 1 END
FROM numbered n
INNER JOIN recursive r ON n.rownum = r.rownum + 1
)
SELECT MIN(timestamp)
FROM recursive
WHERE passed = 1
Basically, same as #Guffa's solution, only makes use of CTEs to avoid the need of triangular join.