¿Cómo encontrar espacios en la numeración secuencial en mysql?

119

Tenemos una base de datos con una tabla cuyos valores fueron importados de otro sistema. Hay una columna de incremento automático y no hay valores duplicados, pero faltan valores. Por ejemplo, ejecutando esta consulta:

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

debería devolver 100, pero devuelve 87 en su lugar. ¿Hay alguna consulta que pueda ejecutar que devuelva los valores de los números faltantes? Por ejemplo, los registros pueden existir para id 1-70 y 83-100, pero no hay registros con id de 71-82. Quiero devolver 71, 72, 73, etc.

es posible?

EmmyS
fuente
Puede que esto no funcione en MySQL, pero en el trabajo (Oracle) necesitábamos algo similar. Escribimos un proceso almacenado que tomó un número como valor máximo. Luego, Stored Proc creó una tabla temporal con una sola columna. La tabla contenía todos los números del 1 al Max. Luego hizo una unión NOT IN entre la mesa temporal y nuestra mesa de interés. Si lo llamó con Max = Select max (id) from arrc_vouchers, devolvería todos los valores faltantes.
saunderl
2
¿Qué tiene de malo tener espacios en la numeración? El valor de una clave sustituta generalmente no es significativo; todo lo que importa es que es único. Si su aplicación no puede manejar ID no contiguos, probablemente sea un error en la aplicación, no en los datos.
Wyzard
4
En este caso, es un problema porque los datos que heredamos del sistema anterior usaban el número de incremento automático asociado con un registro como clave para imprimir en una tarjeta física que se está entregando a las personas. Esta NO fue nuestra idea. Para saber qué tarjetas faltan, necesitamos saber dónde están los espacios en la numeración secuencial.
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;
Puede usar generar series para generar números desde 1 hasta la identificación más alta de su tabla. Luego, ejecute una consulta donde la identificación no esté en esta serie.
Tsvetelin Salutski

Respuestas:

170

Actualizar

ConfexianMJS proporcionó una respuesta mucho mejor en términos de rendimiento.

La respuesta (no tan rápida como sea posible)

Aquí está la versión que funciona en una mesa de cualquier tamaño (no solo en 100 filas):

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 - primera identificación en la brecha actual
  • gap_ends_at - última identificación en la brecha actual
mate
fuente
6
Ya ni siquiera trabajo para esa empresa, pero esta es la mejor respuesta que he visto y definitivamente vale la pena recordarla para referencia futura. ¡Gracias!
EmmyS
4
el único problema con esto es que no "informa" una posible brecha inicial. por ejemplo, si faltan los primeros 5 identificadores (del 1 al 5), no muestra que ... ¿Cómo podríamos mostrar huecos posibles al principio?
DiegoDD
Nota: esta consulta no funciona en tablas temporales. Mi problema fue order numberque estaba buscando espacios en blanco no es distinto (la tabla almacena líneas de pedido, por lo que el número de pedido al que pertenecen se repite para cada línea). Primera consulta: 2812 filas en conjunto (1 min 31,09 seg) . Hizo otra tabla seleccionando distintos números de orden. Su consulta sin mis repeticiones: 1009 filas en conjunto (18.04 segundos)
Chris K
1
@DiegoDD ¿Qué pasa con SELECT MIN(id) FROM table?
aire el
8
Funcionó, pero tardó aproximadamente 5 horas en ejecutarse en una mesa con 700000 registros
Matt
98

Esto me funcionó para encontrar los espacios en una tabla con más de 80k filas:

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;

Resultado:

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

Tenga en cuenta que el orden de las columnas expectedy gotes fundamental.

Si sabe que YourColno comienza en 1 y eso no importa, puede reemplazar

(SELECT @rownum:=0) AS a

con

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

Nuevo resultado:

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

Si necesita realizar algún tipo de tarea de script de shell en los ID que faltan, también puede usar esta variante para producir directamente una expresión sobre la que pueda iterar en 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;

Esto produce una salida como esta

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

