How do i get the next value and previous in a column - sql-server

Suppose i have an ID column that has the values 1 , 5 , 7 .What SQL statement can i use to get the next value of in the column based on another.
Example : The next value after 1 is 5.
Example 2 : the value before 7 is 5

Without any window functions or CTEs:
select
t.id,
(select max(t1.id) from tbl t1 where t1.id < t.id) as previd,
(select min(t2.id) from tbl t2 where t2.id > t.id) as nextid
from tbl t

There are some great Windowed functions for working across records in later versions of SQL Server (2012+). But in 2008 these aren't available. Instead you could use an OUTER APPLY. This will allow you to filter a sub query using values from your main query.
Outer Apply Query Example
WITH SampleValues AS
(
/* This CTE creates some sample values.
*/
SELECT
r.n
FROM
(
VALUES
(1),
(5),
(7)
) AS r(n)
)
SELECT
o.n,
prev.Previous_n,
[next].Next_n
FROM
SampleValues AS o
OUTER APPLY
(
-- Find the previou value by looking for the TOP 1 before the current.
SELECT TOP 1
o.n AS Original_n,
p.n AS Previous_n
FROM
SampleValues AS p
WHERE
p.n < o.n
ORDER BY
p.n DESC
) AS prev
OUTER APPLY
(
-- Find the next value by looking for the TOP 1 after the current.
SELECT TOP 1
n.n AS Original_n,
n.n AS Next_n
FROM
SampleValues AS n
WHERE
n.n > o.n
ORDER BY
n.n ASC
)AS [next]
;

Related

T-SQL - Next row with greater value, continuously

