SQL Server how to retrieve all numbers between 2 numbers - sql

I have 4 columns- Code, Amount, Start, End. I would like to take the between amounts in the start and end columns and change them into one column with all results. Any suggestions on how to achieve this? Thanks.
Current Results:
Code Amount Start End
1 5000 2015 2016
2 5000 2014 2016
3 20000 2012 2016
Desired Results:
Code Amount StartEnd
1 5000 2015
1 5000 2016
2 5000 2014
2 5000 2015
2 5000 2016
3 20000 2012
3 20000 2013
3 20000 2014
3 20000 2015
3 20000 2016

You can use a recursive cte to generate all the numbers between minimum start and maximum end and join on the generated numbers.
with cte as (select min(start) col,max(end) mx from tablename
union all
select col+1,mx from cte where col < mx)
select t.code,t.amount,c.col
from cte c
join tablename t on c.col between t.start and t.end
or more simply
with cte as (select id,amount,start startend,end from tablename
union all
select id,amount,start+1,end from cte where start<end)
select id,amount,startend
from cte
order by 1,3

You can query like this
SELECT
c.code,
c.amount,
f.yr
FROM #code c
CROSS APPLY fn_yearslist(c.startyr, c.endyr) f
function you cancreate like this
CREATE FUNCTION fn_yearslist (#startyear int, #endyear int)
RETURNS #t TABLE (
yr int
)
AS
BEGIN
WHILE (#startyear <= #endyear)
BEGIN
INSERT INTO #t (yr)
VALUES (#startyear)
SET #startyear += 1
END
RETURN
END

Another option is a UDF. I use this TVF to generate dynamic ranges
Declare #YourTable table (Code int, Amount int, Start int , [End] int)
Insert into #YourTable values
(1,5000 ,2015,2016),
(2,5000 ,2014,2016),
(3,20000,2012,2016)
Select A.Code
,A.Amount
,StartEnd = cast(B.RetVal as int)
From #YourTable A
Cross Apply (Select * from [dbo].[udf-Range-Number](A.Start,A.[End],1)) B
Returns
Code Amount StartEnd
1 5000 2015
1 5000 2016
2 5000 2014
2 5000 2015
2 5000 2016
3 20000 2012
3 20000 2013
3 20000 2014
3 20000 2015
3 20000 2016
The Function
CREATE FUNCTION [dbo].[udf-Range-Number] (#R1 money,#R2 money,#Incr money)
Returns Table
Return (
with cte0(M) As (Select cast((#R2-#R1)/#Incr as int)),
cte1(N) As (Select 1 From (Values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N(N)),
cte2(N) As (Select Top (Select M from cte0) Row_Number() over (Order By (Select NULL)) From cte1 a,cte1 b,cte1 c,cte1 d,cte1 e,cte1 f,cte1 g,cte1 h )
Select RetSeq=1,RetVal=#R1 Union All Select N+1,(N*#Incr)+#R1
From cte2
)
/*
Max 100 million observations --
Syntax:
Select * from [dbo].[udf-Range-Number](0,4,0.25)
*/

I am not sure if it works on SQL 2008, but here's a CTE:
;with sel_cte as (
select Code, Amount, start StartEnd
from #tblTest t
union all
select t.code, t.amount, c.StartEnd + 1 StartEnd
from sel_cte c
inner join #tblTest t on t.code = c.code
where c.StartEnd + 1 <= [end]
)
select *
from sel_cte
order by Code, StartEnd
Note: replace #tblTest with the actual table name.

If the requirements allow you to ONLY have successive numbers (like 2014, 2015, then 2016, etc) then the approach above (cte) would work fine. However, if not, you could create a another temp table (say numbers having 1 column) with numbers in successive sequence of what all you want to be the result output, like.
Number
2014
2015
2016
2018 <-- Missing 2017 and jumping on 2018
2019
And then use a right join to get results in the progressive series with a query similar to the one below.
select Code, StartEnd, Amount from numbers
right join inputs on number between Start and End

Related

Running Total added to Next partition

declare #Temp table
(
Grp int,
Bal float,
[Value] float
)
declare #Amt float =1000;
Insert into #Temp(Grp,[Value])
Values(1,10),(1,5),(1,15)
,(2,20),(2,5),(2,15)
,(3,50),(3,50)
select Grp,#Amt as Amount,Value,Bal from #Temp
Required Output:
Grp Amount Value Bal
1 1000 10 1000
1 1000 5 1000
1 1000 15 1000
2 1000 20 1030 ---(10+5+15)
2 1000 5 1030
2 1000 15 1030
3 1000 50 1070 ---- (20+5+15)
3 1000 50 1070
Balance calculated based on running total of 'Value' of Group1 added to Group2 and running total of group 2 added to balance of Group3 and soon
I know how to calculate the running total but I can't as sums are added to next partition.
Please help to get required result efficiently. I am using SQL Server 2017
One method is outer apply:
select t.*, t.amount + coalesce(t2.value, 0)
from #temp t outer apply
(select sum(t2.value) as value
from #temp t2
where t2.grp < t.grp
) t2;
It is possibly more efficient to use aggregation and a running sum:
select t.*,
(1000 + tt.running_value)
from #temp t join
(select t.grp, sum(value) as value,
sum(sum(value)) over (order by grp) - sum(value) as running_value
from #temp t
group by grp
) tt
on t.grp = tt.grp;
Unfortunately, SQL Server doesn't fully support range window frames, so I don't think there is a convenient way to do this only with window functions. But the group by will probably have much better performance.

SQL get consecutive days ignoring weekend

I have a table with following format:
ID ID1 ID2 DATE
1 1 1 2018-03-01
2 1 1 2018-03-02
3 1 1 2018-03-05
4 1 1 2018-03-06
5 1 1 2018-03-07
6 2 2 2018-03-05
7 2 2 2018-03-05
8 2 2 2018-03-06
9 2 2 2018-03-07
10 2 2 2018-03-08
From this table I have to get all records where ID1 and ID2 are the same in that column and where DATE is 5 consecutive work days (5 dates in a row, ignoring missing dates for Saturday/Sunday; ignore holidays).
I have really no idea how to achieve this. I did search around, but couldn't find anything that helped me. So my question is, how can I achieve following output?
ID ID1 ID2 DATE
1 1 1 2018-03-01
2 1 1 2018-03-02
3 1 1 2018-03-05
4 1 1 2018-03-06
5 1 1 2018-03-07
SQLFiddle to mess around
Assuming you have no duplicates and work is only on weekdays, then there is a simplish solution for this particular case. We can identify the date 4 rows ahead. For a complete week, it is either 4 days ahead or 6 days ahead:
select t.*
from (select t.*, lead(dat, 4) over (order by id2, dat) as dat_4
from t
) t
where datediff(day, dat, dat_4) in (4, 6);
This happens to work because you are looking for a complete week.
Here is the SQL Fiddle.
select t.* from
(select id1,id2,count(distinct dat) count from t
group by id1,id2
having count(distinct dat)=5) t1 right join
t
on t.id1=t1.id1 and t.id2=t1.id2
where count=5
Check this-
Dates of Two weeks with 10 valid dates
http://sqlfiddle.com/#!18/76556/1
Dates of Two weeks with 10 non-unique dates
http://sqlfiddle.com/#!18/b4299/1
and
Dates of Two weeks with less than 10 but unique
http://sqlfiddle.com/#!18/f16cb/1
This query is very verbose without LEAD or LAG and it is the best I could do on my lunch break. You can probably improve on it given the time.
DECLARE #T TABLE
(
ID INT,
ID1 INT,
ID2 INT,
TheDate DATETIME
)
INSERT #T SELECT 1,1,1,'03/01/2018'
INSERT #T SELECT 2,1,1,'03/02/2018'
INSERT #T SELECT 3,1,1,'03/05/2018'
INSERT #T SELECT 4,1,1,'03/06/2018'
INSERT #T SELECT 5,1,1,'03/07/2018'
--INSERT #T SELECT 5,1,1,'03/09/2018'
INSERT #T SELECT 6,2,2,'03/02/2018'
INSERT #T SELECT 7,2,2,'03/05/2018'
INSERT #T SELECT 8,2,2,'03/05/2018'
--INSERT #T SELECT 9,2,2,'03/06/2018'
INSERT #T SELECT 10,2,2,'03/07/2018'
INSERT #T SELECT 11,2,2,'03/08/2018'
INSERT #T SELECT 12,2,2,'03/15/2018'
INSERT #T SELECT 13,1,1,'04/01/2018'
INSERT #T SELECT 14,1,1,'04/02/2018'
INSERT #T SELECT 15,1,1,'04/05/2018'
--SELECT * FROM #T
DECLARE #LowDate DATETIME = DATEADD(DAY,-1,(SELECT MIN(TheDate) FROM #T))
DECLARE #HighDate DATETIME = DATEADD(DAY,1,(SELECT MAX(TheDate) FROM #T))
DECLARE #DaysThreshold INT = 5
;
WITH Dates AS
(
SELECT DateValue=#LowDate
UNION ALL
SELECT DateValue + 1 FROM Dates
WHERE DateValue + 1 < #HighDate
),
Joined AS
(
SELECT * FROM Dates LEFT OUTER JOIN #T T ON T.TheDate=Dates.DateValue
),
Calculations AS
(
SELECT
ID=MAX(J1.ID),
J1.ID1,J1.ID2,
J1.TheDate,
LastDate=MAX(J2.TheDate),
LastDateWasWeekend = CASE WHEN ((DATEPART(DW,DATEADD(DAY,-1,J1.TheDate) ) + ##DATEFIRST) % 7) NOT IN (0, 1) THEN 0 ELSE 1 END,
Offset = DATEDIFF(DAY,MAX(J2.TheDate),J1.TheDate)
FROM
Joined J1
LEFT OUTER JOIN Joined J2 ON J2.ID1=J1.ID1 AND J2.ID2=J1.ID2 AND J2.TheDate<J1.TheDate
WHERE
NOT J1.ID IS NULL
GROUP BY J1.ID1,J1.ID2,J1.TheDate
)
,FindValid AS
(
SELECT
ID,ID1,ID2,TheDate,
IsValid=CASE
WHEN LastDate=TheDate THEN 0
WHEN LastDate IS NULL THEN 1
WHEN Offset=1 THEN 1
WHEN Offset>3 THEN 0
WHEN Offset<=3 THEN
LastDateWasWeekend
END
FROM
Calculations
UNION
SELECT DISTINCT ID=NULL,ID1,ID2, TheDate=#HighDate,IsValid=0 FROM #T
),
FindMax As
(
SELECT
This.ID,This.ID1,This.ID2,This.TheDate,MaxRange=MIN(Next.TheDate)
FROM
FindValid This
LEFT OUTER JOIN FindValid Next ON Next.ID2=This.ID2 AND Next.ID1=This.ID1 AND This.TheDate<Next.TheDate AND Next.IsValid=0
GROUP BY
This.ID,This.ID1,This.ID2,This.TheDate
),
FindMin AS
(
SELECT
This.ID,This.ID1,This.ID2,This.TheDate,This.MaxRange,MinRange=MIN(Next.TheDate)
FROM
FindMax This
LEFT OUTER JOIN FindMax Next ON Next.ID2=This.ID2 AND Next.ID1=This.ID1 AND This.TheDate<Next.MaxRange-- AND Next.IsValid=0 OR Next.TheDate IS NULL
GROUP BY
This.ID,This.ID1,This.ID2,This.TheDate,This.MaxRange
)
,Final AS
(
SELECT
ID1,ID2,MinRange,MaxRange,SequentialCount=COUNT(*)
FROM
FindMin
GROUP BY
ID1,ID2,MinRange,MaxRange
)
SELECT
T.ID,
T.ID1,
T.ID2,
T.TheDate
FROM #T T
INNER JOIN Final ON T.TheDate>= Final.MinRange AND T.TheDate < Final.MaxRange AND T.ID1=Final.ID1 AND T.ID2=Final.ID2
WHERE
SequentialCount>=#DaysThreshold
OPTION (MAXRECURSION 0)

SQL Running Total Grouped By Limit

I am trying to determine how to group records together based the cumulative total of the Qty column so that the group size doesn't exceed 50. The desired group is given in the group column with sample data below.
Is there a way to accomplish this in SQL (specifically SQL Server 2012)?
Thank you for any assistance.
ID Qty Group
1 10 1
2 20 1
3 30 2 <- 60 greater than 50 so new group
4 40 3
5 2 3
6 3 3
7 10 4
8 25 4
9 15 4
10 5 5
You can use CTE to achieve the goal.
If one of the item exceeds Qty 50, a group still assign for it
DECLARE #Data TABLE (ID int identity(1,1) primary key, Qty int)
INSERT #Data VALUES (10), (20), (30), (40), (2), (3), (10), (25), (15), (5)
;WITH cte AS
(
SELECT ID, Qty, 1 AS [Group], Qty AS RunningTotal FROM #Data WHERE ID = 1
UNION ALL
SELECT data.ID, data.Qty,
-- The group limits to 50 Qty
CASE WHEN cte.RunningTotal + data.Qty > 50 THEN cte.[Group] + 1 ELSE cte.[Group] END,
-- Reset the running total for each new group
data.Qty + CASE WHEN cte.RunningTotal + data.Qty > 50 THEN 0 ELSE cte.RunningTotal END
FROM #Data data INNER JOIN cte ON data.ID = cte.ID + 1
)
SELECT ID, Qty, [Group] FROM cte
The following query gives you most of what you want. One more self-join of the result would compute the group sizes:
select a.ID, G, sum(b.Qty) as Total
from (
select max(ID) as ID, G
from (
select a.ID, sum(b.Qty) / 50 as G
from T as a join T as b
where a.ID >= b.ID
group by a.ID
) as A
group by G
) as a join T as b
where a.ID >= b.ID
group by a.ID
ID G Total
---------- ---------- ----------
2 0 30
3 1 60
8 2 140
10 3 160
The two important tricks:
Use a self-join with an inequality to get running totals
Use integer division to calculate group numbers.
I discuss this and other techniques on my canonical SQL page.
You need to create a stored procedure for this.
If you have Group column in your database then you have to take care about it while inserting a new record by fetching the max Group value and its sum of Qty column otherwise if you want Group column as computed in select statement then you have to code stored procedure accordingly.

grouping results based on time diff in sql

I have results like this
TimeDiffMin | OrdersCount
10 | 2
12 | 5
09 | 6
20 | 15
27 | 11
I would like the following
TimeDiffMin | OrdersCount
05 | 0
10 | 8
15 | 5
20 | 15
25 | 0
30 | 11
So you can see that i want the grouping of every 5 minutes and show the total order count in those 5 minutes. eg. 0-5 minutes 0 orders, 5-10 minutes 8 orders
any help would be appreciated.
current query:
SELECT TimeDifferenceInMinutes, count(OrderId) NumberOfOrders FROM (
SELECT AO.OrderID, AO.OrderDate, AON.CreatedDate AS CancelledDate, DATEDIFF(minute, AO.OrderDate, AON.CreatedDate) AS TimeDifferenceInMinutes
FROM
(SELECT OrderID, OrderDate FROM AC_Orders) AO
JOIN
(SELECT OrderID, CreatedDate FROM AC_OrderNotes WHERE Comment LIKE '%has been cancelled.') AON
ON AO.OrderID = AON.OrderID
WHERE DATEDIFF(minute, AO.OrderDate, AON.CreatedDate) <= 100 AND AO.OrderDate >= '2016-12-01'
) AS Temp1
GROUP BY TimeDifferenceInMinutes
Now, if you are open to a TVF.
I use this UDF to create dynamic Date/Time Ranges. You supply the range and increment
Declare #YourTable table (TimeDiffMin int,OrdersCount int)
Insert Into #YourTable values
(10, 2),
(12, 5),
(09, 6),
(20,15),
(27,11)
Select TimeDiffMin = cast(R2 as int)
,OrdersCount = isnull(sum(OrdersCount),0)
From (Select R1=RetVal,R2=RetVal+5 From [dbo].[udf-Range-Number](0,25,5)) A
Left Join (
-- Your Complicated Query
Select * From #YourTable
) B on TimeDiffMin >= R1 and TimeDiffMin<R2
Group By R1,R2
Order By 1
Returns
TimeDiffMin OrdersCount
5 0
10 6
15 7
20 0
25 15
30 11
The UDF if interested
CREATE FUNCTION [dbo].[udf-Range-Number] (#R1 money,#R2 money,#Incr money)
Returns Table
Return (
with cte0(M) As (Select cast((#R2-#R1)/#Incr as int)),
cte1(N) As (Select 1 From (Values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N(N)),
cte2(N) As (Select Top (Select M from cte0) Row_Number() over (Order By (Select NULL)) From cte1 a,cte1 b,cte1 c,cte1 d,cte1 e,cte1 f,cte1 g,cte1 h )
Select RetSeq=1,RetVal=#R1 Union All Select N+1,(N*#Incr)+#R1
From cte2
)
-- Max 100 million observations
-- Select * from [dbo].[udf-Range-Number](0,4,0.25)
You can do this using a derived table to first build up your time difference windows and then joining from that to sum up all the Orders that fall within that window.
declare #t table(TimeDiffMin int
,OrdersCount int
);
insert into #t values
(10, 2)
,(12, 5)
,(09, 6)
,(20,15)
,(27,11);
declare #Increment int = 5; -- Set your desired time windows here.
with n(n)
as
( -- Select 10 rows to start with:
select n from(values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) as n(n)
),n2 as
( -- CROSS APPLY these 10 rows to get 10*10=100 rows we can use to generate incrementing ROW_NUMBERs. Use more CROSS APPLYs to get more rows:
select (row_number() over (order by (select 1))-1) * #Increment as StartMin
,(row_number() over (order by (select 1))) * #Increment as EndMin
from n -- 10 rows
cross apply n n2 -- 100 rows
--cross apply n n3 -- 1000 rows
--cross apply n n4 -- 10000 rows
)
select m.EndMin as TimeDiffMin
,isnull(sum(t.OrdersCount),0) as OrdersCount
from n2 as m
left join #t t
on(t.TimeDiffMin >= m.StartMin
and t.TimeDiffMin < m.EndMin
)
where m.EndMin <= 30 -- Filter as required
group by m.EndMin
order by m.EndMin
Query result:
TimeDiffMin OrdersCount
5 0
10 6
15 7
20 0
25 15
30 11

Calculating de-cumulatived values in TSQL?

Given a table of cars and their odometer reading at various dates (first of each month), how can I write TSQL (ideally, for use as a SQL Server view) to return the "incremental" values?
In other words, I want the reverse operation from Calculate a Running Total in SQL Server.
Example:
On this table:
CarId | Date | Mileage
---------------------------
1 1/1/2000 10000
1 2/1/2000 11000
1 3/1/2000 12000
2 1/1/2000 10000
2 2/1/2000 11001
2 3/1/2000 12001
3 1/1/2000 10000
(missing datapoint for (3, 2/1/2000))
3 3/1/2000 12000
We'd return something like (the details/edge cases are flexible):
CarId | Date | Delta
---------------------------
1 1/1/2000 10000
1 2/1/2000 1000
1 3/1/2000 1000
2 1/1/2000 10000
2 2/1/2000 1001
2 3/1/2000 1000
3 1/1/2000 10000
3 3/1/2000 2000
This should work for SQL 2005 or higher:
WITH cteData As
(
SELECT
CarId,
Date,
Mileage,
ROW_NUMBER() OVER (PARTITION BY CarId ORDER BY Date) As RowNumber
FROM
dbo.Cars
)
SELECT
C.CarId,
C.Date,
CASE
WHEN P.CarId Is Null THEN C.Mileage
ELSE C.Mileage - P.Mileage
END As Delta
FROM
cteData As C
LEFT JOIN cteData As P
ON P.CarId = C.CarId
And P.RowNumber = C.RowNumber - 1
ORDER BY
C.CarId,
C.Date
;
SQL Fiddle
NB: This assumes that "missing datapoint for (3, 2/1/2000)" means that there is no row in the table for car 3, February 2000.
Same approach as the one from #Richard Deeming, but this one regards possible null values as included in original question.
;with cte ( rn, id, date, mileage )
as
(
select
row_number() over ( partition by id order by id, date )
, id
, date
, mileage
from
cars
where
mileage is not null
)
select
"current".id
, "current".date
, delta = isnull( "current".mileage - predecessor.mileage, "current".mileage )
from
cte as "current"
left join cte as predecessor
on "current".id = predecessor.id
and "current".rn - 1 = predecessor.rn
See SQL-Fiddle.
Trying to do this without dependence on any 2012 functions, cursor, while loop, etc.
This works within some limitation -- namely, the null-entry for car#3's entry is a problem for it:
DECLARE #cars table ([id] int, [date] smalldatetime, [mileage] int)
INSERT INTO #cars ([id], [date], [mileage])
SELECT 1, '1/1/2000', 10000 UNION ALL
SELECT 1, '2/1/2000', 11000 UNION ALL
SELECT 1, '3/1/2000', 12000 UNION ALL
SELECT 2, '1/1/2000', 10000 UNION ALL
SELECT 2, '2/1/2000', 11000 UNION ALL
SELECT 2, '3/1/2000', 12000 UNION ALL
SELECT 3, '1/1/2000', 10000 UNION ALL
SELECT 3, '2/1/2000', NULL UNION ALL
SELECT 3, '3/1/2000', 12000
SELECT t1.id, t1.date, t1.mileage, t2.id, t2.date, t2.mileage, t1.mileage - t2.mileage as miles FROM #cars t1
LEFT JOIN #cars t2
ON t1.id = t2.id
AND t1.date = DATEADD(MONTH,1, t2.date)
Window functions are great. But SQL Server does not have the one you need until SQL Server 2012. There, you have the lag function:
select t.*,
(Milage - lag(Milage) over (partition by carId order by date)) as Delta
from t
For earlier versions, you can use a correlated subquery:
[trouble uploading query], alas.
select t.*, (Mileage - prevMileage) as Delta
from (select t.*,
(select top 1 Mileage from t t2
 where t2.carId = t.carId and t2.date < t.date order by desc
 ) as prevDelta
from t
) t