Comment trouver des lacunes dans la numérotation séquentielle dans mysql?

120

Nous avons une base de données avec une table dont les valeurs ont été importées d'un autre système. Il existe une colonne à incrémentation automatique et aucune valeur en double, mais des valeurs manquantes. Par exemple, en exécutant cette requête:

select count(id) from arrc_vouchers where id between 1 and 100

devrait renvoyer 100, mais il renvoie 87 à la place. Y a-t-il une requête que je peux exécuter qui renverra les valeurs des nombres manquants? Par exemple, les enregistrements peuvent exister pour les identifiants 1-70 et 83-100, mais il n'y a pas d'enregistrements avec des identifiants 71-82. Je veux retourner 71, 72, 73, etc.

Est-ce possible?

EmmyS
la source
Cela peut ne pas fonctionner dans MySQL, mais au travail (Oracle), nous avions besoin de quelque chose de similaire. Nous avons écrit un processus stocké qui a pris un nombre comme valeur Max. Le processus stocké a ensuite créé une table temporaire avec une seule colonne. Le tableau contenait tous les nombres de 1 à Max. Ensuite, il a fait une jointure NOT IN entre la table temporaire et notre table d'intérêt. Si vous l'appeliez avec Max = Select max (id) de arrc_vouchers, il renverrait alors toutes les valeurs manquantes.
saunderl
2
Quel est le problème avec des lacunes dans la numérotation? La valeur d'une clé de substitution n'est généralement pas significative; tout ce qui compte, c'est que c'est unique. Si votre application ne peut pas gérer les ID non contigus, c'est probablement un bogue dans l'application, pas dans les données.
Wyzard
4
Dans ce cas, c'est un problème car les données que nous avons héritées de l'ancien système utilisaient le numéro d'auto-incrémentation associé à un enregistrement comme clé pour imprimer sur une carte physique qui est remise aux gens. Ce n'était PAS notre idée. Afin de savoir quelles cartes manquent, nous devons savoir où se trouvent les lacunes dans la numérotation séquentielle.
EmmyS
xaprb.com/blog/2005/12/06/… select l.id + 1 as start from sequence as l left outer join sequence as r on l.id + 1 = r.id where r.id is null;
Vous pouvez utiliser generate series pour générer des nombres de 1 à l'identifiant le plus élevé de votre table. Exécutez ensuite une requête où id n'est pas dans cette série.
Tsvetelin Salutski

Réponses:

170

Mettre à jour

ConfexianMJS a fourni une bien meilleure réponse en termes de performances.

La réponse (pas aussi rapide que possible)

Voici la version qui fonctionne sur une table de toute taille (pas seulement sur 100 lignes):

SELECT (t1.id + 1) as gap_starts_at, 
       (SELECT MIN(t3.id) -1 FROM arrc_vouchers t3 WHERE t3.id > t1.id) as gap_ends_at
FROM arrc_vouchers t1
WHERE NOT EXISTS (SELECT t2.id FROM arrc_vouchers t2 WHERE t2.id = t1.id + 1)
HAVING gap_ends_at IS NOT NULL
  • gap_starts_at - premier identifiant dans l'écart actuel
  • gap_ends_at - dernier identifiant dans l'intervalle actuel
mat
la source
6
Je ne travaille même plus pour cette entreprise, mais c'est la meilleure réponse que j'ai vue et cela vaut vraiment la peine de s'en souvenir pour référence future. Merci!
EmmyS
4
le seul problème avec ceci, c'est qu'il ne "rapporte" pas un éventuel écart initial. Par exemple, si les 5 premiers identifiants manquent (1 à 5), cela ne montre pas cela ... Comment pourrions-nous montrer des lacunes pissibles au tout début?
DiegoDD
Remarque: cette requête ne fonctionne pas sur les tables temporaires. Mon problème était que order numberje recherchais des lacunes dans n'est pas distinct (le tableau stocke les lignes de commande, donc le numéro de commande auquel elles appartiennent se répète pour chaque ligne). 1ère requête: 2812 lignes dans l'ensemble (1 min 31,09 s) . Fabriqué une autre table en sélectionnant des numéros de commande distincts. Votre requête sans mes répétitions: 1009 lignes dans l'ensemble (18,04 sec)
Chris K
1
@DiegoDD Quel est le problème avec SELECT MIN(id) FROM table?
Air
8
A travaillé mais il a fallu environ 5 heures pour fonctionner sur une table avec 700000 enregistrements
Matt
98