I have table described bellow from which I need to select all rows with [Value] greater for example at least 5 points than [Value] from previous row (ordered by [Id]). Starting with first row of [Id] 1, desired output would be:
[Id] [Value]
---------------
1 1
4 12
8 21
Code:
declare #Data table
(
[Id] int not null identity(1, 1) primary key,
[Value] int not null
);
insert into #Data ([Value])
select 1 [Value]
union all
select 5
union all
select 3
union all
select 12
union all
select 8
union all
select 9
union all
select 16
union all
select 21;
select [t1].*
from #Data [t1];
Edit:
So, based on JNevill's and Hogan's answers I end with this:
;with [cte1]
as (
select [t1].[Id],
[t1].[Value],
cast(1 as int) [rank]
from #Data [t1]
where [t1].[Id] = 1
union all
select [t2].[Id],
[t2].[Value],
cast(row_number() over (order by [t2].id) as int) [rank]
FROM [cte1] [t1]
inner join #Data [t2] on [t2].[value] - [t1].[value] > 5
and [t2].[Id] > [t1].[Id]
where [t1].[rank] = 1
)
select [t1].[Id],
[t1].[Value]
from [cte1] [t1]
where [t1].[rank] = 1;
which is working. Alan Burstein answer is correct too (but applicable only on MSSQL 2012+ - due to LAG fc). I will do some performance tests (I'm on 2016 version) and will see performance over my real data (approx. 30 millions of records).
If you are on 2012+ you can use LAG which will provide a better performing solution that a recursive CTE. I'm including your sample data so you can just copy/paste/test...
-- Your sample data
DECLARE #Data TABLE
(
Id int not null identity(1, 1) primary key,
Value int not null
);
insert into #Data ([Value])
select 1 [Value] union all select 5 union all select 3 union all select 12 union all
select 8 union all select 9 union all select 16 union all select 21;
-- Solution using window functions
WITH
prevRows AS
(
SELECT t1.Id, t1.Value, prevDiff = LAG(t1.Value, 1) OVER (ORDER BY t1.id) - t1.Value
FROM #Data t1
),
NewPrev AS
(
SELECT t1.Id, t1.Value, NewDiff = Value - LAG(t1.Value,1) OVER (ORDER BY t1.id)
FROM prevRows t1
WHERE prevDiff <= -5 OR prevDiff IS NULL
)
SELECT t1.Id, t1.Value
FROM NewPrev t1
WHERE NewDiff >= 5 OR NewDiff IS NULL;
I believe the best way to pull this off is using a recursive CTE. A Recursive CTE is a special type of CTE that refers back to itself. It's made up of two parts.
The recursive seed/anchor which establishes the beginning of the recursion. In your case, record with ID=1.
The recursive term/member which is the statement that refers back to itself by the name of the CTE. Here we pull through the next record that is greater than 5 from the previous found record according to the ID sorted ascending.
Code:
WITH RECURSIVE recCTE AS
(
/*Select first record for recursive seed/anchor*/
SELECT
id,
value,
cast(1 as INT) as [rank]
FROM table
WHERE id = 1
UNION ALL
/*find the next value that is more than 5 from the current value*/
SELECT
table.id,
table.value
ROW_NUMBER() OVER (ORDER BY id)
FROM
recCTE INNER JOIN table
ON table.value - recCTE.value > 5
AND table.id > recCTE.id
WHERE recCTE.[rank]=1
)
SELECT id, value FROM recCTE;
I've made use of the Row_Number() Window Function to find the rank of the matching record by ID sorted Ascending. With the WHERE clause in the recursive term we only grab the first found record that is 5 more than the previous found record. Then we head into the next recursive step.
You can do it with a recursive CTE
with find_values as
(
-- Find first value
SELECT Value
FROM #Table
ORDER BY ID ASC
FETCH FIRST 1 ROW ONLY
UNION ALL
-- Find next value
SELECT Value
FROM #Table
CROSS JOIN find_values
WHERE Value >= find_values.Value + 5
ORDER BY ID ASC
FETCH FIRST 1 ROW ONLY
)
SELECT *
FROM find_values

How to join first two child records to parent?

I am trying to select the top 25 parent records and join to it the first two child records ordered by date. The parent record can have 0 to n children.
The end result would be something like:
P1, C1, C2
P2, C1, C2
...
P25, C1, C2
I have found an example using max date, but I am having trouble getting a specific row number
select top 25 *
from parentTable p
left join childTable c
on p.Key = c.Key
and c.dateColumn = (
select Max(c.dateColumn)
from c
where p.Key = c.Key
)
You should be able to get what you are looking for using ROW_NUMBER():
WITH c AS
(
SELECT ROW_NUMBER() OVER (PARTITION BY [key] ORDER BY [date] asc) as rn
,[key]
,[date]
from child
)
SELECT top 25 p.[key], c1.[key], c1.[date], c2.[key], c2.[date]
FROM parent p
LEFT JOIN c c1
ON p.[key] = c1.[key]
AND c1.rn = 1
LEFT JOIN c c2
ON p.[key] = c2.[key]
AND c2.rn = 2
Check SQLFiddle for test data/results
You can use CTE.
;with cte as (
select top (25) *
from parentTable
)
select *
from cte p
left join childTable c
on p.[Key] = c.[Key];
First solution (because data set seems to be small) one solution is to use OUTER APPLY:
select top (25) ... columns ...
from parentTable p
outer apply (
select top (2) ... columns ...
from childTable c
where p.[Key] = c.[Key]
order by c.dateColumn desc -- asc ?
) a
-- Most of the times, when top filter is used order by clause should be also used
order by p.dateColumn desc -- asc ?
-- order by p.idColumn desc -- asc ?
Second solution (could be less eficient):
select top (25) ... columns ...
from parentTable p
left join (
select top (2) ... columns ..., ROW_NUMBER() over(partition by c.[Key] order by c.dateColumn desc) as rn -- asc ?
from childTable c
) a on p.[Key] = a.[Key] and a.rn < 3
-- Most of the times, when top filter is used order by clause should be also used
order by p.dateColumn desc -- asc ?
-- order by p.idColumn desc -- asc ?
Note: at least for the first solution one of following indices could help from the point of view of performance:
create index ix_name on dbo.childTable ([Key], [dateColumn])
--or
create index ix_name on dbo.childTable ([Key], [dateColumn])
include (... columns from select top(2) clause ...)

Why does window functions not work in CROSS APPLY?

There is a simple code. I always thought that both outside ROW_NUMBER and the one in CROSS APPLY clause are supposed to generate the same output (in my example I excepct rn = crn). Could you please explain why isn't it like that?
CREATE TABLE #tmp ( id INT, name VARCHAR(200) );
INSERT INTO #tmp
VALUES ( 1, 'a' ),
( 2, 'a' ),
( 3, 'a' ),
( 4, 'b' ),
( 5, 'b' ),
( 6, 'c' ),
( 7, 'a' );
SELECT name,
ROW_NUMBER() OVER ( PARTITION BY name ORDER BY id ) AS rn,
a.crn
FROM #tmp
CROSS APPLY (
SELECT ROW_NUMBER() OVER ( PARTITION BY name ORDER BY id ) AS crn
) a;
OUTPUT:
name rn crn
a 1 1
a 2 1
a 3 1
a 4 1
b 1 1
b 2 1
c 1 1
The query in the CROSS APPLY is applied to each row in #tmp. The query selects for that one row it is applied to, the row number for that one row which is of course one.
Maybe this article on Microsoft's Technet would give you more insight into how CROSS APPLY works. An excerpt that highlights what I wrote in previous paragraph:
The APPLY operator allows you to invoke a table-valued function for each row returned by an outer table expression of a query. The table-valued function acts as the right input and the outer table expression acts as the left input. The right input is evaluated for each row from the left input and the rows produced are combined for the final output. The list of columns produced by the APPLY operator is the set of columns in the left input followed by the list of columns returned by the right input.
Note that APPLY is using the fields from you main query as the parameters.
SELECT ROW_NUMBER() OVER ( PARTITION BY name ORDER BY id ) AS crn
The above query does not have a FROM clause. So it's treating the name and id as literals. To illustrate, for the first row of #tmp, the resulting query of the CROSS APPLY is:
SELECT ROW_NUMBER() OVER ( PARTITION BY (SELECT 'a') ORDER BY (SELECT 1)) AS crn
which returns:
crn
--------------------
1
This is the result of your CROSS APPLY for every rows.
To achieve the desired result:
SELECT
t.name,
ROW_NUMBER() OVER ( PARTITION BY t.name ORDER BY t.id ) AS rn,
a.crn
FROM #tmp t
CROSS APPLY(
SELECT id, ROW_NUMBER() OVER (PARTITION BY name ORDER BY id ) AS crn
FROM #tmp
) a
WHERE t.id = a.id

Insert row for each integer between 0 and <value> without cursor

I have a source table with id and count.
id count
a 5
b 2
c 31
I need to populate a destination table with each integer up to the count for each id.
id value
a 1
a 2
a 3
a 4
a 5
b 1
b 2
c 1
c 2
etc...
My current solution is like so:
INSERT INTO destination (id,value)
source.id
sequence.number
FROM
(VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9)) AS sequence(number)
INNER JOIN
source ON sequence.number <= source.count
This solution has an upper limit and is plain lame. Is there anyway to replace the sequence with a set of all integers? Or another solution that does not use looping.
this should work:
WITH r AS (
SELECT id, count, 1 AS n FROM SourceTable
UNION ALL
SELECT id, count, n+1 FROM r WHERE n<count
)
SELECT id,n FROM r
order by id,n
OPTION (MAXRECURSION 0)
Unfortunately, there is not set of all integers in SQL Server. However, using a little trickery, you can easily generate such a set:
select N from (
select ROW_NUMBER() OVER (ORDER BY t1.object_id) AS N
from sys.all_objects t1, sys.all_objects t2
) AS numbers
where N between 1 and 1000000
will generate a set of all numbers from 1 through 1000000. If you need more than a few million numbers, add sys.all_objects to the cross join a third time.
You can find many examples in this page:
DECLARE #table TABLE (ID VARCHAR(1), counter INT)
INSERT INTO #table SELECT 'a', 5
INSERT INTO #table SELECT 'b', 3
INSERT INTO #table SELECT 'c', 31
;WITH cte (ID, counter) AS (
SELECT id, 1
FROM #table
UNION ALL
SELECT c.id, c.counter +1
FROM cte AS c
INNER JOIN #table AS t
ON t.id = c.id
WHERE c.counter + 1 <= t.counter
)
SELECT *
FROM cte
ORDER BY ID, Counter

