Найти «n» последовательных свободных номеров из таблицы

16

У меня есть таблица с такими номерами (статус БЕСПЛАТНЫЙ или НАЗНАЧЕН)

Идентификационный номер         
-----------------------
1 000001 НАЗНАЧЕН
1 000002 БЕСПЛАТНО
1 000003 НАЗНАЧЕН
1 000004 БЕСПЛАТНО
1 000005 БЕСПЛАТНО
1 000006 НАЗНАЧЕН
1 000007 НАЗНАЧЕН
1 000008 БЕСПЛАТНО
1 000009 БЕСПЛАТНО
1 000010 БЕСПЛАТНО
1 000011 НАЗНАЧЕН
1 000012 НАЗНАЧЕН
1 000013 НАЗНАЧЕН
1 000014 БЕСПЛАТНО
1 000015 НАЗНАЧЕН

и мне нужно найти "n" последовательных чисел, поэтому для n = 3 запрос будет возвращать

1 000008 БЕСПЛАТНО
1 000009 БЕСПЛАТНО
1 000010 БЕСПЛАТНО

Он должен возвращать только первую возможную группу каждого id_set (фактически, он будет выполняться только для id_set для запроса)

Я проверял функции WINDOW, пробовал некоторые запросы вроде COUNT(id_number) OVER (PARTITION BY id_set ROWS UNBOUNDED PRECEDING), но это все, что я получил :) Я не мог придумать логики, как это сделать в Postgres.

Я думал о создании виртуального столбца, используя функции WINDOW, считая предыдущие строки для каждого числа, где status = 'FREE', затем выбираю первое число, где count равно моему "n" числу.

Или, может быть, группировать номера по статусу, но только от одного ASSIGNED к другому ASSIGNED и выбирать только группы, содержащие как минимум "n" номеров

РЕДАКТИРОВАТЬ

Я нашел этот запрос (и немного его изменил)

WITH q AS
(
  SELECT *,
         ROW_NUMBER() OVER (PARTITION BY id_set, status ORDER BY number) AS rnd,
         ROW_NUMBER() OVER (PARTITION BY id_set ORDER BY number) AS rn
  FROM numbers
)
SELECT id_set,
       MIN(number) AS first_number,
       MAX(number) AS last_number,
       status,
       COUNT(number) AS numbers_count
FROM q
GROUP BY id_set,
         rnd - rn,
         status
ORDER BY
     first_number

который производит группы номеров FREE / ASSIGNED, но я хотел бы, чтобы все номера были только из первой группы, которая удовлетворяет условию

SQL Fiddle

boobiq
источник

Ответы:

16

Это проблема . Предполагая, что в одном id_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
;

Вот ссылка на демонстрацию SQL Fiddle * для этого запроса: http://sqlfiddle.com/#!1/a2633/1 .

ОБНОВИТЬ

Чтобы вернуть только один набор, вы можете добавить еще один раунд рейтинга:

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
;

Вот демо для этого тоже: http://sqlfiddle.com/#!1/a2633/2 .

Если вам когда-нибудь понадобится сделать один набор на одинid_set , измените RANK()вызов следующим образом:

RANK() OVER (PARTITION BY id_set ORDER BY grp) AS rnk

Кроме того, вы можете сделать так, чтобы запрос возвращал наименьший соответствующий набор (т.е. сначала попытайтесь вернуть первый набор из ровно трех последовательных чисел, если он существует, в противном случае четыре, пять и т. Д.), Например так:

RANK() OVER (ORDER BY cnt, id_set, grp) AS rnk

или вот так (по одному на каждый id_set):

RANK() OVER (PARTITION BY id_set ORDER BY cnt, grp) AS rnk

* Демонстрации SQL Fiddle, связанные в этом ответе, используют экземпляр 9.1.8, поскольку в данный момент 9.2.1, похоже, не работает.

Андрей М
источник
Спасибо большое, это выглядит красиво, но возможно изменить это так, чтобы возвращалась только первая группа чисел? Если я поменяю его на cnt> = 2, то получу 5 номеров (2 группы = 2 + 3 номера)
boobiq
@boobiq: Вы хотите один за id_setили только один? Пожалуйста, обновите ваш вопрос, если это подразумевалось как его часть с самого начала. (Чтобы другие могли увидеть все требования и предложить свои предложения или обновить свои ответы.)
Андрей М
Я отредактировал свой вопрос (после того, как хотел вернуть), он будет выполнен только для одного id_set, поэтому найдена только первая возможная группа
boobiq
10

