Select minimum number in a range - sql

I have a table with data like.
ItemCode
1000
1002
1003
1020
1060
I'm trying to write a SQL statement to get the minimum number (ItemCode) that is NOT in this table and it should be able to get the next lowest number once the previous minimum order ID has been inserted in the table but also skip the numbers that are already in the DB. I only want to get 1 result each time the query is run.
So, it should get 1001 as the first result based on the table above. Once the ItemCode = 1001 has been inserted into the table, the next result it should get should be 1004 because 1000 to 1003 already exist in the table.
Based on everything I have seen online, I think, I have to use a While loop to do this. Here is my code which I'm still working on.
DECLARE #Count int
SET #Count= 0
WHILE Exists (Select ItemCode
from OITM
where itemCode like '10%'
AND convert(int,ItemCode) >= '1000'
and convert(int,ItemCode) <= '1060')
Begin
SET #COUNT = #COUNT + 1
select MIN(ItemCode) + #Count
from OITM
where itemCode like '10%'
AND convert(int,ItemCode) >= '1000'
and convert(int,ItemCode) <= '1060'
END
I feel like there has to be an easier way to accomplish this. Is there a way for me to say...
select the minimum number between 1000 and 1060 that doesn't exist in table X
EDIT: Creating a new table isn't an option in my case
Final Edit: Thanks guys! I got it. Here is my final query that returns exactly what I want. I knew I was making it too complicated for no reason!
With T0 as ( select convert(int,ItemCode) + row_number() over (order by convert(int,ItemCode)) as ItemCode
from OITM
where itemCode like '10%'
AND convert(int,ItemCode) >= '1000'
And convert(int,ItemCode) <= '1060')
Select MIN(convert(varchar,ItemCode)) as ItemCode
from T0
where convert(int,ItemCode) Not in (Select convert(int,ItemCode)
from OITM
where itemCode like '10%'
AND convert(int,ItemCode) >= '1000'
and convert(int,ItemCode) <= '1060');

This should do the thing. Here you are generating sequantial number for rows, then comparing each row with next row(done by joining condition), and filtering those rows only where difference is not 1, ordering by sequence and finally picking the top most.
;with c as(select id, row_number() over(order by id) rn)
select top 1 c1.id + 1 as NewID
from c as c1
join c as c2 on c1.rn + 1 = c2.rn
where c2.id - c1.id <> 1
order by c1.rn

