Trova "n" numeri liberi consecutivi dalla tabella


16

Ho una tabella con numeri come questo (lo stato è GRATUITO o ASSEGNATO)

stato numero id_set         
-----------------------
1 000001 ASSEGNATO
1 000002 GRATIS
1 000003 ASSEGNATO
1 000004 GRATIS
1 000005 GRATUITO
1 000006 ASSEGNATO
1 000007 ASSEGNATO
1 000008 GRATIS
1 000009 GRATUITO
1 000010 GRATIS
1 000011 ASSEGNATO
1 000012 ASSEGNATO
1 000013 ASSEGNATO
1 000014 GRATIS
1 000015 ASSEGNATO

e ho bisogno di trovare "n" numeri consecutivi, quindi per n = 3, la query ritornerebbe

1 000008 GRATIS
1 000009 GRATUITO
1 000010 GRATIS

Dovrebbe restituire solo il primo gruppo possibile di ciascun id_set (in effetti, verrebbe eseguito solo per id_set per query)

Stavo controllando le funzioni di WINDOW, ho provato alcune domande del genere COUNT(id_number) OVER (PARTITION BY id_set ROWS UNBOUNDED PRECEDING), ma è tutto ciò che ho ottenuto :) Non riuscivo a pensare alla logica, a come farlo in Postgres.

Stavo pensando di creare una colonna virtuale usando le funzioni di WINDOW contando le righe precedenti per ogni numero dove status = 'FREE', quindi selezionare il primo numero, dove count è uguale al mio numero "n".

O forse raggruppare i numeri per stato, ma solo da un ASSEGNATO ad un altro ASSEGNATO e selezionare solo i gruppi contenenti almeno "n" numeri

MODIFICARE

Ho trovato questa query (e l'ho cambiata un po ')

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

che produce gruppi di numeri LIBERI / ASSEGNATI, ma vorrei avere tutti i numeri solo dal primo gruppo che soddisfa la condizione

SQL Fiddle

Risposte:


16

Questo è un problema di . Supponendo che non ci siano spazi vuoti o duplicati nello stesso id_setset:

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
;

Ecco un link demo * di SQL Fiddle per questa query: http://sqlfiddle.com/#!1/a2633/1 .

AGGIORNARE

Per restituire solo un set, è possibile aggiungere un altro round di classifica:

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
;

Ecco una demo anche per questo: http://sqlfiddle.com/#!1/a2633/2 .

Se hai mai bisogno di renderlo un set perid_set , cambia la RANK()chiamata in questo modo:

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

Inoltre, è possibile fare in modo che la query restituisca il set di corrispondenze più piccolo (ovvero prima provare a restituire il primo set di esattamente tre numeri consecutivi se esiste, altrimenti quattro, cinque ecc.), In questo modo:

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

o come questo (uno per id_set):

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

* Le demo di SQL Fiddle collegate in questa risposta usano l'istanza 9.1.8 poiché quella 9.2.1 non sembra funzionare al momento.


Grazie mille, sembra carino, ma è possibile cambiarlo così viene restituito solo il primo gruppo di numeri? Se lo cambio in cnt> = 2, ottengo 5 numeri (2 gruppi = 2 + 3 numeri)
boobiq

@boobiq: ne vuoi uno per id_seto solo uno? Si prega di aggiornare la domanda se questo era inteso come parte dall'inizio. (In modo che altri possano vedere tutti i requisiti e offrire i loro suggerimenti o aggiornare le loro risposte.)
Andriy M

Ho modificato la mia domanda (dopo aver desiderato il ritorno), verrà eseguita solo per un id_set, quindi è stato trovato solo il primo gruppo possibile
boobiq

10

Una variante semplice e veloce :

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;
  • Richiede una sequenza di numeri gapless in number(come indicato nella domanda).

  • Funziona per qualsiasi numero di valori possibili in statusaggiunta 'FREE', anche con NULL.

  • La caratteristica principale è quella di sottrarre row_number()da numberdopo l'eliminazione delle righe non qualificata. I numeri consecutivi finiscono nello stesso grp- ed grpè anche garantito che siano in ordine crescente .

  • Quindi puoi GROUP BY grpe contare i membri. Dal momento che sembra che desideri la prima occorrenza ORDER BY grp LIMIT 1e ottieni la posizione iniziale e la lunghezza della sequenza (può essere> = n ).

Set di righe

Per ottenere un vero insieme di numeri, non cercare la tabella un'altra volta. Molto più economico 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;

Se desideri effettivamente una stringa con zeri iniziali come quelli visualizzati nei valori di esempio, utilizza to_char()il modificatore FM(modalità di riempimento):

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

SQL Fiddle con caso di test esteso ed entrambe le query.

Risposta strettamente correlata:


8

Questo è un modo abbastanza generico per farlo.

Ricorda che dipende dal fatto che la tua numbercolonna sia consecutiva. Se non è una funzione di Windows e / o una soluzione di tipo CTE sarà probabilmente necessaria:

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 dichiarazione non funzionerà così in Postgres.
a_horse_with_no_name

@a_horse_with_no_name Non esitare a risolverlo allora :)
JNK

Nessuna funzione finestra, molto bella! Anche se penso che dovrebbe essere M.number-consec+1(ad esempio per 10 dovrebbe essere 10-3+1=8).
Andriy M,

@AndriyM Beh, non è "bello", è fragile poiché si basa su valori sequenziali di quel numbercampo. Buona chiamata in matematica, lo correggerò.
JNK,

2
Mi sono preso la libertà di sistemare la sintassi di Postgres. il primo EXISTSpotrebbe essere semplificato. Dal momento che abbiamo solo bisogno di assicurarsi che ogni n esistono righe precedenti, siamo in grado di far cadere il AND status = 'FREE'. E vorrei cambiare la condizione nel 2 ° EXISTSper status <> 'FREE'indurire contro opzioni aggiunte in futuro.
Erwin Brandstetter,

5

Questo restituirà solo il primo dei 3 numeri. Non richiede che i valori di numbersiano consecutivi. Testato su 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 questo mostrerà tutti i numeri (dove ci sono 3 o più 'FREE'posizioni consecutive ):

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 ;

In questo caso 5 numeri consecutivi - quindi la differenza deve essere 4 o in altre parole count(r3.number) = ne r2.number = r1.number + n - 1.

Con join:

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 ;

Pensi che un prodotto cartesiano a 4 vie sia un modo efficace per farlo?
JNK,

In alternativa puoi scriverlo con la JOINsintassi moderna ?
JNK,

Beh, non volevo fare affidamento sulle funzioni della finestra e ho dato una soluzione che avrebbe funzionato su qualsiasi 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

Sto usando il cursore per migliorare le prestazioni - se SELEZIONA restituisce un numero elevato di righe
Ravi Ramaswamy

Ho riformattato la tua risposta evidenziando il codice e premendo il { }pulsante sull'editor. Godere!
jcolebrand

Potresti anche voler modificare la tua risposta e dire perché ritieni che il cursore offra prestazioni migliori.
jcolebrand

Il cursore è un processo sequenziale. È quasi come leggere un file flat un record alla volta. In una delle situazioni, ho sostituito la tabella MEM TEMP con un solo cursore. Ciò ha ridotto i tempi di elaborazione da 26 ore a 6 ore. Ho dovuto usare il comando WHILE cancellato per scorrere il set di risultati.
Ravi Ramaswamy,

Hai mai provato a testare i tuoi presupposti? Potresti essere sorpreso. Ad eccezione di casi angolari, l'SQL normale è il più veloce.
Erwin Brandstetter,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.