Простой и быстрый вариант:

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;
  • Требуется последовательная последовательность чисел в number(как предусмотрено в вопросе).

  • Работает для любого числа возможных значений, statusкроме того 'FREE', даже с NULL.

  • Главная особенность является вычитать row_number()из numberпосле устранения неквалификационных строк. Последовательные числа заканчиваются тем же grp- и grpтакже гарантированно будут в возрастающем порядке .

  • Тогда можно GROUP BY grpи посчитать участников. Поскольку вы, кажется, хотите первое вхождение, ORDER BY grp LIMIT 1вы получаете начальную позицию и длину последовательности (может быть> = n ).

Набор строк

Чтобы получить фактический набор чисел, не ищите таблицу в другой раз. Гораздо дешевле с 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;

Если вы действительно хотите строку с ведущими нулями , как вы показываете в вашем примере значений, используйте to_char()с FMмодификатором (режим заполнения):

SELECT to_char(generate_series(8, 11), 'FM000000')

SQL Fiddle с расширенным тестовым набором и обоими запросами.

Тесно связанный ответ:

Эрвин Брандштеттер
источник
8

Это довольно общий способ сделать это.

Имейте в виду, это зависит от того, является ли ваша numberколонка последовательной. Если это не оконная функция и / или, возможно, потребуется решение типа CTE:

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')
JNK
источник
Объявление не будет так работать в Postgres.
a_horse_with_no_name
@a_horse_with_no_name Пожалуйста, не стесняйтесь исправить это тогда :)
JNK
Нет оконных функций, очень приятно! Хотя я думаю, что это должно быть M.number-consec+1(например, для 10 это должно быть 10-3+1=8).
Андрей М
@AndriyM Ну, это не «хорошо», это хрупко, поскольку оно опирается на последовательные значения этого numberполя. Хороший вызов по математике, я исправлю это.
JNK
2
Я позволил себе исправить синтаксис для Postgres. первое EXISTSможно упростить. Поскольку нам нужно только убедиться, что существуют n предыдущих строк, мы можем отбросить AND status = 'FREE'. И я хотел бы изменить состояние во 2 - EXISTSк status <> 'FREE'твердеть его от добавленных вариантов в будущем.
Эрвин Брандштеттер,
5

Это вернет только первое из 3 чисел. Это не требует, чтобы значения numberбыли последовательными. Протестировано в SQL-Fiddle :

WITH cte3 AS
( SELECT
    *,
    COUNT(CASE WHEN status = 'FREE' THEN 1 END) 
        OVER (PARTITION BY id_set ORDER BY number
              ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING)
      AS cnt
  FROM atable
)
SELECT
  id_set, number
FROM cte3
WHERE cnt = 3 ;

И это покажет все числа (где есть 3 или более последовательных 'FREE'позиций):