You could use row_number() to produce sequential values for each row, and then look for the first row where the row_number() doesn't match the value stored in the table. My SQL Server installation isn't working at the moment and SQL Fiddle seems to be down too, so I wrote this without being able to test it, but something like this should work:
declare #lowerBound int = 1000;
declare #upperBound int = 1060;
declare #x table ([id] int);
insert #x values (1000), (1002), (1003), (1020), (1060);
with [SequenceCTE] as
(
select
[id],
[seq] = (#lowerBound - 1) + row_number() over (order by [id])
from
#x
)
select top 1
[seq]
from
[SequenceCTE]
where
[seq] != [id] and
[seq] <= #upperBound;
EDIT: Here is a SQL Fiddle that demonstrates this approach. I don't know why the site wasn't working for me before. It doesn't seem to like my declare statements for some reason so I hard-coded the bounds instead, but hopefully it still gets the idea across.

You can do this using a Tally Table. Check this article by Jeff Moden for reference.
Basically, you want to generate numbers from #start to #end. That's where the Tally Table comes in. It will be used in the numbers generation. When you have your numbers, you just have to check for the minimum value that does not exist in your table.
SQL Fiddle
DECLARE #start INT = 1000
DECLARE #end INT = 1060
;WITH E1(N) AS(
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
)
,E2(N) AS(SELECT 1 FROM E1 a, E1 b)
,E4(N) AS(SELECT 1 FROM E2 a, E2 b)
,Tally(N) AS(
SELECT TOP(#end - #start + 1) ROW_NUMBER() OVER(ORDER BY(SELECT NULL)) - 1 FROM E4
)
SELECT
MIN(#start + t.N)
FROM Tally t
WHERE
#start + t.N <= #end
AND NOT EXISTS(
SELECT 1
FROM OITM
WHERE CAST(ItemCode AS INT) = #start + t.N
)
Here is another query that uses sys.columns to generate the Tally Table:
;WITH Tally(N) AS(
SELECT TOP(#end - #start + 1) ROW_NUMBER() OVER(ORDER BY(SELECT NULL)) - 1
FROM sys.columns a
--CROSS JOIN sys.columns b
)
SELECT
MIN(#start + t.N)
FROM Tally t
WHERE
#start + t.N <= #end
AND NOT EXISTS(
SELECT 1
FROM OITM
WHERE CAST(ItemCode AS INT) = #start + t.N
)

Related

Generate Random Test Data with ORDER BY NEWID() , include duplicate rows

I need to select random rows from a table for test data. There may be times I need more rows of test data than there are records in the table. Duplicates are okay. How do I structure my select so that I can get duplicate rows?
CREATE TABLE [Northwind].[dbo].[Persons]
(PersonID int, LastName varchar(255))
INSERT INTO [Northwind].[dbo].[Persons]
VALUES
(1, 'Smith'),
(2, 'Jones'),
(3, 'Washington')
SELECT TOP 5 *
FROM [Northwind].[dbo].[Persons]
ORDER BY NEWID()
How do I get the Select statement to give me five records in random order, with repeats? Currently, it only returns three in random order.
I'd like to be able to extend this to get 100 rows or 1000 rows or however many I need.
Use a recursive CTE to union enough rows so that they are larger than what you desire. Then select from that as you have done before.
declare
#desired int = 5,
#actual int = (select count(*) from persons);
with
persons as (
select personId,
lastName,
batch = 0
from Persons
union all
select personId,
lastName,
batch = batch + 1
from persons
where (batch + 1) * #actual < #desired
)
select
top (#desired) personId, lastName
from persons
order by newid()
As mentioned. You could instead us a tally table and then get the random rows;
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL)))N(N)),
Tally AS(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1, N N2, N N3, N N4) --Repeat for more
SELECT TOP 500 YT.*
FROM Tally T
CROSS JOIN YourTable YT
ORDER BY NEWID();
I was thinking of how you would solve this without ordering all the records, especially multiple times.
One method is to generate random numbers and to use those for looking up values in your data:
with n as (
select rand(checksum(newid())) as r, 1 as n
union all
select rand(checksum(newid())) as r, n + 1
from n
where n < 10
),
tt as (
select t.*, lag(tile_end, 1, 0) over (order by tile_end) as tile_start
from (select t.*, row_number() over (order by newid()) * 1.0 / count(*) over () as tile_end
from t
) t
)
select tt.*, n.r, (select count(*) from n)
from n left join
tt
on n.r >= tt.tile_start and n.r < tt.tile_end;
Here is a db<>fiddle. The row_number() does not need to use order by newid(). It can order by a key that has an index -- which makes that component much more efficient.
For more than 100 rows, you will need OPTION (MAXRECURSION 0).
I added a temp results table and looped through the query and pushed the results into the temp table.
declare #results table(
SSN varchar(10),
Cusip varchar(10),
...
EndBillingDate varchar(10))
DECLARE #cnt INT = 0;
WHILE #cnt < #trades
BEGIN
INSERT INTO #results
Select ...
set #cnt = #cnt + 10
END
select * from #results

Creating sequence in SQL with different length

I have a table with the customer identifier as PK and his time to maturity in months:
Customer | Maturity
---------+-----------
1 80
2 60
3 52
4 105
I want to create a table which will have customer identifier and the maturity will be defined as sequence of number with the increment + 1:
Customer | Maturity
---------+------------
1 1
1 2
1 ....
1 80
2 1
2 2
2 ...
2 60
I don't know whether I should use a sequence or the cross join or how to solve this problem.
one way is to use recursive CTE.
; with cte as
(
select Customer, M = 1, Maturity
from yourtable
union all
select Customer, M = M + 1, Maturity
from yourtable
where M < Maturity
)
select *
from cte
option (MAXRECURSION 0)
You can try joining your current table to a sequence table to generate the maturity ranges you want.
WITH cte AS (
SELECT 1 AS seq
UNION ALL
SELECT seq + 1
FROM cte
WHERE seq < 500
)
SELECT
t1.Customer,
t2.seq AS Maturity
FROM yourTable t1
INNER JOIN cte t2
ON t2.seq <= t1.Maturity
ORDER BY
t1.Customer,
t2.seq
OPTION (MAXRECURSION 0);
Demo here:
Rextester
you can try query like below
create table t (Customer int, Maturity int)
insert into t values
(1,80)
,(2,60)
,(3,52)
,(4,105);
select Customer, r from
t cross join
(select top (select max(maturity) from t)
row_number() over( order by (select NULL)) r
from sys.objects s1 cross join sys.objects s2) k
where r<=Maturity
order by Customer asc,r asc
see live demo
You can try the below.
Created two temporary tables to represent your tables in below example.
You need to replace them with you table names and drop the first three lines.
declare #Customer table (Customer int, Maturity int)
declare #NewTable table (Customer int, Maturity int)
insert #Customer select 1, 80
declare #x int = 0
declare #iterations table (x int)
while #x <= (select max(Maturity) from #Customer)
begin
set #x += 1
insert #iterations select #x
end
insert #NewTable
select c.Customer, i.x from #Customer c left join #iterations i on i.x <= c.Maturity
select * from #NewTable
Late answer, but another option is an ad-hoc tally table in concert with a CROSS APPLY
Example
Select A.customer
,Maturity = B.N
From YourTable A
Cross Apply (
Select Top (A.Maturity) N=Row_Number() Over (Order By (Select NULL))
From master..spt_values n1
) B

Split row into several with SQL statement

I have a row in a databasetable that is on the following form:
ID | Amount | From | To
5 | 5439 | 01.01.2014 | 05.01.2014
I want to split this up to one row pr month using SQL/T-SQL:
Amount | From
5439 | 01.01.2014
5439 | 02.01.2014
5439 | 03.01.2014
5439 | 04.01.2014
5439 | 05.01.2014
I, sadly, cannot change the database source, and I want to preferrably do this in SQL as I am trying to result of this Query with an other table in Powerpivot.
Edit: Upon requests on my code, I have tried the following:
declare #counter int
set #counter = 0
WHILE #counter < 6
begin
set #counter = #counter +1
select amount, DATEADD(month, #counter, [From]) as Dato
FROM [database].[dbo].[table]
end
This however returns several databasesets.
You can use a tally table to generate all dates.
SQL Fiddle
;WITH E1(N) AS(
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
),
E2(N) AS(SELECT 1 FROM E1 a CROSS JOIN E1 b),
E4(N) AS(SELECT 1 FROM E2 a CROSS JOIN E2 b),
Tally(N) AS(
SELECT TOP(SELECT MAX(DATEDIFF(DAY, [From], [To])) + 1 FROM yourTable)
ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
FROM E4
)
SELECT
yt.Id,
yt.Amount,
[From] = DATEADD(DAY, N-1, yt.[From])
FROM yourTable yt
CROSS JOIN Tally t
WHERE
DATEADD(DAY, N-1, yt.[From]) <= yt.[To]
Simplified explanation on Tally Table
You need a tally table with "running numbers". This may be a function (I posted one shortly here: https://stackoverflow.com/a/32096945/5089204) or a physical table (I posted an example here: https://stackoverflow.com/a/32474751/5089204) or a CTE to do this "on the fly" (the table example does it this way).
If you go with the posted function it could be like this:
declare #startDate DATETIME={d'2015-09-01'};
declare #EndDate DATETIME={d'2015-09-10'};
select DATEADD(DAY, Nmbr,#startDate)
from dbo.GetRunningNumbers(DATEDIFF(DAY,#startDate,#endDate)+1,0);
select * INTO #TEMP1 from
(values
(5 , 5439 , '01.01.2014', '05.01.2014'))t(id,amount,fromd,tod)
WITH CTE
AS
(
SELECT CAST(FROMD AS DATE) AS FROMD,amount,1 AS RN,ID FROM #TEMP1
UNION ALL
SELECT DATEADD(M,1,C.FROMD),C.amount,C.RN+1,C.ID
FROM CTE C
INNER JOIN #TEMP1 T ON T.id = C.ID AND DATEADD(M,1,c.FROMD)<=T.tod
)
SELECT * FROM CTE
create table t (fd date, td date)
insert into t values ('2015-01-01','2015-01-05')
WITH DATES (fd, td, Level)
AS
(
SELECT fd, td, 0 AS Level
FROM t
UNION ALL
-- Recursive member definition
SELECT DATEADD(day,level+1,e.fd),e.td,Level + 1
FROM t AS e
INNER JOIN Dates AS d ON DATEADD(day,-d.level,d.fd) = e.fd AND d.fd < d.td
)
-- Statement that executes the CTE
SELECT fd,td,level
from DATES
variant using recursive cte
--variable table for data sample
DECLARE #tbl AS TABLE
(
ID INT ,
Amount FLOAT ,
[From] DATE ,
[To] DATE
)
INSERT INTO #tbl
( ID, Amount, [From], [To] )
VALUES ( 5, 5439, '2014-01-01', '2014-01-05' )
--final query using recursive cte
;
WITH cte
AS ( SELECT T.ID ,
T.Amount ,
T.[From] ,
T.[To] ,
CONVERT(DATE, NULL) AS Dt ,
n = 0
FROM #tbl AS T
UNION ALL
SELECT cte.ID ,
cte.Amount ,
cte.[From] ,
cte.[To] ,
DATEADD(DAY, n, cte.[From]) ,
cte.n + 1
FROM cte
WHERE n <= DATEDIFF(day, cte.[From], cte.[To])
)
SELECT cte.ID ,
cte.Amount ,
dt AS [From]
FROM cte
WHERE cte.Dt IS NOT NULL
SQL Fiddle

SQL to get sequence of phone numbers

I have table called PhoneNumbers with columns Phone and Range as below
here in the phone column i have a phone numbers and in range column i have a range of values i need the phone numbers to be included.For the first phone number 9125678463 I need to include the phone numbers till the range 9125678465 ie (9125678463,9125678464,9125678465).Similarly for other phone numbers too.here is the sample destination table should look like
How can i write the sql to get this?
Thanks in advance
I have a solution which goes a classic way BUT: it does not need recursions and it does not need any loops! And it works even if your range has length of 3 or 5, or whatever...
first i create a table with numbers (from 1 to 1 million in this example - you can adopt this in TOP () clause):
SELECT TOP (1000000) n = CONVERT(INT, ROW_NUMBER() OVER (ORDER BY s1.[object_id]))
INTO dbo.Numbers
FROM sys.all_objects AS s1 CROSS JOIN sys.all_objects AS s2
OPTION (MAXDOP 1);
CREATE UNIQUE CLUSTERED INDEX idx_numbers ON dbo.Numbers(n)
;
if you have that table it's pretty simple:
;WITH phonenumbers
AS
(
SELECT phone,
[range],
CAST(RIGHT(phone,LEN([range])) AS INT) AS number_to_increase,
CAST(LEFT(phone,LEN(phone)-LEN([range])) + REPLICATE('0',LEN([range])) AS BIGINT) AS base_number
FROM PhoneNumbers
)
SELECT p.base_number + num.n
FROM phonenumbers p
INNER JOIN dbo.Numbers num ON num.n BETWEEN p.number_to_increase AND p.[range]
You don't have to use a CTE like here - it's just to see a bit clearer what the idea behind this approach is. Maybe this suits for you
You can use CTE like this:
;WITH CTE (PhoneNumbers, [Range], i) AS (
SELECT CAST(Phone AS bigint), [Range], CAST(1 AS bigint)
FROM yourTable
UNION ALL
SELECT CAST(PhoneNumbers + 1 AS bigint), [Range], i + 1
FROM CTE
WHERE (PhoneNumbers + 1) % 10000 <= [Range]
)
SELECT PhoneNumbers
FROM CTE
ORDER BY PhoneNumbers
Here is one example of using a tally table. In my system I have that set of ctes as a view so I never have to write it again.
if OBJECT_ID('tempdb..#PhoneNumbers') is not null
drop table #PhoneNumbers;
create table #PhoneNumbers
(
Phone char(10)
, Range smallint
)
insert #PhoneNumbers
select 9135678463, 8465 union all
select 3279275678, 5679 union all
select 6372938103, 8105;
WITH
E1(N) AS (select 1 from (values (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))dt(n)),
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS
(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
)
select *
from #PhoneNumbers p
join cteTally t on t.N >= RIGHT(Phone, 4) and t.N <= Range
order by p.Phone
One more approach:
--Creating dummy table
select '9999991234' phone, '1237' rang into #tbl
union
select '9999995689', '5692'
SELECT [phone] low
,(CAST(9999995689/10000 AS bigINT) * 10000 + [Rang]) high
into #tbl1
FROM #tbl
--Creating 'numbrs' to have numbers between 0 & 9999 i.e. max range
select (rn-1)rn
into #numbrs
from
(select row_number() over (partition by null order by A.object_id) rn from sys.objects A
cross join sys.objects B)A
where rn between 0 and 9999
select (low + rn)phn from #numbrs cross join #tbl1
where (low + rn) between low and high

Define ranges to cover gaps in a number sequence (T-SQL)

Simplifying my problem down - I have 6-digit field which assigns numbers to customers starting from 1 and ending to 999999. Most numbers are sequentially assigned, but numbers can be assigned manually by users, and this feature has been used in an unpredicatable pattern throughout the range.
We now need to identify numbers that have not been assigned (easy) - and then convert this into a number of ranges (seems complex).
For example given the following numbers have been assigned
1,2,3,4,5,
1001,1002,1003,1004,1005,
999101,999102,999103,999104,999105
I need a resulting set of ranges like
Start End
6 1000
1006 999100
999106 999999
My thinking so far is this is probably too complex to write in queries - and best achieved by looping from 1 to 999999, and adding ranges to a temp table.
Interested to hear ideas as I can imagine there are a few approaches. I'm using SQL Server 2008 R2. This is a one-off exercise so even a non-SQL solution might be appropriate, if this were for example easily done in Excel.
Try this
declare #t table (num int)
insert #t values (2),(3),(6),(7),(9),(10),(11)
select
MIN(number) as rangestart,
MAX(number) as rangeend
from
(
select *,
ROW_NUMBER() over (order by number) -
ROW_NUMBER() over (order by num,number) grp
from
(
select number from master..spt_values where type='p' and number between 1 and 15
) numbers
left join #t t
on numbers.number = t.num
) v
where num is null
group by grp
Reference : gaps and islands by itzik ben-gan
To create a numbers query upto 999999
select p1.number + p2.number * 2048 as number
from
(select * from master..spt_values where type='p' ) p1,
(select * from master..spt_values where type='p' and number<489) p2
where p1.number + p2.number * 2048 <=999999
declare #t table (num int)
insert #t values
(2),(3),(4),(5),
(1001),(1002),(1003),(1004),(1005),
(999101),(999102),(999103),(999104),(999105)
;with cte as
(
select num,(ROW_NUMBER() OVER(ORDER BY num)) + 1 as idx from #t
union
select 0 [num],1 [idx] --start boundary
union
select 1000000 [num],COUNT(num) + 2 [idx] from #t --end boundary
)
select c1.num + 1 [Start], c2.num - 1 [End]
from cte c1
inner join cte c2 on c2.idx = c1.idx + 1
where c2.num != c1.num + 1
create table #temp (id int)
insert into #temp (id)
values (1),(2),(3),(1000),(1001),(1002),(2000)
--drop table #temp
with cte as
(
select *, ROW_NUMBER() over(order by id) as rn
from #temp a
)
select a.id + 1, b.id - 1
from cte a join cte b on a.rn = b.rn - 1 and a.id <> b.id -1
it wont include tail ranges, like 2001-9999
Here is SQLFiddle demo
select
case when max(n1)=0 then 1 else max(n1)end,
case when max(n2)=0 then 999999 else max(n2)end
from
(
select t.n+1 as n1,0 n2,
row_number() over(order by t.n)
+isnull((select 0 from t where n=1),1)
rn
from t
left join t t2 on t.n+1=t2.n
where t2.n is null
union all
select 0 n1, t.n-1 as n2 ,
row_number() over(order by t.n) rn
from t
left join t t2 on t.n-1=t2.n
where t2.n is null
and t.n>1
) t3
group by rn
declare #t table(id int)
insert #t values
(1),(2),(3),(4),(5),(1001),(1002),(1003),(1004),(1005),
(999101),(999102),(999103),(999104),(999105)
select t1.id+1 [start], coalesce(t3.[end], 999999) [end]
from #t t1
left join #t t2 on t1.id +1 = t2.id
cross apply
(select min(id)-1 [end] from #t where t1.id < id
) t3
where t2.id is null
if you have a table called "kh" for example with a column "myval" which is your list of integers you could try this SELECT.
SELECT MAX(t1.myval+1) AS 'StartRange',t3.myval-1 AS 'EndRange'
FROM kh t1, kh t3
WHERE t1.myval+1 NOT IN (SELECT myval FROM kh t2 ORDER BY myval)
AND t3.myval-1 NOT IN (SELECT myval FROM kh t4 ORDER BY myval)
AND t1.myval < t3.myval
GROUP BY t3.myval
ORDER BY StartRange