Pass Value To Derived Table

Is there a way to pass a value to a derived table query?
In the derived table I want reference a value ([docSVsys].[sID]) from the outer query.
I get an error:
Msg 4104, Level 16, State 1, Line 7 The multi-part identifier
"docSVsys.sID" could not be bound.
Yes I know this query can be simplified to no loop.
Have a cursor that has to loop and trying to convert it so set.
select top 10 [docSVsys].[sID], [sI].[count]
from docSVsys
join
(
select count(*) as [count]
from docSVenum1 as [sIt]
where [sIt].[sID] = [docSVsys].[sID]
) as [sI]
on '1' = '1'
order by [docSVsys].[sID]
Cross apply seemed to do the trick.
And it is exactly 1/3 faster than the cursor version.
Real query using cross apply below.
SELECT [sO].[sID], [sI].[max], [sI].[avg], [sI].[stdev]
FROM docSVsys as [sO] with (nolock)
cross apply
(
select [sO].[sID], max(list.match) as 'max', avg(list.match) as 'avg', stdev(list.match) as 'stdev'
from
(
select #SampleSet.[sID], [match] = 200 * count(*) / CAST ( #SampleSetSummary.[count] + [sO].[textUniqueWordCount] as numeric(8,0) )
from #SampleSet with (nolock)
join FTSindexWordOnce as [match] with (nolock) -- this is current #sID
on match.wordID = #SampleSet.wordID
and [match].[sID] = [sO].[sID]
join #SampleSetSummary with (nolock) -- to get the word count from the sample set
on #SampleSetSummary.[sID] = #SampleSet.[sID]
group by #SampleSet.[sID], #SampleSetSummary.[count]
) as list
having max(list.match) > 60
) as [sI]
where [textUniqueWordCount] is not null and [textUniqueWordCount] > 4 and [sO].[sID] <= 10686
order by [sO].[sID]
You can do what you want with a CROSS APPLY rather than a JOIN:
select top 10 [docSVsys].[sID], [sI].[count]
from docSVsys
cross apply
(
select count(*) as [count]
from docSVenum1 as [sIt]
where [sIt].[sID] = [docSVsys].[sID]
) as [sI]
order by [docSVsys].[sID]
Add the ID to the derived table, and join on that:
select top 10 [docSVsys].[sID], [sI].[count]
from docSVsys
join
(
select [sIt].[sID], count(*) as [count]
from docSVenum1 as [sIt]
group by [sIt].[sID]
) as [sI]
on [sI].[sID] = [docSVsys].[sID]
order by [docSVsys].[sID]

Resources