How to get previous and next row's value effeciently in SQL server - sql

Say I have these rows,
InstrumentID
547
698
708
InstrumentID is not autogenerated column.
Say if I pass the parameter in procedure as 698, I should get previous value as 547 and next value as 708. How do I do this efficiently in SQL?
I have this procedure but it is not efficient (and not correct).
Alter PROCEDURE GetNextAndPreviousInsturmentID
(
#InstrumentID varchar(14),
#PreviousInstrumentID varchar(14) OUT,
#NextInstrumentID varchar(14) OUT
)
AS
BEGIN
Declare #RowNum int = 0
Select #RowNum = ROW_NUMBER() Over (Order by Cast(#InstrumentID as decimal(18))) From Documents Where InstrumentID = #InstrumentID
;With normal As
(
Select ROW_NUMBER() Over (Order by Cast(#InstrumentID as decimal(18))) as RowNum, Cast(InstrumentID as decimal(18)) as InstrumentID
From Documents
)
Select #PreviousInstrumentID = InstrumentID From normal
Where RowNum = #RowNum - 1
Select #NextInstrumentID = InstrumentID From normal
Where RowNum = #RowNum + 1
END
GO

Here is a simpler solution, still it's more efficient
SELECT P.PreviousID, N.NextID
FROM
(SELECT MAX(D.InstrumentID) PreviousID
FROM Documents D
WHERE InstrumentID < #InstrumentID) P
CROSS JOIN
(SELECT MIN(D.InstrumentID) NextID
FROM Documents D
WHERE InstrumentID > #InstrumentID) N

Try this:
Alter PROCEDURE GetNextAndPreviousInsturmentID
(
#InstrumentID varchar(14),
#PreviousInstrumentID varchar(14) OUT,
#NextInstrumentID varchar(14) OUT
)
AS
BEGIN
Declare #Ids TABLE(Id varchar(14))
;With normal As
(
--Numerate our rows
Select ROW_NUMBER() Over (Order by Cast(Documents.InstrumentID as decimal(18)) as RowNumber,
Documents.InstrumentID
From Documents
)
--Insert three rows from our table with our id and previos/next id
INSERT INTO #Ids(Id)
SELECT TOP(3) normal.InstrumentID
FROM normal
WHERE RowNumber >=
(
SELECT RowNumber - 1
FROM normal
WHERE normal.InstrumentID = #InstrumentID
)
ORDER BY normal.RowNumber
--select next and previos ids
SELECT #PreviousInstrumentID = Min(CAST(Id as decimal(18))),
#NextInstrumentID = MAX(CAST(Id as decimal(18)))
FROM #Ids
END
GO
In MS SQL 2012 we have new window functions like FIRST_VALUE and LAST_VALUE, unfortunately in sql 2008 these functions are missing.

WITH CTE AS (
SELECT rownum = ROW_NUMBER() OVER (ORDER BY p.LogDate), p.LogDate
FROM DeviceLogs p
)
SELECT prev.logdate PreviousValue, CTE.logdate, nex.logdate NextValue
FROM CTE
LEFT JOIN CTE prev ON prev.rownum = CTE.rownum - 1
LEFT JOIN CTE nex ON nex.rownum = CTE.rownum + 1
GO

select
LAG(InstrumentID) OVER (ORDER BY InstrumentID) PreviousValue,
InstrumentID,
LEAD(InstrumentID) OVER (ORDER BY InstrumentID) NextValue
from documents

Hi I think this will be much more efficient:
Select Next :
select top 1 ID from mytable
where ID >'698'
order by ID asc
Select Prev:
select top 1 ID from mytable
where ID <'698'
order by ID desc

Related

Selecting data from table where sum of values in a column equal to the value in another column

Sample data:
create table #temp (id int, qty int, checkvalue int)
insert into #temp values (1,1,3)
insert into #temp values (2,2,3)
insert into #temp values (3,1,3)
insert into #temp values (4,1,3)
According to data above, I would like to show exact number of lines from top to bottom where sum(qty) = checkvalue. Note that checkvalue is same for all the records all the time. Regarding the sample data above, the desired output is:
Id Qty checkValue
1 1 3
2 2 3
Because 1+2=3 and no more data is needed to show. If checkvalue was 4, we would show the third record: Id:3 Qty:1 checkValue:4 as well.
This is the code I am handling this problem. The code is working very well.
declare #checkValue int = (select top 1 checkvalue from #temp);
declare #counter int = 0, #sumValue int = 0;
while #sumValue < #checkValue
begin
set #counter = #counter + 1;
set #sumValue = #sumValue + (
select t.qty from
(
SELECT * FROM (
SELECT
ROW_NUMBER() OVER (ORDER BY id ASC) AS rownumber,
id,qty,checkvalue
FROM #temp
) AS foo
WHERE rownumber = #counter
) t
)
end
declare #sql nvarchar(255) = 'select top '+cast(#counter as varchar(5))+' * from #temp'
EXECUTE sp_executesql #sql, N'#counter int', #counter = #counter;
However, I am not sure if this is the best way to deal with it and wonder if there is a better approach. There are many professionals here and I'd like to hear from them about what they think about my approach and how we can improve it. Any advice would be appreciated!
Try this:
select id, qty, checkvalue from (
select t1.*,
sum(t1.qty) over (partition by t2.id) [sum]
from #temp [t1] join #temp [t2] on t1.id <= t2.id
) a where checkvalue = [sum]
Smart self-join is all you need :)
For SQL Server 2012, and onwards, you can easily achieve this using ROWS BETWEEN in your OVER clause and the use of a CTE:
WITH Running AS(
SELECT *,
SUM(qty) OVER (ORDER BY id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS RunningQty
FROM #temp t)
SELECT id, qty, checkvalue
FROM Running
WHERE RunningQty <= checkvalue;
One basic improvement is to try & reduce the no. of iterations. You're incrementing by 1, but if you repurpose the logic behind binary searching, you'd get something close to this:
DECLARE #RoughAverage int = 1 -- Some arbitrary value. The closer it is to the real average, the faster things should be.
DECLARE #CheckValue int = (SELECT TOP 1 checkvalue FROM #temp)
DECLARE #Sum int = 0
WHILE 1 = 1 -- Refer to BREAK below.
BEGIN
SELECT TOP (#RoughAverage) #Sum = SUM(qty) OVER(ORDER BY id)
FROM #temp
ORDER BY id
IF #Sum = #CheckValue
BREAK -- Indicating you reached your objective.
ELSE
SET #RoughAverage = #CheckValue - #Sum -- Most likely incomplete like this.
END
For SQL 2008 you can use recursive cte. Top 1 with ties limits result with first combination. Remove it to see all combinations
with cte as (
select
*, rn = row_number() over (order by id)
from
#temp
)
, rcte as (
select
i = id, id, qty, sumV = qty, checkvalue, rn
from
cte
union all
select
a.id, b.id, b.qty, a.sumV + b.qty, a.checkvalue, b.rn
from
rcte a
join cte b on a.rn + 1 = b.rn
where
a.sumV < b.checkvalue
)
select
top 1 with ties id, qty, checkvalue
from (
select
*, needed = max(case when sumV = checkvalue then 1 else 0 end) over (partition by i)
from
rcte
) t
where
needed = 1
order by dense_rank() over (order by i)

SQL Server window functions: building up a history string

I have a table of longitudinal data that looks like this:
where id is the partition variable, period is the time dimension, and val is the observation value.
I want to build up a history of val for each panel of id, like this:
I'm trying to do this with SQL window functions and not a cursor, but the issue I keep running into is the self-referential nature of the hist column definition. It almost seems like I'd have to create one row/column per period. For example, the closest I could come was this:
IF OBJECT_ID('dbo.my_try', 'U') IS NOT NULL
DROP TABLE dbo.my_try;
GO
SELECT
id, period, val,
CASE
WHEN (
period = MIN(period)
OVER (PARTITION by id order by period ROWS
BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
) THEN CAST (val AS VARCHAR(60))
ELSE NULL
END AS hist
INTO my_try
FROM my_test
SELECT
id, period, val,
CASE
WHEN (
period = MIN(period) OVER
(PARTITION by id order by period ROWS
BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
) THEN hist
ELSE (
CONCAT(
val, ' | ', LAG(hist, 1) OVER (PARTITION by id order by period)
)
)
END AS hist2
FROM my_try
I would have to spool out the iteration and do a hist3, etc. for it to finally work.
Is it possible to accomplish this with SQL window functions, or is cursor the only route?
Sample Data
Here is some code to generate the original table:
CREATE TABLE my_test (
id INT,
period INT,
val INT
)
BEGIN
DECLARE #id INT = 1;
DECLARE #period INT = 1;
WHILE #id <= 3
BEGIN
SET #period = 1
WHILE #period <= 3
BEGIN
INSERT INTO my_test VALUES (#id, #period, #period * POWER(10, #id))
SET #period = #period + 1
END
SET #id = #id + 1
END
END
Actually you don't need recursion here. You can leverage STUFF pretty easily. Of course if you are on 2017 you can use string_agg as suggested above. But if you are like me and your company is not the fastest to adopt the latest and greatest you can use this.
select t1.id
, t1.period
, t1.val
, STUFF((select ' | ' + convert(varchar(10), val)
from my_test t2
where t2.id = t1.id
and t2.period <= t1.period
order by t1.period
FOR XML PATH('')), 1, 3,'')
from my_test t1
order by t1.id
, t1.period
As discussed in the comments try using recursive query
with cte as(
select id, [period], val, convert(varchar(max), val) as agg from my_try where [period] = 1
union all
select t.id, t.[period], t.val, CONCAT(c.agg, ' | ', t.val) from my_try t join cte c on c.[period] +1 = t.[period] and c.id = t.id
)
select * from cte order by id, [period]

Select TOP 2 results in variables without loop

I want to store the top 2 results in 2 variables.
create table t(id int);
insert into t (id) values (1),(2),(3),(4);
declare #id1 int
declare #id2 int
select top 2 #id1 = first id,
#id2 = next id
from t
SQLFiddle
Can I do it in one query without using a loop?
declare #id1 int,#id2 int
;with cte as (
select top (2) id
from t
order by id
)
select #id1 = min(id), #id2 = max(id)
from cte
select #id1,#id2
Fiddle demo
with cte as (
select top 2 id, row_number() over(order by id) as rn
from t
order by id
)
select
#id1 = (select id from cte where rn = 1),
#id2 = (select id from cte where rn = 2)
or
with cte as (
select top 2 id, row_number() over(order by id) as rn
from t
order by id
)
select
#id1 = max(case when rn = 1 then id end),
#id2 = max(case when rn = 2 then id end)
from cte
sql fiddle demo
You can use LEAD() for SQL Server 2012.
SELECT TOP 1 #id1 = ID, #id2 = LEAD(ID) OVER (ORDER BY ID) FROM t
SQLFiddle Demo
With two SELECT it's easy...
DECLARE #id1 INT
DECLARE #id2 INT
SELECT TOP 1 #id1 = x.id
FROM (SELECT *, ROW_NUMBER() OVER (ORDER BY id) RN FROM t) x
WHERE x.RN = 1
SELECT TOP 1 #id2 = x.id
FROM (SELECT *, ROW_NUMBER() OVER (ORDER BY id) RN FROM t) x
WHERE x.RN = 2
SELECT #id1, #id2
With SQL 2012 you clearly could
SELECT #id1 = id
FROM t ORDER BY id OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY
SELECT #id2 = id
FROM t ORDER BY id OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY
Or evein in 2008 you could
; WITH Base AS (
SELECT *, ROW_NUMBER() OVER (ORDER BY id) RN FROM t
)
SELECT #id1 = b1.id, #id2 = b2.id
FROM Base b1, Base b2
WHERE b1.RN = 1 AND B2.RN = 2
declare #id1 int
declare #id2 int
declare #table table(id int,rownum int)
insert into #table
select top 2 id,row_number() over( order by id) as rn from t
select #id1=case rownum when 1 then id else #id1 end,
#id2=case rownum when 2 then id end from #table
select #id1,#id2
SQL FIDDLE
More easy way with 2 selects:
declare #id1 int
declare #id2 int
select top 1 #id1 = id from t
select top 2 #id2 = id from t
select #id1, #id2
SQL Fiddle

sql server 2008, cannot use order by in subquery

here's my sql server 2008 stored procedure.
ALTER PROCEDURE [dbo].[GetSharedSmoothies]
#Page INT ,
#Status INT ,
#ItemPerPage INT
AS
BEGIN
SET NOCOUNT ON;
DECLARE #X INT
DECLARE #Y INT
SET #X = ( #Page - 1 ) * #ItemPerPage
SET #Y = #Page * #ItemPerPage
SELECT *
FROM ( SELECT S.* ,
U.Avatar ,
U.Displayname ,
( SELECT COUNT(Id)
FROM Vote
WHERE Vote.SmoothieId = S.Id
) AS Votes ,
ROW_NUMBER() OVER ( ORDER BY S.Id ) rownum
FROM dbo.Smoothie AS S
INNER JOIN dbo.[User] AS U ON S.UserId = U.Id
WHERE S.IsPublic = 1
AND S.Status = 3
AND S.UserId > 0
-- ORDER BY S.CreatedDate DESC
) seq
WHERE seq.rownum BETWEEN #X AND #Y
ORDER BY seq.rownum
END
in my code, you will see I comment out the order by
-- ORDER BY S.CreatedDate DESC
because order by will not work in subquery. i need to show the lastest one on the top. is there a way I can use order by in my code?
You may add S.CreatedDate within the Row_NUMBER()
ROW_NUMBER() OVER (PARTITION BY S.Id ORDER BY S.CreatedDate DESC) AS RowNum
That's right. It is not allowed, because it will do nothing.
Having the latest one at the top in the subquery will do nothing to the result set using the subquery.
Add the needed column to the result set ORDER BY:
ORDER BY seq.CreatedDate DESC, seq.rownum
Or:
ORDER BY seq.rownum, seq.CreatedDate DESC

Group by numbers that are in sequence

I have some data like this:
row id
1 1
2 36
3 37
4 38
5 50
6 51
I would like to query it to look like this:
row id group
1 1 1
2 36 2
3 37 2
4 38 2
5 50 3
6 51 3
... so that I can GROUP BY where the numbers are consecutively sequential.
Also, looping/cursoring is out of the question since I'm working with a pretty large set of data, thanks.
;WITH firstrows AS
(
SELECT id, ROW_NUMBER() OVER (ORDER BY id) groupid
FROM Table1 a
WHERE id - 1 NOT IN (SELECT b.id FROM Table1 b)
)
SELECT id,
(
SELECT MAX(b.groupid)
FROM firstrows b
WHERE b.id <= a.id
) groupid
FROM Table1 a
with
data(row, id) as (
select *
from (
values
(1,1)
,(2,36)
,(3,37)
,(4,38)
,(5,50)
,(6,51)
) as foo(row, id)
),
anchor(row, id) as (
select row, id
from data d1
where not exists(select 0 from data d2 where d2.id = d1.id - 1)
)
select d1.*, dense_rank() over(order by foo.id) as thegroup
from
data d1
cross apply (select max(id) from anchor where anchor.id <= d1.id) as foo(id)
order by
d1.row
;
This solution does more work that is strictly necessary on the basis that there may be gaps in the sequence of row values, and on the assumption that those gaps should be ignored.
Set up test data:
DECLARE #table TABLE
(ROW INT,
id INT
)
INSERT #table
SELECT 1,1
UNION SELECT 2,36
UNION SELECT 3,37
UNION SELECT 4,38
UNION SELECT 5,50
UNION SELECT 6,51
Output query
;WITH grpCTE
AS
(
SELECT ROW, id,
ROW_NUMBER() OVER (ORDER BY ROW
) AS rn
FROM #table
)
,recCTE
AS
(
SELECT ROW, id, rn, 1 AS grp
FROM grpCTE
WHERE rn = 1
UNION ALL
SELECT g.row, g.id, g.rn, CASE WHEN g.id = r.id + 1 THEN r.grp ELSE r.grp + 1 END AS grp
FROM grpCTE AS g
JOIN recCTE AS r
ON g.rn = r.rn + 1
)
SELECT row, id, grp FROM recCTE
create table #temp
(
IDUnique int Identity(1,1),
ID int,
grp int
)
Insert into #temp(ID) Values(1)
Insert into #temp(ID) Values(36)
Insert into #temp(ID) Values(37)
Insert into #temp(ID) Values(38)
Insert into #temp(ID) Values(50)
Insert into #temp(ID) Values(51)
declare #IDUnique int
declare #PreviousUnique int
declare #ID int
declare #grp int
declare #Previous int
declare #Row int
DECLARE #getAccountID CURSOR SET #getAccountID = CURSOR FOR SELECT Row_Number() Over(Order by IDUnique) Row, IDUnique, ID From #temp
OPEN #getAccountID
FETCH NEXT FROM #getAccountID INTO #Row, #IDUnique, #ID
WHILE ##FETCH_STATUS = 0
BEGIN
IF(#Row = 1)
Begin
update #temp set grp = 1 Where IDUnique = #IDUnique
set #Previous = #ID
set #grp = 1
End
Else If (#Previous + 1 = #ID)
Begin
update #temp set grp = #grp Where IDUnique = #IDUnique
set #Previous = #ID
End
Else
Begin
set #Previous = #ID
set #grp = #grp + 1
update #temp set grp = #grp Where IDUnique = #IDUnique
End
FETCH NEXT FROM #getAccountID INTO #Row, #IDUnique, #ID
END
CLOSE #getAccountID
DEALLOCATE #getAccountID
Select * from #temp
Drop Table #temp
Select T.Id, T.Row, groupId as "Group", dr FROM tbrows T
Left Outer Join
(
Select min(id) as groupId,DENSE_RANK() over( order by min(id)) as dr, MIN(row-id) as d, Sum(1) as s FROM tbrows
Group BY (row-id)
) U
On (T.Id >= U.groupId) and (T.Id < U.groupId+U.s)
Order By T.Id