Cela a fonctionné pour moi pour trouver les lacunes dans un tableau avec plus de 80 000 lignes:

SELECT
 CONCAT(z.expected, IF(z.got-1>z.expected, CONCAT(' thru ',z.got-1), '')) AS missing
FROM (
 SELECT
  @rownum:=@rownum+1 AS expected,
  IF(@rownum=YourCol, 0, @rownum:=YourCol) AS got
 FROM
  (SELECT @rownum:=0) AS a
  JOIN YourTable
  ORDER BY YourCol
 ) AS z
WHERE z.got!=0;

Résultat:

+------------------+
| missing          |
+------------------+
| 1 thru 99        |
| 666 thru 667     |
| 50000            |
| 66419 thru 66456 |
+------------------+
4 rows in set (0.06 sec)

Notez que l'ordre des colonnes expectedet gotest critique.

Si vous savez que YourColcela ne commence pas à 1 et que cela n'a pas d'importance, vous pouvez remplacer

(SELECT @rownum:=0) AS a

avec

(SELECT @rownum:=(SELECT MIN(YourCol)-1 FROM YourTable)) AS a

Nouveau résultat:

+------------------+
| missing          |
+------------------+
| 666 thru 667     |
| 50000            |
| 66419 thru 66456 |
+------------------+
3 rows in set (0.06 sec)

Si vous devez effectuer une sorte de tâche de script shell sur les ID manquants, vous pouvez également utiliser cette variante afin de produire directement une expression sur laquelle vous pouvez itérer dans bash.

SELECT GROUP_CONCAT(IF(z.got-1>z.expected, CONCAT('$(',z.expected,' ',z.got-1,')'), z.expected) SEPARATOR " ") AS missing
FROM (  SELECT   @rownum:=@rownum+1 AS expected,   IF(@rownum=height, 0, @rownum:=height) AS got  FROM   (SELECT @rownum:=0) AS a   JOIN block   ORDER BY height  ) AS z WHERE z.got!=0;

Cela produit une sortie comme celle-ci

$(seq 1 99) $(seq 666 667) 50000 $(seq 66419 66456)

Vous pouvez ensuite le copier et le coller dans une boucle for dans un terminal bash pour exécuter une commande pour chaque ID

for ID in $(seq 1 99) $(seq 666 667) 50000 $(seq 66419 66456); do
  echo $ID
  # fill the gaps
done

C'est la même chose que ci-dessus, mais c'est à la fois lisible et exécutable. En modifiant la commande "CONCAT" ci-dessus, la syntaxe peut être générée pour d'autres langages de programmation. Ou peut-être même SQL.

ConfexianMJS
la source
8
belle solution, pour moi c'est mieux que la réponse préférée - merci
Wee Zel
6
C'est beaucoup plus efficace que la réponse acceptée.
symcbean
1
beaucoup plus rapide que la réponse acceptée. La seule chose que j'ajouterais est que CONVERT( YourCol, UNSIGNED )cela donnera de meilleurs résultats si YourCol n'est pas déjà un entier.
Barton Chittenden
1
@AlexandreCassagne: Si je comprends bien votre question, je ferais simplement une requête séparée comme celle intégrée pour trouver le min:SELECT MAX(YourCol) FROM YourTable;
ConfexianMJS
1
@temuri Basculer vers la variante GROUP_CONCAT si nécessaire:SELECT IF((z.got-IF(z.over>0, z.over, 0)-1)>z.expected, CONCAT(z.expected,' thru ',(z.got-IF(z.over>0, z.over, 0)-1)), z.expected) AS missing FROM ( SELECT @rownum:=@rownum+1 AS expected, @target-@missing AS under, (@missing:=@missing+IF(@rownum=YourCol, 0, YourCol-@rownum))-@target AS over, IF(@rownum=YourCol, 0, @rownum:=YourCol) AS got FROM (SELECT @rownum:=0, @missing:=0, @target:=10) AS a JOIN YourTable ORDER BY YourCol ) AS z WHERE z.got!=0 AND z.under>0;
ConfexianMJS
11