WITH cte3 AS
( SELECT
    *,
    COUNT(CASE WHEN status = 'FREE' THEN 1 END) 
        OVER (PARTITION BY id_set ORDER BY number
              ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING)
      AS cnt
  FROM atable
)
, cte4 AS
( SELECT
    *, 
    MAX(cnt) 
        OVER (PARTITION BY id_set ORDER BY number
              ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
      AS maxcnt
  FROM cte3
)
SELECT
  id_set, number
FROM cte4
WHERE maxcnt >= 3 ;
ypercubeᵀᴹ
источник
0
select r1.number from some_table r1, 
some_table r2,
some_table r3,
some_table r4 
where r3.number <= r2.number 
and r3.number >= r1.number 
and r3.status = 'FREE' 
and r2.number = r1.number + 4 
and r4.number <= r2.number 
and r4.number >= r1.number 
and r4.status = 'ASSIGNED'
group by r1.number, r2.number having count(r3.number) = 5 and count(r4.number) = 0 order by r1.number asc limit 1 ;

В этом случае 5 последовательных чисел - поэтому разница должна быть 4 или другими словами count(r3.number) = nи r2.number = r1.number + n - 1.

С присоединениями:

select r1.number 
from some_table r1 join 
 some_table r2 on (r2.number = r1.number + :n -1) join
 some_table r3 on (r3.number <= r2.number and r3.number >= r1.number) join
 some_table r4 on (r4.number <= r2.number and r4.number >= r1.number)
where  
 r3.status = 'FREE' and
 r4.status = 'ASSIGNED'
group by r1.number, r2.number having count(r3.number) = :n and count(r4.number) = 0 order by r1.number asc limit 1 ;
унуноктий
источник
Вы думаете, что четырехстороннее декартово произведение является эффективным способом сделать это?
JNK
Или вы можете написать его с современным JOINсинтаксисом?
JNK
Ну, я не хотел полагаться на оконные функции и дал решение, которое будет работать на любом sql-db.
Ununoctium
-1
CREATE TABLE #ConsecFreeNums
(
     id_set BIGINT
    ,number VARCHAR(10)
    ,status VARCHAR(10)
)

CREATE TABLE #ConsecFreeNumsResult
(
     Seq    INT
    ,id_set BIGINT
    ,number VARCHAR(10)
    ,status VARCHAR(10)
)

INSERT #ConsecFreeNums
SELECT 1, '000002', 'FREE' UNION
SELECT 1, '000003', 'ASSIGNED' UNION
SELECT 1, '000004', 'FREE' UNION
SELECT 1, '000005', 'FREE' UNION
SELECT 1, '000006', 'ASSIGNED' UNION
SELECT 1, '000007', 'ASSIGNED' UNION
SELECT 1, '000008', 'FREE' UNION
SELECT 1, '000009', 'FREE' UNION
SELECT 1, '000010', 'FREE' UNION
SELECT 1, '000011', 'ASSIGNED' UNION
SELECT 1, '000012', 'ASSIGNED' UNION
SELECT 1, '000013', 'ASSIGNED' UNION
SELECT 1, '000014', 'FREE' UNION
SELECT 1, '000015', 'ASSIGNED'

DECLARE @id_set AS BIGINT, @number VARCHAR(10), @status VARCHAR(10), @number_count INT, @number_count_check INT

DECLARE ConsecFreeNumsCursor CURSOR FAST_FORWARD FOR
SELECT
       id_set
      ,number
      ,status
 FROM
      #ConsecFreeNums
WHERE id_set = 1
ORDER BY number

OPEN ConsecFreeNumsCursor

FETCH NEXT FROM ConsecFreeNumsCursor INTO @id_set, @number, @status

SET @number_count_check = 3
SET @number_count = 0

WHILE @@FETCH_STATUS = 0
BEGIN
    IF @status = 'ASSIGNED'
    BEGIN
        IF @number_count = @number_count_check
        BEGIN
            SELECT 'Results'
            SELECT * FROM #ConsecFreeNumsResult ORDER BY number
            BREAK
        END
        SET @number_count = 0
        TRUNCATE TABLE #ConsecFreeNumsResult
    END
    ELSE
    BEGIN
        SET @number_count = @number_count + 1
        INSERT #ConsecFreeNumsResult SELECT @number_count, @id_set, @number, @status
    END
    FETCH NEXT FROM ConsecFreeNumsCursor INTO @id_set, @number, @status
END

CLOSE ConsecFreeNumsCursor
DEALLOCATE ConsecFreeNumsCursor

DROP TABLE #ConsecFreeNums
DROP TABLE #ConsecFreeNumsResult
Рави Рамасвами
источник
Я использую курсор для повышения производительности - если SELECT возвращает большое количество строк
Ravi Ramaswamy
Я переформатировал ваш ответ, выделив код и нажав { }кнопку в редакторе. Наслаждайтесь!
Jcolebrand
Вы также можете отредактировать свой ответ и сообщить, почему, по вашему мнению, курсор обеспечивает лучшую производительность.
Jcolebrand
Курсор - это последовательный процесс. Это почти как чтение плоского файла по одной записи за раз. В одной из ситуаций я заменил таблицу MEM TEMP одним курсором. Это сократило время обработки с 26 часов до 6 часов. Я должен был использовать neseted WHILE, чтобы пройти через набор результатов.
Рави Рамасвами
Вы когда-нибудь пытались проверить свои предположения? Вы можете быть удивлены. За исключением угловых случаев простой SQL является самым быстрым.
Эрвин Брандштеттер