Find "n" consecutive free numbers from table
This is a gaps-and-islands problem. Assuming there are no gaps or duplicates in the same id_set
set:
WITH partitioned AS (
SELECT
*,
number - ROW_NUMBER() OVER (PARTITION BY id_set) AS grp
FROM atable
WHERE status = 'FREE'
),
counted AS (
SELECT
*,
COUNT(*) OVER (PARTITION BY id_set, grp) AS cnt
FROM partitioned
)
SELECT
id_set,
number
FROM counted
WHERE cnt >= 3
;
Here's a SQL Fiddle demo* link for this query: http://sqlfiddle.com/#!1/a2633/1.
UPDATE
To return only one set, you could add in one more round of ranking:
WITH partitioned AS (
SELECT
*,
number - ROW_NUMBER() OVER (PARTITION BY id_set) AS grp
FROM atable
WHERE status = 'FREE'
),
counted AS (
SELECT
*,
COUNT(*) OVER (PARTITION BY id_set, grp) AS cnt
FROM partitioned
),
ranked AS (
SELECT
*,
RANK() OVER (ORDER BY id_set, grp) AS rnk
FROM counted
WHERE cnt >= 3
)
SELECT
id_set,
number
FROM ranked
WHERE rnk = 1
;
Here's a demo for this one too: http://sqlfiddle.com/#!1/a2633/2.
If you ever need to make it one set per id_set
, change the RANK()
call like this:
RANK() OVER (PARTITION BY id_set ORDER BY grp) AS rnk
Additionally, you could make the query return the smallest matching set (i.e. first try to return the first set of exactly three consecutive numbers if it exists, otherwise four, five etc.), like this:
RANK() OVER (ORDER BY cnt, id_set, grp) AS rnk
or like this (one per id_set
):
RANK() OVER (PARTITION BY id_set ORDER BY cnt, grp) AS rnk
* The SQL Fiddle demos linked in this answer use the 9.1.8 instance as the 9.2.1 one doesn't appear to be working at the moment.
A simple and fast variant:
SELECT min(number) AS first_number, count(*) AS ct_free
FROM (
SELECT *, number - row_number() OVER (PARTITION BY id_set ORDER BY number) AS grp
FROM tbl
WHERE status = 'FREE'
) x
GROUP BY grp
HAVING count(*) >= 3 -- minimum length of sequence only goes here
ORDER BY grp
LIMIT 1;
Requires a gapless sequence of numbers in
number
(as provided in the question).Works for any number of possible values in
status
besides'FREE'
, even withNULL
.The major feature is to subtract
row_number()
fromnumber
after eliminating non-qualifying rows. Consecutive numbers end up in the samegrp
- andgrp
is also guaranteed to be in ascending order.Then you can
GROUP BY grp
and count the members. Since you seem to want the first occurrence,ORDER BY grp LIMIT 1
and you get starting position and length of the sequence (can be >= n).
Set of rows
To get an actual set of numbers, don't look up the table another time. Much cheaper with generate_series()
:
SELECT generate_series(first_number, first_number + ct_free - 1)
-- generate_series(first_number, first_number + 3 - 1) -- only 3
FROM (
SELECT min(number) AS first_number, count(*) AS ct_free
FROM (
SELECT *, number - row_number() OVER (PARTITION BY id_set ORDER BY number) AS grp
FROM tbl
WHERE status = 'FREE'
) x
GROUP BY grp
HAVING count(*) >= 3
ORDER BY grp
LIMIT 1
) y;
If you actually want a string with leading zeros like you display in your example values, use to_char()
with the FM
(fill mode) modifier:
SELECT to_char(generate_series(8, 11), 'FM000000')
SQL Fiddle with extended test case and both queries.
Closely related answer:
- Select longest continuous sequence
This is a fairly generic way to do this.
Bear in mind it depends on your number
column being consecutive. If it's not a Window function and/or CTE type-solution will probably be needed:
SELECT
number
FROM
mytable m
CROSS JOIN
(SELECT 3 AS consec) x
WHERE
EXISTS
(SELECT 1
FROM mytable
WHERE number = m.number - x.consec + 1
AND status = 'FREE')
AND NOT EXISTS
(SELECT 1
FROM mytable
WHERE number BETWEEN m.number - x.consec + 1 AND m.number
AND status = 'ASSIGNED')