web-dev-qa-db-fra.com

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

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 retourner 100, mais il retourne 87 à la place. Existe-t-il une requête que je peux exécuter qui renvoie les valeurs des nombres manquants? Par exemple, les enregistrements peuvent exister pour les identifiants 1-70 et 83-100, mais il n'y en a aucun dont l'identifiant est 71-82. Je veux retourner 71, 72, 73, etc.

Est-ce possible?

94
EmmyS

Mettre à jour

ConfexianMJS a fourni bien mieuxanswer en termes de performances.

La réponse (pas aussi rapide que possible)

Voici la version qui fonctionne sur les tables de toutes tailles (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'intervalle actuel
  • gap_ends_at - dernier identifiant dans l'intervalle actuel 
152
matt

Cela m'a permis de trouver les lacunes dans un tableau de 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 expected et got est critique.

Si vous savez que YourCol 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 identifiants manquants, vous pouvez également utiliser cette variante afin de générer directement une expression sur laquelle vous pouvez effectuer une itération 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 attendu, SI (@ rownum = height, 0, @rownum: = height) AS OBTENU DE (SELECT @rownum: = 0) COMME un bloc JOIN COMMANDE PAR hauteur O z.got! = 0;

Cela produit une sortie comme si

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

Vous pouvez ensuite le copier et le coller dans une boucle for d'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, sauf que 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.

53
ConfexianMJS

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 une table montrant l'id qui a des id manquants au dessus, et next_id qui existe, et combien il en manque entre ...

  
 id next_id missing_inbetween 
 1 4 2 
 68 70 1 
 75 87 11 
8
Ben

Une autre solution nécessitant une requête + du code pour le 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 car 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 valeur centrale (valeurC) qui n'a pas une valeur inférieure (ValeurI) ou une valeur supérieure (valeurR), 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 les 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

Donc l’idée de base est 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', nous vérifions 3-1 = 2 à gauche et 3 + 1 à à droite), et quand une emprise a une valeur nulle à droite ou à gauche, alors nous savons qu’il n’ya 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 étant UNSIGNED. Par conséquent, elle ne vous permettra pas de la 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'inclue simplement en tant que 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éduire la valeur de publication de la ligne suivante.
2
mgo1977

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

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

Coder cet aveugle, mais devrait fonctionner.

select tempid from temptable 
left join arrc_vouchers on temptable.tempid = arrc_vouchers.id 
where arrc_vouchers.id is null
2
amelvin

sur la base de la réponse donnée précédemment par Lucek, cette procédure stockée vous permet de spécifier les noms de table et de colonne que vous souhaitez tester pour rechercher des enregistrements non contigus - répondant ainsi à la question d'origine et montrant également comment utiliser @var pour représenter des tables et/ou des 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
1
RamRaider

Si vous utilisez une variable MariaDB, vous avez une option plus rapide (800%)

SELECT * FROM seq_1_to_50000 where seq not in (select col from table);

https://mariadb.com/kb/en/mariadb/sequence/

1
Moshe L

Bien que tous semblent fonctionner, l'ensemble de résultats est renvoyé très longtemps lorsqu'il existe 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;
0
Rob

Vous pouvez utiliser generate series pour générer des nombres de 1 à l'id le plus élevé de votre table. Puis lancez une requête où id ne fait pas partie de cette série.

0
Tsvetelin Salutski

Probablement pas pertinent, mais je cherchais quelque chose comme ceci pour lister les lacunes dans une séquence de nombres et ai trouvé ce post qui propose de multiples solutions en fonction de ce que vous recherchez. Je recherchais le premier intervalle disponible dans la séquence (c'est-à-dire le prochain numéro disponible), et cela semble bien fonctionner.

SÉLECTIONNER MIN (séquence de numéro + 1) comme prochaine variable de patients comme l LEFT OUTER JOIN comme patients sur r sur séquence de numéro + 1 = r. nombre de séquence si NULL. Plusieurs autres scénarios et solutions discutés ici, à partir de 2005!

Comment trouver les valeurs manquantes dans une séquence avec SQL

0
sscotti

S'il existe une séquence ayant un espace d'un maximum entre deux nombres (comme 1,3,5,6), la requête pouvant ê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_table - source1
  • nom_colonne - id
0
PRAKHAR GUPTA

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

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 id existe, uniquement si next si n'est pas trouvé, la sous-requête recherche l'id suivant qui existe pour trouver la fin de gap. Je l'ai fait parce que la requête avec égal (=) offre de meilleures performances que l'opérateur supérieur à (>).

En utilisant le sqlfiddle il n'affiche pas les performances des requêtes des autres, mais dans une base de données réelle, cette requête ci-dessus est 3 fois plus rapide 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

0
lynx_74