Requête rapide et sale qui devrait faire l'affaire:

SELECT a AS id, b AS next_id, (b - a) -1 AS missing_inbetween
FROM 
 (
SELECT a1.id AS a , MIN(a2.id) AS b 
FROM arrc_vouchers  AS a1
LEFT JOIN arrc_vouchers AS a2 ON a2.id > a1.id
WHERE a1.id <= 100
GROUP BY a1.id
) AS tab

WHERE 
b > a + 1

Cela vous donnera un tableau montrant l'id qui a des identifiants manquants au-dessus, et next_id qui existe, et combien sont manquants entre ...

 
id next_id missing_inbetween
 1 4 2
68 70 1
75 87 11
Ben
la source
1
Cela a très bien fonctionné pour moi. Merci.! J'ai pu facilement modifier cela pour mes besoins.
Rahim Khoja
Il semble que ce soit la meilleure réponse lors de la recherche du «prochain identifiant» dans les lacunes. Malheureusement, il est EXTRÊMEMENT lent pour les tables avec 10K de lignes. J'attends depuis plus de 10 minutes sur une table de ~ 46K alors qu'avec @ConfexianMJS j'ai obtenu des résultats en moins d'une seconde!
BringBackCommodore64
5

Si vous utilisez un, MariaDBvous disposez d'une option plus rapide (800%) utilisant le moteur de stockage de séquence :

SELECT * FROM seq_1_to_50000 WHERE SEQ NOT IN (SELECT COL FROM TABLE);
Moshe L
la source
2
pour développer cette idée, le maximum de la séquence peut être établi en utilisant "SELECT MAX(column) FROM table"et en définissant une variable à partir du résultat, disons $ MAX ... l'instruction sql peut alors être écrite "SELECT * FROM seq_1_to_". $MAX ." WHERE seq not in (SELECT column FROM table)" ma syntaxe est basée sur php
me_
ou vous pouvez utiliser SELECT @var:= max FROM ....; select * from .. WHERE seq < @max;avec des variables MySQL.
Moshe L
2

Créez une table temporaire avec 100 lignes et une seule colonne contenant les valeurs 1 à 100.

Externe Joignez cette table à votre table arrc_vouchers et sélectionnez les valeurs de colonne unique où l'id arrc_vouchers est nul.

Coder ce aveugle, mais devrait fonctionner.

select tempid from temptable 
left join arrc_vouchers on temptable.tempid = arrc_vouchers.id 
where arrc_vouchers.id is null
Amelvin
la source
OK, 1 - 100 était juste un moyen facile de donner un exemple. Dans ce cas, nous regardons entre 20 000 et 85 000. Alors, est-ce que je crée une table temporaire avec 65 000 lignes numérotées de 20000 à 85000? Et comment dois-je procéder? J'utilise phpMyAdmin; si je mets la valeur par défaut de la colonne à 25000 et que je l'incrémente automatiquement, puis-je simplement insérer 65000 lignes et il démarrera l'incrémentation automatique avec 25000?
EmmyS
J'ai eu une situation similaire (j'ai 100 articles en ordre et j'ai besoin de trouver des articles manquants dans 100). Pour ce faire, j'ai créé une autre table 1-100, puis exécutez cette instruction dessus et cela fonctionne à merveille. Cela remplace une fonction très complexe pour créer des tables temporaires. Juste un conseil pour quelqu'un dans une situation similaire, il est parfois plus rapide de créer une table que des tables temporaires.
newshorts
2

Une solution alternative qui nécessite une requête + du code effectuant un traitement serait:

select l.id lValue, c.id cValue, r.id rValue 
  from 
  arrc_vouchers l 
  right join arrc_vouchers c on l.id=IF(c.id > 0, c.id-1, null)
  left  join arrc_vouchers r on r.id=c.id+1