Luego puede copiarlo y pegarlo en un bucle for en una terminal bash para ejecutar un comando para cada ID

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

Es lo mismo que el anterior, solo que es legible y ejecutable. Al cambiar el comando "CONCAT" anterior, se puede generar sintaxis para otros lenguajes de programación. O tal vez incluso SQL.

ConfexianMJS
fuente
8
buena solución, para mí es mejor que la respuesta preferida - gracias
Wee Zel
6
Es mucho más eficiente que la respuesta aceptada.
symcbean
1
mucho más rápido que la respuesta aceptada. Lo único que agregaría es que CONVERT( YourCol, UNSIGNED )dará mejores resultados si YourCol no es ya un número entero.
Barton Chittenden
1
@AlexandreCassagne: Si entiendo su pregunta correctamente, simplemente haría una consulta separada como la incrustada para encontrar el mínimo:SELECT MAX(YourCol) FROM YourTable;
ConfexianMJS
1
@temuri Cambie a la variante GROUP_CONCAT si es necesario: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

Consulta rápida y sucia que debería hacer el truco:

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

Esto le dará una tabla que muestra la identificación a la que le faltan identificadores encima, y ​​el next_id que existe, y cuántos faltan entre ...

 
id next_id missing_inbetween
 1 4 2
68 70 1
75 87 11
Ben
fuente
1
Esto funciono muy bien para mi. Gracias.! Pude modificar esto fácilmente para mis propósitos.
Rahim Khoja
Parece que esta es la mejor respuesta cuando se busca la 'siguiente identificación' en los espacios. Desafortunadamente, es EXTREMADAMENTE lento para tablas con 10K de filas. ¡He estado esperando más de 10 minutos en una mesa de ~ 46K mientras que con @ConfexianMJS obtuve resultados en menos de un segundo!
BringBackCommodore64
5

Si está usando un MariaDB, tiene una opción más rápida (800%) usando el motor de almacenamiento de secuencias :

SELECT * FROM seq_1_to_50000 WHERE SEQ NOT IN (SELECT COL FROM TABLE);
Moshe L
fuente
2
para expandir esta idea, el máximo de la secuencia se puede establecer usando "SELECT MAX(column) FROM table"y configurando una variable del resultado, digamos $ MAX ... luego se puede escribir la declaración sql "SELECT * FROM seq_1_to_". $MAX ." WHERE seq not in (SELECT column FROM table)" mi sintaxis está basada en php
me_
o puede usar SELECT @var:= max FROM ....; select * from .. WHERE seq < @max;con variables MySQL.
Moshe L
2

Cree una tabla temporal con 100 filas y una sola columna que contenga los valores del 1 al 100.

Outer Únase a esta tabla con su tabla arrc_vouchers y seleccione los valores de una sola columna donde la identificación de arrc_vouchers es nula.

Codificando esto a ciegas, pero debería funcionar.

select tempid from temptable 
left join arrc_vouchers on temptable.tempid = arrc_vouchers.id 
where arrc_vouchers.id is null
amelvin
fuente
De acuerdo, 1 - 100 fue solo una manera fácil de dar un ejemplo. En este caso, estamos viendo 20.000 - 85.000. Entonces, ¿creo una tabla temporal con 65,000 filas numeradas 20000-85000? ¿Y cómo hago para hacer eso? Estoy usando phpMyAdmin; Si configuro el valor predeterminado de la columna en 25000 y lo hago en incremento automático, ¿puedo simplemente insertar 65,000 filas y comenzará el incremento automático con 25000?
EmmyS
Tuve una situación similar (tengo 100 artículos en orden y necesito encontrar los artículos faltantes en 100). Para hacer esto, creé otra tabla 1-100, luego ejecuté esta declaración en ella y funciona muy bien. Esto reemplaza una función muy compleja para crear tablas temporales. Solo un consejo para alguien en una situación similar, a veces es más rápido crear una tabla que tablas temporales.
newshorts
2

Una solución alternativa que requiere una consulta + algún código haciendo algún procesamiento sería:

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;

