Define ranges to cover gaps in a number sequence (T-SQL) - 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

Related

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

concatenate recursive cross join

I need to concatenate the name in a recursive cross join way. I don't know how to do this, I have tried a CTE using WITH RECURSIVE but no success.
I have a table like this:
group_id | name
---------------
13 | A
13 | B
19 | C
19 | D
31 | E
31 | F
31 | G
Desired output:
combinations
------------
ACE
ACF
ACG
ADE
ADF
ADG
BCE
BCF
BCG
BDE
BDF
BDG
Of course, the results should multiply if I add a 4th (or more) group.
Native Postgresql Syntax:
SqlFiddleDemo
WITH RECURSIVE cte1 AS
(
SELECT *, DENSE_RANK() OVER (ORDER BY group_id) AS rn
FROM mytable
),cte2 AS
(
SELECT
CAST(name AS VARCHAR(4000)) AS name,
rn
FROM cte1
WHERE rn = 1
UNION ALL
SELECT
CAST(CONCAT(c2.name,c1.name) AS VARCHAR(4000)) AS name
,c1.rn
FROM cte1 c1
JOIN cte2 c2
ON c1.rn = c2.rn + 1
)
SELECT name as combinations
FROM cte2
WHERE LENGTH(name) = (SELECT MAX(rn) FROM cte1)
ORDER BY name;
Before:
I hope if you don't mind that I use SQL Server Syntax:
Sample:
CREATE TABLE #mytable(
ID INTEGER NOT NULL
,TYPE VARCHAR(MAX) NOT NULL
);
INSERT INTO #mytable(ID,TYPE) VALUES (13,'A');
INSERT INTO #mytable(ID,TYPE) VALUES (13,'B');
INSERT INTO #mytable(ID,TYPE) VALUES (19,'C');
INSERT INTO #mytable(ID,TYPE) VALUES (19,'D');
INSERT INTO #mytable(ID,TYPE) VALUES (31,'E');
INSERT INTO #mytable(ID,TYPE) VALUES (31,'F');
INSERT INTO #mytable(ID,TYPE) VALUES (31,'G');
Main query:
WITH cte1 AS
(
SELECT *, rn = DENSE_RANK() OVER (ORDER BY ID)
FROM #mytable
),cte2 AS
(
SELECT
TYPE = CAST(TYPE AS VARCHAR(MAX)),
rn
FROM cte1
WHERE rn = 1
UNION ALL
SELECT
[Type] = CAST(CONCAT(c2.TYPE,c1.TYPE) AS VARCHAR(MAX))
,c1.rn
FROM cte1 c1
JOIN cte2 c2
ON c1.rn = c2.rn + 1
)
SELECT *
FROM cte2
WHERE LEN(Type) = (SELECT MAX(rn) FROM cte1)
ORDER BY Type;
LiveDemo
I've assumed that the order of "cross join" is dependent on ascending ID.
cte1 generate DENSE_RANK() because your IDs contain gaps
cte2 recursive part with CONCAT
main query just filter out required length and sort string
The recursive query is a bit simpler in Postgres:
WITH RECURSIVE t AS ( -- to produce gapless group numbers
SELECT dense_rank() OVER (ORDER BY group_id) AS grp, name
FROM tbl
)
, cte AS (
SELECT grp, name
FROM t
WHERE grp = 1
UNION ALL
SELECT t.grp, c.name || t.name
FROM cte c
JOIN t ON t.grp = c.grp + 1
)
SELECT name AS combi
FROM cte
WHERE grp = (SELECT max(grp) FROM t)
ORDER BY 1;
The basic logic is the same as in the SQL Server version provided by #lad2025, I added a couple of minor improvements.
Or you can use a simple version if your maximum number of groups is not too big (can't be very big, really, since the result set grows exponentially). For a maximum of 5 groups:
WITH t AS ( -- to produce gapless group numbers
SELECT dense_rank() OVER (ORDER BY group_id) AS grp, name AS n
FROM tbl
)
SELECT concat(t1.n, t2.n, t3.n, t4.n, t5.n) AS combi
FROM (SELECT n FROM t WHERE grp = 1) t1
LEFT JOIN (SELECT n FROM t WHERE grp = 2) t2 ON true
LEFT JOIN (SELECT n FROM t WHERE grp = 3) t3 ON true
LEFT JOIN (SELECT n FROM t WHERE grp = 4) t4 ON true
LEFT JOIN (SELECT n FROM t WHERE grp = 5) t5 ON true
ORDER BY 1;
Probably faster for few groups. LEFT JOIN .. ON true makes this work even if higher levels are missing. concat() ignores NULL values. Test with EXPLAIN ANALYZE to be sure.
SQL Fiddle showing both.

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

querying SQL Server 2005

I have a table that is time and milemarkers:
08:00 101.2
08:45 109.8
09:15 109.8
09:30 111.0
10:00 114.6
I need output that looks like this:
08:00-08:45 101.1-109.8
08:45-09:15 109.8-109.8
09:15-09:30 109.8-111.0
09:30-10:00 111.0-114.6
I figure I need 2 identical recordsets and somehow tie the first record of one to the second record of the other, but am clueless on how to accomplish that (or how to ask the question). Any help would be greatly appreciated.
Thanks in advance,
Ginny
The following query will get the next values:
select tm.*,
(select top 1 time
from timemilemarkers tm2
where tm2.time > tm.time
order by 1 desc
) as nexttime,
(select top 1 milemarker
from timemilemarkers tm2
where tm2.time > tm.time
order by 1 desc
) as nextmilemarker
from timemilemarkers tm;
You can put them into the form you want with something like:
select concat_ws('-', milemarker, nextmilemarker), concat_ws('-', time, nexttime)
from (select tm.*,
(select top 1 time
from timemilemarkers tm2
where tm2.time > tm.time
order by 1 desc
) as nexttime,
(select top 1 milemarker
from timemilemarkers tm2
where tm2.time > tm.time
order by 1 desc
) as nextmilemarker
from timemilemarkers tm
) tm
where nextmilemarker is not null;
Other way to do it is:
SQLFiddle
select cast(A.TIME_COL as varchar) + ' - ' + cast(B.TIME_COL as varchar),
cast(A.MILES as varchar) + ' - ' + cast(B.MILES as varchar)
from (select row_number() OVER (order by time_col) ID, * from TABLE_A) A
inner join (select row_number() OVER (order by time_col) ID, * from TABLE_A) B
on A.ID = B.ID - 1
UPDATE: this query will only works for SQL Server 2008 and upwards and obviously not answer your question. I will not erase the answer cause it can be helpful for othe people.
UPDATE2: It works on SQL Server 2005.
try this,
Declare #t table (times time(0), milemarkers decimal(5,2))
insert into #t
select '08:00','101.2' union all
select'08:45','109.8' union all
select'09:15','109.8' union all
select'09:30','111.0' union all
select'10:00','114.6'
;With cte1 as
(select *,ROW_NUMBER()over(order by times)rn from #t
)
,cte2 as
(select max(rn) rn1 from cte1)
, cte as
(select
(select times from cte1 where rn=1)lowerlimit,(select times from cte1 where rn=2)upperlimit,
(select milemarkers from cte1 where rn=1)lowerlimit1,(select milemarkers from cte1 where rn=2)upperlimit1
,1 rn from cte1
union all
select upperlimit,(select times from cte1 where rn=a.rn+2)
,upperlimit1,(select milemarkers from cte1 where rn=a.rn+2)
,rn+1
from cte a where a.rn<(select rn1 from cte2)
)
select distinct cast(lowerlimit as varchar(10))+'-'+cast(upperlimit as varchar(10)) ,
cast(lowerlimit1 as varchar(10))+'-'+cast(upperlimit1 as varchar(10))
from cte a where a.rn<(select rn1 from cte2)
Using CTE we can get the OutPut it is also other way to do Find below query
DECLARE #TABLE_A table(time_col time, miles float)
insert into #TABLE_A values ('08:00',101.2)
insert into #TABLE_A values ('08:45',109.8)
insert into #TABLE_A values ('09:15',109.8)
insert into #TABLE_A values ('09:30',111.0)
insert into #TABLE_A values ('10:00',114.6)
;WITH CTE AS
(
SELECT t.time_col,t.miles,t.RN FROM
(
Select ROW_NUMBER()OVER(ORDER BY time_col )RN,* FRom #TABLE_A
)t
INNER JOIN (
Select ROW_NUMBER()OVER(ORDER BY time_col )RN,* FRom #TABLE_A
)tt
ON t.RN = tt.RN
)
,CTE2(TimeSpan,Miles) AS
(
Select CONVERT(VARCHAR,c.time_col,108) +'-'+
(Select CONVERT(VARCHAR,time_col,108) FROM CTE WHERE RN = cc.RN + 1) As TimeSpan,
CAST(c.miles AS VARCHAR) +' - '+ (Select CAST(miles AS VARCHAR) FROM CTE WHERE RN = CC.RN + 1)AS Miles FROM CTE c
INNER JOIN CTE CC
ON CC.miles = c.miles
AND CC.time_col = c.time_col
)
Select TimeSpan,Miles from CTE2
WHERE TimeSpan IS NOT NULL

Find all integer gaps in SQL

I have a database which is used to store information about different matches for a game that I pull in from an external source. Due to a few issues, there are occasional gaps (which could be anywhere from 1 missing ID to a few hundred) in the database. I want to have the program pull in the data for the missing games, but I need to get that list first.
Here is the format of the table:
id (pk-identity) | GameID (int) | etc. | etc.
I had thought of writing a program to run through a loop and query for each GameID starting at 1, but it seems like there should be a more efficient way to get the missing numbers.
Is there an easy and efficient way, using SQL Server, to find all the missing numbers from the range?
The idea is to look at where the gaps start. Let me assume you are using SQL Server 2012, and so have the lag() and lead() functions. The following gets the next id:
select t.*, lead(id) over (order by id) as nextid
from t;
If there is a gap, then nextid <> id+1. You can now characterize the gaps using where:
select id+1 as FirstMissingId, nextid - 1 as LastMissingId
from (select t.*, lead(id) over (order by id) as nextid
from t
) t
where nextid <> id+1;
EDIT:
Without the lead(), I would do the same thing with a correlated subquery:
select id+1 as FirstMissingId, nextid - 1 as LastMissingId
from (select t.*,
(select top 1 id
from t t2
where t2.id > t.id
order by t2.id
) as nextid
from t
) t
where nextid <> id+1;
Assuming the id is a primary key on the table (or even that it just has an index), both methods should have reasonable performance.
Numbers table!
CREATE TABLE dbo.numbers (
number int NOT NULL
)
ALTER TABLE dbo.numbers
ADD
CONSTRAINT pk_numbers PRIMARY KEY CLUSTERED (number)
WITH FILLFACTOR = 100
GO
INSERT INTO dbo.numbers (number)
SELECT (a.number * 256) + b.number As number
FROM (
SELECT number
FROM master..spt_values
WHERE type = 'P'
AND number <= 255
) As a
CROSS
JOIN (
SELECT number
FROM master..spt_values
WHERE type = 'P'
AND number <= 255
) As b
GO
Then you can perform an OUTER JOIN or EXISTS` between your two tables and find the gaps...
SELECT *
FROM dbo.numbers
WHERE NOT EXISTS (
SELECT *
FROM your_table
WHERE id = numbers.number
)
-- OR
SELECT *
FROM dbo.numbers
LEFT
JOIN your_table
ON your_table.id = numbers.number
WHERE your_table.id IS NULL
I like the "gaps and islands" approach. It goes a little something like this:
WITH Islands AS (
SELECT GameId, GameID - ROW_NUMBER() OVER (ORDER BY GameID) AS [IslandID]
FROM dbo.yourTable
)
SELECT MIN(GameID), MAX(Game_id)
FROM Islands
GROUP BY IslandID
That query will get you the list of contiguous ranges. From there, you can self-join that result set (on successive IslandIDs) to get the gaps. There is a bit of work in getting the IslandIDs themselves to be contiguous though. So, extending the above query:
WITH
cte1 AS (
SELECT GameId, GameId - ROW_NUMBER() OVER (ORDER BY GameId) AS [rn]
FROM dbo.yourTable
)
, cte2 AS (
SELECT [rn], MIN(GameId) AS [Start], MAX(GameId) AS [End]
FROM cte1
GROUP BY [rn]
)
,Islands AS (
SELECT ROW_NUMBER() OVER (ORDER BY [rn]) AS IslandId, [Start], [End]
from cte2
)
SELECT a.[End] + 1 AS [GapStart], b.[Start] - 1 AS [GapEnd]
FROM Islands AS a
LEFT JOIN Islands AS b
ON a.IslandID + 1 = b.IslandID
SELECT * FROM #tab1
id col1
----------- --------------------
1 a
2 a
3 a
8 a
9 a
10 a
11 a
15 a
16 a
17 a
18 a
WITH cte (id,nextId) as
(SELECT t.id, (SELECT TOP 1 t1.id FROM #tab1 t1 WHERE t1.id > t.id) AS nextId FROM #tab1 t)
SELECT id AS 'GapStart', nextId AS 'GapEnd' FROM cte
WHERE id + 1 <> nextId
GapStart GapEnd
----------- -----------
3 8
11 15
Try this (This covers upto 10000 Ids starting from 1, if you need more you can add more to Numbers table below):
;WITH Digits AS (
select Digit
from ( values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) as t(Digit))
,Numbers AS (
select u.Digit
+ t.Digit*10
+ h.Digit*100
+ th.Digit*1000
+ tth.Digit*10000
--Add 10000, 100000 multipliers if required here.
as myId
from Digits u
cross join Digits t
cross join Digits h
cross join Digits th
cross join Digits tth
--Add the cross join for higher numbers
)
SELECT myId
FROM Numbers
WHERE myId NOT IN (SELECT GameId FROM YourTable)
Problem: we need to find the gap range in id field
SELECT * FROM #tab1
id col1
----------- --------------------
1 a
2 a
3 a
8 a
9 a
10 a
11 a
15 a
16 a
17 a
18 a
Solution
WITH cte (id,nextId) as
(SELECT t.id, (SELECT TOP 1 t1.id FROM #tab1 t1 WHERE t1.id > t.id) AS nextId FROM #tab1 t)
SELECT id + 1, nextId - 1 FROM cte
WHERE id + 1 <> nextId
Output
GapStart GapEnd
----------- -----------
4 7
12 14