Encuentra "n" números libres consecutivos de la tabla


16

Tengo una tabla con números como este (el estado es GRATUITO o ASIGNADO)

id_set number status         
-----------------------
1 000001 ASIGNADO
1 000002 GRATIS
1 000003 ASIGNADO
1 000004 GRATIS
1 000005 GRATIS
1 000006 ASIGNADO
1 000007 ASIGNADO
1 000008 GRATIS
1 000009 GRATIS
1 000010 GRATIS
1 000011 ASIGNADO
1 000012 ASIGNADO
1 000013 ASIGNADO
1 000014 GRATIS
1 000015 ASIGNADO

y necesito encontrar "n" números consecutivos, por lo que para n = 3, la consulta devolvería

1 000008 GRATIS
1 000009 GRATIS
1 000010 GRATIS

Debería devolver solo el primer grupo posible de cada id_set (de hecho, se ejecutaría solo para id_set por consulta)

Estaba comprobando las funciones de WINDOW, intenté algunas consultas como COUNT(id_number) OVER (PARTITION BY id_set ROWS UNBOUNDED PRECEDING), pero eso es todo lo que obtuve :) No podía pensar en la lógica, cómo hacer eso en Postgres.

Estaba pensando en crear una columna virtual usando las funciones de VENTANA contando las filas anteriores para cada número donde status = 'FREE', luego seleccione el primer número, donde count es igual a mi número "n".

O quizás agrupe los números por estado, pero solo de un ASIGNADO a otro ASIGNADO y seleccione solo grupos que contengan al menos "n" números

EDITAR

Encontré esta consulta (y la cambié un poco)

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 produce grupos de números GRATUITOS / ASIGNADOS, pero me gustaría tener todos los números del primer grupo que cumpla la condición

Violín de SQL

Respuestas:


16

Este es un problema de . Asumiendo que no hay espacios o duplicados en el mismo 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
;

Aquí hay un enlace de demostración de SQL Fiddle * para esta consulta: http://sqlfiddle.com/#!1/a2633/1 .

ACTUALIZAR

Para devolver solo un conjunto, puede agregar una ronda más de clasificación:

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
;

Aquí hay una demostración para este también: http://sqlfiddle.com/#!1/a2633/2 .

Si alguna vez necesita para que sea un juego porid_set , cambiar la RANK()llamada como ésta:

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

Además, puede hacer que la consulta devuelva el conjunto coincidente más pequeño (es decir, primero intente devolver el primer conjunto de exactamente tres números consecutivos si existe, de lo contrario, cuatro, cinco, etc.), de esta manera:

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

o como este (uno por id_set):

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

* Las demostraciones de SQL Fiddle vinculadas en esta respuesta usan la instancia 9.1.8 ya que la 9.2.1 no parece estar funcionando en este momento.


Muchas gracias, esto se ve bien, pero es posible cambiarlo para que solo se devuelva el primer grupo de números. Si lo cambio a cnt> = 2, entonces obtengo 5 números (2 grupos = 2 + 3 números)
boobiq

@boobiq: ¿Quieres uno por id_seto solo uno? Actualice su pregunta si esto se entiende como parte del principio. (Para que otros puedan ver los requisitos completos y ofrecer sus sugerencias o actualizar sus respuestas.)
Andriy M

Edité mi pregunta (después del retorno deseado), se ejecutará solo para un id_set, por lo que solo se encontró el primer grupo posible
boobiq

10

Una variante simple y 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;
  • Requiere una secuencia de números sin espacios number(como se proporciona en la pregunta).

  • Funciona para cualquier número de valores posibles statusademás 'FREE', incluso con NULL.

  • La principal característica es restar row_number()de numberdespués de la eliminación de filas que no confieren. Los números consecutivos terminan en el mismo grp, y grptambién se garantiza que están en orden ascendente .

  • Entonces puedes GROUP BY grpy contar los miembros. Ya que parece querer la primera aparición, ORDER BY grp LIMIT 1y obtiene la posición inicial y la longitud de la secuencia (puede ser> = n ).

Conjunto de filas

Para obtener un conjunto real de números, no busque la tabla otra vez. Mucho más barato con 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;

Si realmente desea una cadena con ceros a la izquierda como se muestra en sus valores de ejemplo, use to_char()con el FMmodificador (modo de relleno):

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

SQL Fiddle con caso de prueba extendido y ambas consultas.

Respuesta estrechamente relacionada:


8

Esta es una forma bastante genérica de hacer esto.

Tenga en cuenta que depende de que su numbercolumna sea consecutiva. Si no es una función de Windows y / o una solución de tipo CTE, probablemente será necesaria:

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

La declaración no funcionará así en Postgres.
a_horse_with_no_name

@a_horse_with_no_name Por favor, siéntase libre de arreglar eso entonces :)
JNK

No hay funciones de ventana, muy agradable! Aunque creo que debería ser M.number-consec+1(por ejemplo, para 10 debería serlo 10-3+1=8).
Andriy M

@AndriyM Bueno, no es "agradable", es frágil ya que se basa en valores secuenciales de ese numbercampo. Buena decisión en matemáticas, lo corregiré.
JNK

2
Me tomé la libertad de arreglar la sintaxis de Postgres. El primero EXISTSpodría simplificarse. Como solo necesitamos asegurarnos de que existan n filas anteriores, podemos descartar el AND status = 'FREE'. Y me gustaría cambiar la condición en el segundo EXISTSa status <> 'FREE'endurecer contra opciones añadidas en el futuro.
Erwin Brandstetter

5

Esto devolverá solo el primero de los 3 números. No requiere que los valores de numbersean consecutivos. Probado en 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 ;

Y esto mostrará todos los números (donde hay 3 o más 'FREE'posiciones 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 ;

En este caso, 5 números consecutivos, por lo tanto, la diferencia debe ser 4 o, en otras palabras, count(r3.number) = ny r2.number = r1.number + n - 1.

Con une:

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 ;

¿Crees que un producto cartesiano de 4 vías es una manera eficiente de hacer esto?
JNK

Alternativamente, ¿puedes escribirlo con una JOINsintaxis moderna ?
JNK

Bueno, no quería confiar en las funciones de la ventana y di una solución que funcionaría en cualquier 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

Estoy usando el cursor para un mejor rendimiento, si SELECT devuelve una gran cantidad de filas
Ravi Ramaswamy,

Cambié el formato de su respuesta resaltando el código y presionando el { }botón en el editor. ¡Disfrutar!
jcolebrand

También es posible que desee editar su respuesta y decir por qué cree que el cursor proporciona un mejor rendimiento.
jcolebrand

El cursor es un proceso secuencial. Es casi como leer un archivo plano un registro a la vez. En una de las situaciones, reemplacé la tabla MEM TEMP con un solo cursor. Esto redujo el tiempo de procesamiento de 26 horas a 6 horas. Tuve que usar WHILE anidado para recorrer el conjunto de resultados.
Ravi Ramaswamy

¿Alguna vez has intentado probar tus supuestos? Puede que te sorprendas. Excepto en los casos de esquina, SQL simple es el más rápido.
Erwin Brandstetter
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.