Tenga en cuenta que la consulta no contiene ninguna subselección que sepamos que el planificador de MySQL no la maneja correctamente.

Eso devolverá una entrada por centralValue (cValue) que no tiene un valor más pequeño (lValue) o un valor mayor (rValue), es decir:

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


Sin entrar en más detalles (los veremos en los siguientes párrafos) este resultado significa que:

  • Sin valores entre 0 y 2
  • Sin valores entre 9 y 22
  • Sin valores entre 24 y 29
  • Sin valores entre 29 y 33
  • Sin valores entre 33 y MAX VALUE

Entonces, la idea básica es hacer uniones DERECHA e IZQUIERDA con la misma tabla viendo si tenemos valores adyacentes por valor (es decir, si el valor central es '3', entonces verificamos 3-1 = 2 a la izquierda y 3 + 1 en derecha), y cuando una FILA tiene un valor NULO en DERECHA o IZQUIERDA, sabemos que no hay un valor adyacente.

La salida sin procesar completa de mi tabla es:

select * from arrc_vouchers order by id asc;

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

Algunas notas:

  1. La instrucción SQL IF en la condición de unión es necesaria si define el campo 'id' como SIN FIRMAR, por lo tanto, no le permitirá disminuirlo por debajo de cero. Esto no es estrictamente necesario si mantiene c.value> 0 como se indica en la siguiente nota, pero lo incluyo como documento.
  2. Estoy filtrando el valor central cero ya que no estamos interesados ​​en ningún valor anterior y podemos derivar el valor de publicación de la siguiente fila.
mgo1977
fuente
2

Si hay una secuencia que tiene un espacio máximo de uno entre dos números (como 1,3,5,6), la consulta que se puede utilizar es:

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);
  • nombre de la tabla - source1
  • column_name - id
PRAKHAR GUPTA
fuente
1

basado en la respuesta dada anteriormente por Lucek, este procedimiento almacenado le permite especificar los nombres de tabla y columna que desea probar para encontrar registros no contiguos, respondiendo así la pregunta original y también demostrando cómo se podría usar @var para representar tablas y / o columnas en un procedimiento almacenado.

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
Profesor Abronsius
fuente
1

Lo probé de diferentes maneras y el mejor rendimiento que encontré fue esta simple consulta:

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
;

... one left join para verificar si existe la siguiente identificación , solo si la siguiente si no se encuentra, entonces la subconsulta busca la siguiente identificación que existe para encontrar el final del espacio. Lo hice porque la consulta con igual (=) tiene un mejor rendimiento que el operador mayor que (>).

Usando sqlfiddle , no muestra un rendimiento tan diferente de la consulta de otros, pero en una base de datos real, esta consulta anterior resulta 3 veces más rápida que otras.

El esquema:

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)
;

Siga a continuación todas las consultas que hice para comparar el rendimiento:

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
;

Quizás ayude a alguien y sea útil.

Puede ver y probar mi consulta usando este sqlfiddle :

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

lynx_74
fuente
0

Aunque todos parecen funcionar, el conjunto de resultados regresa en un tiempo muy largo cuando hay 50.000 registros.

Usé esto, y encuentra el espacio o el siguiente disponible (último usado + 1) con un retorno mucho más rápido de la consulta.

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;
Robar
fuente
esto encuentra la primera brecha que no es lo que pedía la pregunta.
empate el
0

Probablemente no sea relevante, pero estaba buscando algo como esto para enumerar los huecos en una secuencia de números y encontré esta publicación, que tiene múltiples soluciones diferentes dependiendo exactamente de lo que está buscando. Estaba buscando el primer espacio disponible en la secuencia (es decir, el siguiente número disponible), y esto parece funcionar bien.

SELECCIONE MIN (l.number_sequence + 1) como nextavabile de pacientes como l LEFT OUTER JOIN pacientes como r en l.number_sequence + 1 = r.number_sequence DONDE r.number_sequence es NULL. ¡Varios otros escenarios y soluciones discutidos allí, desde 2005!

Cómo encontrar valores perdidos en una secuencia con SQL

SScotti
fuente