Trouver «n» numéros gratuits consécutifs dans le tableau


16

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

état du numéro id_set         
-----------------------
1 000001 ATTRIBUÉ
1 000002 GRATUIT
1 000003 ATTRIBUÉ
1 000004 GRATUIT
1 000005 GRATUIT
1 000006 ATTRIBUÉ
1 000007 ATTRIBUÉ
1 000008 GRATUIT
1 000009 GRATUIT
1 000010 GRATUIT
1 000011 ATTRIBUÉ
1 000012 ATTRIBUÉ
1 000013 ATTRIBUÉ
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 de 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 comptant les lignes précédentes pour chaque nombre où status = 'FREE', puis sélectionnez le premier nombre, où count est égal à mon nombre "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

ÉDITER

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

Réponses:


16

C'est un problème de . En supposant qu'il n'y a pas de lacunes ou de doublons dans le même id_setensemble:

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 un lien de démonstration SQL Fiddle * 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 également une démo pour celui-ci: http://sqlfiddle.com/#!1/a2633/2 .

Si jamais vous avez besoin d'en faire un jeu parid_set , changez l' RANK()appel 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.


Merci beaucoup, cela a l'air bien, mais il est possible de le changer pour que seul le premier groupe de numéros soit retourné? Si je le change en cnt> = 2, alors j'obtiens 5 numéros (2 groupes = 2 + 3 numéros)
boobiq

@boobiq: Voulez-vous un par id_setou un seul? Veuillez mettre à jour votre question si cela a été conçu comme faisant partie du début. (Pour que les autres puissent voir toutes les exigences et offrir leurs suggestions ou mettre à jour leurs réponses.)
Andriy M

J'ai édité ma question (après le retour voulu), elle ne sera exécutée que pour un id_set, donc seul le premier groupe possible trouvé
boobiq

10

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 number(comme prévu dans la question).

  • Fonctionne pour n'importe quel nombre de valeurs possibles en statusplus 'FREE', même avec NULL.

  • La principale caractéristique est de soustraire row_number()de numberaprès avoir éliminé les lignes de non-qualification. Les numéros consécutifs finissent dans le même grp- et grpsont également garantis dans l' ordre croissant .

  • Ensuite, vous pouvez GROUP BY grpet compter les membres. Puisque vous semblez vouloir la première occurrence, ORDER BY grp LIMIT 1et 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 en tête comme vous l'affichez dans vos exemples de valeurs, utilisez to_char()avec le FMmodificateur (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:


8

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

Gardez à l'esprit que cela dépend de votre numbercolonne 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')

La déclaration ne fonctionnera pas comme ça dans Postgres.
a_horse_with_no_name

@a_horse_with_no_name N'hésitez pas à corriger cela alors :)
JNK

Aucune fonction de fenêtre, très agréable! Bien que je pense que cela devrait l'être M.number-consec+1(par exemple, pour 10, cela devrait être le cas 10-3+1=8).
Andriy M

@AndriyM Eh bien, ce n'est pas "agréable", il est fragile car il repose sur des valeurs séquentielles de ce numberchamp. Bon appel au calcul, je vais le corriger.
JNK

2
J'ai pris la liberté de corriger la syntaxe de Postgres. le premier EXISTSpourrait être simplifié. Comme nous devons seulement nous assurer que n anciennes lignes existent, nous pouvons supprimer le AND status = 'FREE'. Et je changerais la condition dans le 2ème EXISTSà status <> 'FREE'durcir contre options ajoutées à l'avenir.
Erwin Brandstetter

5

Cela ne renverra que le premier des 3 chiffres. Il ne nécessite pas que les valeurs de numbersoient consécutives. Testé chez 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 'FREE'positions consécutives ou plus ):

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 ;

Dans ce cas, 5 nombres consécutifs - donc la différence doit être 4 ou en d'autres termes count(r3.number) = netr2.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 ;

Vous pensez qu'un produit cartésien à 4 voies est un moyen efficace de le faire?
JNK

Sinon, pouvez-vous l'écrire avec une JOINsyntaxe moderne ?
JNK

Eh bien, je ne voulais pas compter sur les fonctions de la fenêtre et j'ai donné une solution qui fonctionnerait sur n'importe quel 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

J'utilise le curseur pour de meilleures performances - si le SELECT retourne un grand nombre de lignes
Ravi Ramaswamy

J'ai reformaté votre réponse en surlignant le code et en appuyant sur la touche { } bouton de l'éditeur. Prendre plaisir!
jcolebrand

Vous pouvez également modifier votre réponse et expliquer pourquoi vous pensez que le curseur offre de meilleures performances.
jcolebrand

Le curseur est un processus séquentiel. C'est presque comme lire un fichier plat un enregistrement à la fois. Dans l'une des situations, j'ai remplacé la table MEM TEMP par un seul curseur. Cela a réduit le temps de traitement de 26 heures à 6 heures. J'ai dû utiliser neseted WHILE pour parcourir le jeu de résultats.
Ravi Ramaswamy

Avez-vous déjà essayé de tester vos hypothèses? Vous pourriez être surpris. À l'exception des cas d'angle, le SQL simple est le plus rapide.
Erwin Brandstetter
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.