where 1=1
  and c.id > 0 
  and (l.id is null or r.id is null)
order by c.id asc;

Notez que la requête ne contient aucune sous-sélection dont nous savons qu'elle n'est pas gérée de manière performante par le planificateur de MySQL.

Cela renverra une entrée par centralValue (cValue) qui n'a pas une valeur plus petite (lValue) ou une valeur plus grande (rValue), c'est-à-dire:

lValue |cValue|rValue
-------+------+-------
{null} | 2    | 3      
8      | 9    | {null} 
{null} | 22   | 23     
23     | 24   | {null} 
{null} | 29   | {null} 
{null} | 33   | {null} 


Sans entrer dans plus de détails (nous les verrons dans les paragraphes suivants), cette sortie signifie que:

  • Aucune valeur entre 0 et 2
  • Aucune valeur entre 9 et 22
  • Aucune valeur entre 24 et 29
  • Aucune valeur entre 29 et 33
  • Aucune valeur entre 33 et MAX VALUE

L'idée de base est donc de faire une jointure DROITE et GAUCHE avec la même table pour voir si nous avons des valeurs adjacentes par valeur (c'est-à-dire: si la valeur centrale est '3' alors nous vérifions 3-1 = 2 à gauche et 3 + 1 à right), et quand une ROW a une valeur NULL à RIGHT ou LEFT, nous savons qu'il n'y a pas de valeur adjacente.

La sortie brute complète de ma table est:

select * from arrc_vouchers order by id asc;

0  
2  
3  
4  
5  
6  
7  
8  
9  
22 
23 
24 
29 
33 

Quelques notes:

  1. L'instruction SQL IF dans la condition de jointure est nécessaire si vous définissez le champ 'id' comme UNSIGNED, par conséquent, il ne vous permettra pas de le réduire sous zéro. Ce n'est pas strictement nécessaire si vous conservez la valeur c.value> 0 comme indiqué dans la note suivante, mais je l'inclus comme doc.
  2. Je filtre la valeur centrale zéro car nous ne sommes intéressés par aucune valeur précédente et nous pouvons dériver la valeur de publication de la ligne suivante.
mgo1977
la source
2

S'il y a une séquence ayant un écart de maximum un entre deux nombres (comme 1, 3, 5, 6) alors la requête qui peut être utilisée est:

select s.id+1 from source1 s where s.id+1 not in(select id from source1) and s.id+1<(select max(id) from source1);
  • nom de la table - source1
  • nom de colonne - id
GUPTA DE PRAKHAR
la source
1

basé sur la réponse donnée ci-dessus par Lucek, cette procédure stockée vous permet de spécifier les noms de table et de colonne que vous souhaitez tester pour trouver des enregistrements non contigus - répondant ainsi à la question d'origine et démontrant également comment on pourrait utiliser @var pour représenter des tables & / ou colonnes dans une procédure stockée.

create definer=`root`@`localhost` procedure `spfindnoncontiguous`(in `param_tbl` varchar(64), in `param_col` varchar(64))
language sql
not deterministic
contains sql
sql security definer
comment ''
begin
declare strsql varchar(1000);
declare tbl varchar(64);
declare col varchar(64);

set @tbl=cast(param_tbl as char character set utf8);
set @col=cast(param_col as char character set utf8);

