web-dev-qa-db-fra.com

Trouver "n" numéros gratuits consécutifs dans le tableau

J'ai un tableau avec des chiffres comme celui-ci (le statut est GRATUIT ou ASSIGNÉ)

 id_set numéro statut 
 ----------------------- 
 1 000001 ATTRIBUÉ 
 1 000002 GRATUIT 
 1 000003 ASSIGNÉ 
 1 000004 GRATUIT 
 1 000005 GRATUIT 
 1 000006 ASSIGNÉ 
 1 000007 ASSIGNÉ 
 1 000008 GRATUIT 
 1 000009 GRATUIT 
 1 000010 GRATUIT 
 1 000011 ASSIGNÉ 
 1 000012 ASSIGNÉ 
 1 000013 ASSIGNÉ 
 1 000014 GRATUIT 
 1 000015 ATTRIBUÉ

et j'ai besoin de trouver "n" nombres consécutifs, donc pour n = 3, la requête retournerait

 1 000008 GRATUIT 
 1 000009 GRATUIT 
 1 000010 GRATUIT 

Il ne devrait renvoyer que le premier groupe possible de chaque id_set (en fait, il ne serait exécuté que pour id_set par requête)

Je vérifiais les fonctions WINDOW, essayais quelques requêtes comme COUNT(id_number) OVER (PARTITION BY id_set ROWS UNBOUNDED PRECEDING), mais c'est tout ce que j'ai eu :) Je ne pouvais pas penser à la logique, comment faire cela dans Postgres.

Je pensais à créer une colonne virtuelle en utilisant les fonctions WINDOW en comptant les lignes précédentes pour chaque numéro où status = 'FREE', puis sélectionnez le premier nombre, où count est égal à mon numéro "n".

Ou peut-être regrouper les numéros par statut, mais uniquement d'un ASSIGNÉ à un autre ASSIGNÉ et sélectionner uniquement les groupes contenant au moins "n" numéros

[~ # ~] modifier [~ # ~]

J'ai trouvé cette requête (et l'ai un peu modifiée)

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

qui produit des groupes de numéros GRATUITS/ATTRIBUÉS, mais j'aimerais avoir tous les numéros du seul premier groupe qui remplit la condition

SQL Fiddle

17
boobiq

C'est un problème lacunes-et-îles . En supposant qu'il n'y ait pas de lacunes ou de doublons dans le même ensemble 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
;

Voici une démonstration SQL Fiddle* lien pour cette requête: http://sqlfiddle.com/#!1/a2633/1 .

[~ # ~] mise à jour [~ # ~]

Pour renvoyer un seul ensemble, vous pouvez ajouter un autre tour de classement:

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
;

Voici une démo pour celui-ci aussi: http://sqlfiddle.com/#!1/a2633/2 .

Si jamais vous avez besoin d'en faire un ensemble par id_set, changez l'appel RANK() comme ceci:

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

De plus, vous pouvez faire en sorte que la requête renvoie le plus petit ensemble correspondant (c'est-à-dire essayer d'abord de renvoyer le premier ensemble d'exactement trois nombres consécutifs s'il existe, sinon quatre, cinq, etc.), comme ceci:

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

ou comme ça (un par id_set):

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

* Les démos SQL Fiddle liées dans cette réponse utilisent l'instance 9.1.8 car celle 9.2.1 ne semble pas fonctionner pour le moment.

17
Andriy M

Une variante simple et rapide :

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;
  • Nécessite une séquence de chiffres sans intervalle dans number (comme prévu dans la question).

  • Fonctionne pour n'importe quel nombre de valeurs possibles dans status en plus de 'FREE', Même avec NULL.

  • La principale caractéristique est de soustraire row_number() de number après avoir éliminé les lignes non éligibles. Les nombres consécutifs se retrouvent dans le même grp - et grp est également garanti d'être dans ordre croissant.

  • Ensuite, vous pouvez GROUP BY grp Et compter les membres. Puisque vous semblez vouloir l'occurrence d'abord, ORDER BY grp LIMIT 1 Et vous obtenez la position de départ et la longueur de la séquence (peut être> = n) .

Ensemble de rangées

Pour obtenir un ensemble réel de nombres, ne recherchez pas le tableau une autre fois. Beaucoup moins cher avec 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 vous voulez réellement une chaîne avec des zéros non significatifs comme vous l'affichez dans vos valeurs d'exemple, utilisez to_char() avec le modificateur FM (mode de remplissage):

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

SQL Fiddle avec cas de test étendu et les deux requêtes.

Réponse étroitement liée:

10
Erwin Brandstetter

C'est une façon assez générique de le faire.

Gardez à l'esprit que cela dépend de votre colonne number consécutive. Si ce n'est pas une fonction Window et/ou une solution de type CTE sera probablement nécessaire:

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

Cela ne renverra que le premier des 3 chiffres. Il ne nécessite pas que les valeurs de number soient consécutives. Testé à 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 ;

Et cela montrera tous les nombres (où il y a 3 ou plus 'FREE' positions):

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 ;
5
ypercubeᵀᴹ
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 ;

Dans ce cas, 5 nombres consécutifs - donc la différence doit être 4 ou en d'autres termes count(r3.number) = n et r2.number = r1.number + n - 1.

Avec jointures:

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 ;
0
Ununoctium