Encontre "n" números gratuitos consecutivos da tabela


16

Eu tenho uma tabela com números como este (o status é GRATUITO ou ATRIBUÍDO)

id_set number status         
-----------------------
1 000001 ATRIBUÍDO
1 000002 GRÁTIS
1 000003 ATRIBUÍDO
1 000004 GRÁTIS
1 000005 GRÁTIS
1 000006 ATRIBUÍDO
1 000007 ATRIBUÍDO
1 000008 GRÁTIS
1 000009 GRÁTIS
1 000010 GRÁTIS
1 000011 ATRIBUÍDO
1 000012 ATRIBUÍDO
1 000013 ATRIBUÍDO
1 000014 GRÁTIS
1 000015 ATRIBUÍDO

e eu preciso encontrar "n" números consecutivos, então para n = 3, a consulta retornaria

1 000008 GRÁTIS
1 000009 GRÁTIS
1 000010 GRÁTIS

Ele deve retornar apenas o primeiro grupo possível de cada id_set (na verdade, seria executado apenas para id_set por consulta)

Eu estava checando as funções do WINDOW, tentei algumas consultas COUNT(id_number) OVER (PARTITION BY id_set ROWS UNBOUNDED PRECEDING), mas foi tudo o que consegui :) Não consegui pensar em lógica, como fazer isso no Postgres.

Eu estava pensando em criar uma coluna virtual usando as funções do WINDOW, contando as linhas anteriores para cada número em que status = 'FREE' e, em seguida, selecione o primeiro número, em que a contagem é igual ao meu número "n".

Ou talvez agrupe números por status, mas apenas de um ASSIGNED para outro ASSIGNED e selecione apenas grupos que contenham pelo menos "n" números

EDITAR

Encontrei esta consulta (e a alterei um pouco)

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

que produz grupos de números GRATUITOS / ATRIBUÍDOS, mas eu gostaria de ter todos os números do primeiro grupo que atenda à condição

SQL Fiddle

Respostas:


16

Este é um problema de . Supondo que não haja lacunas ou duplicatas no mesmo id_setconjunto:

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
;

Aqui está um link de demonstração do SQL Fiddle * para esta consulta: http://sqlfiddle.com/#!1/a2633/1 .

ATUALIZAR

Para retornar apenas um conjunto, você pode adicionar mais uma rodada de classificação:

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
;

Aqui está uma demonstração para este também: http://sqlfiddle.com/#!1/a2633/2 .

Se você precisar definir um conjunto porid_set , altere a RANK()chamada da seguinte maneira:

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

Além disso, você pode fazer com que a consulta retorne o menor conjunto correspondente (ou seja, primeiro tente retornar o primeiro conjunto com exatamente três números consecutivos, se existir, caso contrário, quatro, cinco etc.), assim:

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

ou assim (um por id_set):

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

* As demos do SQL Fiddle vinculadas nesta resposta usam a instância 9.1.8, pois a 9.2.1 não parece estar funcionando no momento.


Muito obrigado, isso parece legal, mas é possível alterá-lo para que apenas o primeiro grupo de números seja retornado? Se eu mudar para cnt> = 2, então eu recebo 5 números (2 grupos = 2 + 3 números)
boobiq

@boobiq: Você quer um por id_setou apenas um? Atualize sua pergunta se isso foi feito como parte desde o início. (Para que outras pessoas possam ver todos os requisitos e oferecer suas sugestões ou atualizar suas respostas.)
Andriy M

Eu editei a minha pergunta (após o retorno queria), ele será executado apenas por uma id_set, então apenas o primeiro possível grupo encontrou
boobiq

10

Uma variante simples e rápida :

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;
  • Requer uma sequência contínua de números em number(conforme fornecido na pergunta).

  • Funciona para qualquer número de valores possíveis em statusalém disso 'FREE', mesmo com NULL.

  • A principal característica é subtrair row_number()a partir de numberdepois de eliminar linhas não-qualificadas. Os números consecutivos terminam na mesma grp- e grptambém é garantido que esteja em ordem crescente .

  • Então você pode GROUP BY grpe contar os membros. Como você parece querer a primeira ocorrência ORDER BY grp LIMIT 1e obtém a posição inicial e o comprimento da sequência (pode ser> = n ).

Conjunto de linhas

Para obter um conjunto real de números, não procure a tabela outra vez. Muito mais barato com 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;

Se você realmente deseja uma string com zeros à esquerda, como os valores de exemplo, use to_char()com o FMmodificador (modo de preenchimento):

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

SQL Fiddle com caso de teste estendido e ambas as consultas.

Resposta intimamente relacionada:


8

Essa é uma maneira bastante genérica de fazer isso.

Lembre-se de que depende da sua numbercoluna ser consecutiva. Se não for uma função do Windows e / ou uma solução do tipo CTE provavelmente será necessária:

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')

A declaração não funcionará assim no Postgres.
a_horse_with_no_name

@a_horse_with_no_name Sinta-se livre para corrigir isso, então :)
JNK

Sem funções de janela, muito bom! Embora eu ache que deveria ser M.number-consec+1(por exemplo, para 10, seria necessário 10-3+1=8).
precisa

@AndriyM Bem, não é "legal", é frágil, pois se baseia em valores seqüenciais desse numbercampo. Boa chamada na matemática, eu vou corrigi-la.
JNK

2
Tomei a liberdade de corrigir a sintaxe do Postgres. o primeiro EXISTSpoderia ser simplificado. Uma vez que só precisa ter certeza de qualquer n existem linhas anteriores, podemos largar o AND status = 'FREE'. E eu gostaria de mudar a condição na 2ª EXISTSpara status <> 'FREE'a endurecer-lo contra opções adicionadas no futuro.
Erwin Brandstetter 18/03/2013

5

Isso retornará apenas o primeiro dos três números. Não requer que os valores de numbersejam consecutivos. Testado no 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 ;

E isso mostrará todos os números (onde existem 3 ou mais 'FREE'posições consecutivas ):

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 ;

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 ;

Nesse caso, 5 números consecutivos - portanto, a diferença deve ser 4 ou, em outras palavras, count(r3.number) = ne r2.number = r1.number + n - 1.

Com junções:

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 ;

Você acha que um produto cartesiano de quatro vias é uma maneira eficiente de fazer isso?
JNK

Como alternativa, você pode escrevê-lo com JOINsintaxe moderna ?
JNK

Bem, eu não queria confiar nas funções da janela e dei uma solução que funcionaria em qualquer sql-db.
Ununoctium 19/03/2013

-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

Eu estou usando cursor para um melhor desempenho - deve o SELECT retornar grande número de linhas
Ravi Ramaswamy

Reformatei sua resposta destacando o código e pressionando o { }botão no editor. Desfrutar!
Jcolebrand

Você também pode editar sua resposta e dizer por que acha que o cursor oferece melhor desempenho.
Jcolebrand

O cursor é um processo seqüencial. É quase como ler um arquivo simples, um registro de cada vez. Em uma das situações, substituí a tabela MEM TEMP por um único cursor. Isso reduziu o tempo de processamento de 26 horas para 6 horas. Eu tive que usar neseted WHILE para percorrer o conjunto de resultados.
Ravi Ramaswamy

Você já tentou testar suas suposições? Você pode se surpreender. Exceto nos casos de canto, o SQL simples é o mais rápido.
Erwin Brandstetter 21/03
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.