set @strsql=concat("select 
    ( t1.",@col," + 1 ) as starts_at, 
  ( select min(t3.",@col,") -1 from ",@tbl," t3 where t3.",@col," > t1.",@col," ) as ends_at
    from ",@tbl," t1
        where not exists ( select t2.",@col," from ",@tbl," t2 where t2.",@col," = t1.",@col," + 1 )
        having ends_at is not null");

prepare stmt from @strsql;
execute stmt;
deallocate prepare stmt;
end
Professeur Abronsius
la source
1

Je l'ai essayé de différentes manières et la meilleure performance que j'ai trouvée était cette simple requête:

select a.id+1 gapIni
    ,(select x.id-1 from arrc_vouchers x where x.id>a.id+1 limit 1) gapEnd
    from arrc_vouchers a
    left join arrc_vouchers b on b.id=a.id+1
    where b.id is null
    order by 1
;

... une jointure gauche pour vérifier si le prochain identifiant existe, seulement si le suivant s'il n'est pas trouvé, alors la sous-requête trouve l'identifiant suivant qui existe pour trouver la fin de l'écart. Je l'ai fait car une requête avec égal (=) est de meilleures performances que l' opérateur supérieur à (>).

En utilisant sqlfiddle , les performances des autres requêtes ne sont pas si différentes, mais dans une base de données réelle, cette requête ci-dessus résulte 3 fois plus rapidement que les autres.

Le schéma:

CREATE TABLE arrc_vouchers (id int primary key)
;
INSERT INTO `arrc_vouchers` (`id`) VALUES (1),(4),(5),(7),(8),(9),(10),(11),(15),(16),(17),(18),(19),(20),(21),(22),(23),(24),(25),(26),(27),(28),(29)
;

Suivez ci-dessous toutes les requêtes que j'ai faites pour comparer les performances:

select a.id+1 gapIni
    ,(select x.id-1 from arrc_vouchers x where x.id>a.id+1 limit 1) gapEnd
    from arrc_vouchers a
    left join arrc_vouchers b on b.id=a.id+1
    where b.id is null
    order by 1
;
select *, (gapEnd-gapIni) qt
    from (
        select id+1 gapIni
        ,(select x.id from arrc_vouchers x where x.id>a.id limit 1) gapEnd
        from arrc_vouchers a
        order by id
    ) a where gapEnd <> gapIni
;
select id+1 gapIni
    ,(select x.id from arrc_vouchers x where x.id>a.id limit 1) gapEnd
    #,coalesce((select id from arrc_vouchers x where x.id=a.id+1),(select x.id from arrc_vouchers x where x.id>a.id limit 1)) gapEnd
    from arrc_vouchers a
    where id+1 <> (select x.id from arrc_vouchers x where x.id>a.id limit 1)
    order by id
;
select id+1 gapIni
    ,coalesce((select id from arrc_vouchers x where x.id=a.id+1),(select x.id from arrc_vouchers x where x.id>a.id limit 1)) gapEnd
    from arrc_vouchers a
    order by id
;
select id+1 gapIni
    ,coalesce((select id from arrc_vouchers x where x.id=a.id+1),concat('*** GAT *** ',(select x.id from arrc_vouchers x where x.id>a.id limit 1))) gapEnd
    from arrc_vouchers a
    order by id
;

Peut-être que cela aide quelqu'un et utile.

Vous pouvez voir et tester ma requête en utilisant ce sqlfiddle :

http://sqlfiddle.com/#!9/6bdca7/1

lynx_74
la source
0

Bien que tous semblent fonctionner, le jeu de résultats retourne très longtemps lorsqu'il y a 50 000 enregistrements.

Je l'ai utilisé, et il trouve l'écart ou le prochain disponible (dernier utilisé + 1) avec un retour beaucoup plus rapide de la requête.

SELECT a.id as beforegap, a.id+1 as avail
FROM table_name a
where (select b.id from table_name b where b.id=a.id+1) is null
limit 1;
Rob
la source
cela trouve le premier écart qui n'est pas ce que la question demandait.
tirage
0

Probablement pas pertinent, mais je cherchais quelque chose comme celui-ci pour répertorier les lacunes dans une séquence de nombres et j'ai trouvé ce post, qui propose plusieurs solutions différentes en fonction exactement de ce que vous recherchez. Je cherchais le premier espace disponible dans la séquence (c'est-à-dire le prochain numéro disponible), et cela semble fonctionner correctement.

SELECT MIN (l.number_sequence + 1) comme prochainavabile des patients comme l LEFT OUTER JOIN patients comme r sur l.number_sequence + 1 = r.number_sequence WHERE r.number_sequence est NULL. Plusieurs autres scénarios et solutions y discutés, à partir de 2005!

Comment rechercher des valeurs manquantes dans une séquence avec SQL

SScotti
la source