Como encontrar lacunas na numeração sequencial no mysql?


119

Temos um banco de dados com uma tabela cujos valores foram importados de outro sistema. Há uma coluna de incremento automático e não há valores duplicados, mas há valores ausentes. Por exemplo, executando esta consulta:

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

deve retornar 100, mas retorna 87 em vez disso. Posso executar alguma consulta que retorne os valores dos números ausentes? Por exemplo, os registros podem existir para id 1-70 e 83-100, mas não há registros com ids de 71-82. Eu quero devolver 71, 72, 73, etc.

Isso é possível?


Isso pode não funcionar no MySQL, mas no trabalho (Oracle) precisávamos de algo semelhante. Escrevemos um Stored Proc que considerou um número como o valor máximo. O Stored Proc criou então uma tabela temporária com uma única coluna. A tabela continha todos os números de 1 a máx. Em seguida, ele fez uma junção NOT IN entre a mesa temporária e nossa mesa de interesse. Se você o chamou com Max = Select max (id) de arrc_vouchers, ele retornaria todos os valores ausentes.
saunderl

2
O que há de errado em ter lacunas na numeração? O valor de uma surrogate key geralmente não é significativo; tudo o que importa é que seja único. Se o seu aplicativo não pode lidar com IDs não contíguos, provavelmente é um bug no aplicativo, não nos dados.
Wyzard

4
Nesse caso, é um problema porque os dados que herdamos do sistema antigo usavam o número de incremento automático associado a um registro como uma chave para imprimir em um cartão físico que está sendo entregue às pessoas. Esta NÃO foi nossa ideia. Para descobrir quais cartas estão faltando, precisamos saber onde estão as lacunas na numeração sequencial.
EmmyS de

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;

Você pode usar gerar série para gerar números de 1 ao id mais alto de sua mesa. Em seguida, execute uma consulta onde id não está nesta série.
Tsvetelin Salutski

Respostas:


170

Atualizar

O ConfexianMJS forneceu uma resposta muito melhor em termos de desempenho.

A resposta (não o mais rápido possível)

Esta é a versão que funciona em mesa de qualquer tamanho (não apenas em 100 linhas):

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 - primeiro id na lacuna atual
  • gap_ends_at - última id na lacuna atual

6
Eu nem trabalho mais para aquela empresa, mas esta é a melhor resposta que já recebi e definitivamente vale a pena lembrar para referência futura. Obrigado!
EmmyS de

4
o único problema com isso é que ele não "relata" uma possível lacuna inicial. por exemplo, se os primeiros 5 ids estiverem faltando (1 a 5), ​​isso não mostra que ... Como poderíamos mostrar lacunas irrisórias no início?
DiegoDD

Nota: Esta consulta não funciona em tabelas temporárias. Meu problema era que order numbereu estava procurando por lacunas em não é distinto (a tabela armazena as linhas de pedido, portanto, o número do pedido a que pertencem se repete para cada linha). 1ª consulta: 2812 linhas no conjunto (1 min 31,09 seg) . Fez outra tabela selecionando números de pedido distintos. Sua consulta sem minhas repetições: 1009 linhas no conjunto (18,04 segundos)
Chris K

1
@DiegoDD O que há de errado SELECT MIN(id) FROM table?
Air

8
Funcionou, mas levou cerca de 5 horas para ser executado em uma mesa com 700.000 registros
Matt

98

Isso funcionou para mim encontrar as lacunas em uma tabela com mais de 80 mil linhas:

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)

Observe que a ordem das colunas expectede goté crítica.

Se você sabe que YourColnão começa em 1 e que não importa, você pode substituir

(SELECT @rownum:=0) AS a

com

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

Novo resultado:

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

Se você precisar realizar algum tipo de tarefa de script de shell nos IDs ausentes, também pode usar essa variante para produzir diretamente uma expressão que possa iterar no 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;

Isso produz uma saída como esta

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

Você pode então copiar e colar em um loop for em um terminal bash para executar um comando para cada ID

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

É a mesma coisa que acima, só que é legível e executável. Alterando o comando "CONCAT" acima, a sintaxe pode ser gerada para outras linguagens de programação. Ou talvez até SQL.


8
boa solução, para mim é melhor do que a resposta preferida - obrigado
Wee Zel

6
É muito mais eficiente do que a resposta aceita.
Symcbean

1
muito mais rápido do que a resposta aceita. A única coisa que eu adicionaria é que CONVERT( YourCol, UNSIGNED )dará melhores resultados se YourCol ainda não for um número inteiro.
Barton Chittenden

1
@AlexandreCassagne: Se estou entendendo sua pergunta corretamente, eu simplesmente faria uma consulta separada como a incorporada para encontrar o mínimo:SELECT MAX(YourCol) FROM YourTable;
ConfexianMJS

1
@temuri Mude para a variante GROUP_CONCAT se necessário: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 e suja que deve resolver o problema:

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

Isso lhe dará uma tabela mostrando o id que tem ids ausentes acima dele, e next_id que existe, e quantos estão faltando entre ... por exemplo

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

1
Isso funcionou muito bem para mim. Obrigado.! Eu fui capaz de modificar isso facilmente para meus propósitos.
Rahim Khoja

Parece que esta é a melhor resposta ao procurar 'próxima id' nas lacunas. Infelizmente, é EXTREMAMENTE lento para tabelas com 10K de linhas. Esperei por mais de 10 minutos em uma mesa de ~ 46K enquanto com @ConfexianMJS obtive resultados em menos de um segundo!
BringBackCommodore64

5

Se estiver usando um, MariaDBvocê tem uma opção mais rápida (800%) usando o mecanismo de armazenamento de sequência :

SELECT * FROM seq_1_to_50000 WHERE SEQ NOT IN (SELECT COL FROM TABLE);

2
para expandir essa ideia, o máximo da sequência pode ser estabelecido usando "SELECT MAX(column) FROM table"e definindo uma variável do resultado, digamos $ MAX ... a instrução sql pode então ser escrita "SELECT * FROM seq_1_to_". $MAX ." WHERE seq not in (SELECT column FROM table)" minha sintaxe é baseada em php
me_

ou você pode usar SELECT @var:= max FROM ....; select * from .. WHERE seq < @max;com variáveis ​​MySQL.
Moshe L

2

Crie uma tabela temporária com 100 linhas e uma única coluna contendo os valores 1-100.

Externo Junte esta tabela à sua tabela arrc_vouchers e selecione os valores de coluna única em que o id arrc_vouchers é nulo.

Codificar este cego, mas deve funcionar.

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

OK, 1 - 100 foi apenas uma maneira fácil de dar um exemplo. Neste caso, estamos olhando para 20.000 - 85.000. Então, devo criar uma tabela temporária com 65.000 linhas numeradas de 20.000 a 85.000? E como faço para fazer isso? Estou usando o phpMyAdmin; se eu definir o valor padrão da coluna para 25.000 e torná-lo incremento automático, posso apenas inserir 65.000 linhas e ele iniciará o incremento automático com 25.000?
EmmyS de

Eu tive uma situação semelhante (tenho 100 itens em ordem e preciso encontrar itens ausentes em 100). Para fazer isso, criei outra tabela 1-100 e, em seguida, executei esta instrução nela e ela funcionou perfeitamente. Isso substitui uma função muito complexa para criar tabelas temporárias. Apenas um conselho para alguém em situação semelhante, às vezes é mais rápido criar uma tabela do que tabelas temporárias.
newshorts de

2

Uma solução alternativa que requer uma consulta + algum código fazendo algum processamento seria:

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;

Observe que a consulta não contém nenhuma subseleção que sabemos que não é tratada de forma performante pelo planejador do MySQL.

Isso retornará uma entrada por centralValue (cValue) que não tem um valor menor (lValue) ou um valor maior (rValue), ou seja:

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


Sem entrar em mais detalhes (veremos nos próximos parágrafos), esta saída significa que:

  • Sem valores entre 0 e 2
  • Sem valores entre 9 e 22
  • Sem valores entre 24 e 29
  • Sem valores entre 29 e 33
  • Nenhum valor entre 33 e MAX VALUE

Portanto, a ideia básica é fazer uma junção RIGHT e LEFT com a mesma tabela, vendo se temos valores adjacentes por valor (ou seja: se o valor central for '3', então verificamos 3-1 = 2 à esquerda e 3 + 1 em direita), e quando uma ROW tem um valor NULL em RIGHT ou LEFT, então sabemos que não há valor adjacente.

O resultado bruto completo da minha tabela é:

select * from arrc_vouchers order by id asc;

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

Algumas notas:

  1. A instrução SQL IF na condição de junção é necessária se você definir o campo 'id' como UNSIGNED, portanto, não permitirá que você diminua para zero. Isso não é estritamente necessário se você mantiver c.value> 0 conforme declarado na próxima nota, mas estou incluindo-o apenas como doc.
  2. Estou filtrando o valor central zero, pois não estamos interessados ​​em nenhum valor anterior e podemos derivar o valor post da próxima linha.

2

Se houver uma sequência com intervalo de no máximo um entre dois números (como 1,3,5,6), a consulta que pode ser usada é:

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);
  • Nome da tabela - source1
  • nome da coluna - id

1

com base na resposta dada acima por Lucek, este procedimento armazenado permite que você especifique os nomes da tabela e da coluna que deseja testar para encontrar registros não contíguos - respondendo assim à pergunta original e também demonstrando como se poderia usar @var para representar tabelas & / ou colunas em um procedimento armazenado.

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

Eu tentei -lo de diferentes maneiras e o melhor desempenho que eu encontrei foi esta consulta simples:

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
;

... uma junção à esquerda para verificar se o próximo id existe, apenas se next se não for encontrado, então a subconsulta encontra o próximo id que existe para encontrar o fim do gap. Fiz isso porque a consulta com igual (=) tem melhor desempenho do que maior que operador (>).

Usando o sqlfiddle ele não mostra um desempenho tão diferente de outras consultas, mas em um banco de dados real esta consulta acima resulta 3 vezes mais rápido do que outras.

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

Segue abaixo todas as consultas que fiz para comparar o desempenho:

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
;

Talvez ajude alguém e seja útil.

Você pode ver e testar minha consulta usando este sqlfiddle :

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


0

Embora tudo isso pareça funcionar, o conjunto de resultados retorna em muito tempo quando há 50.000 registros.

Eu usei isso, e ele encontra o gap ou o próximo disponível (último usado + 1) com um retorno muito mais rápido da 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;

isto encontra a primeira lacuna que não é o que a pergunta estava pedindo.
desenhou em

0

Provavelmente não é relevante, mas estava procurando algo assim para listar as lacunas em uma sequência de números e encontrei este post, que tem várias soluções diferentes dependendo exatamente do que você está procurando. Eu estava procurando a primeira lacuna disponível na sequência (ou seja, o próximo número disponível) e isso parece funcionar bem.

SELECT MIN (l.number_sequence + 1) como o próximo disponível de pacientes como l LEFT OUTER JOIN pacientes como r em l.number_sequence + 1 = r.number_sequence ONDE r.number_sequence é NULL. Vários outros cenários e soluções discutidos lá, a partir de 2005!

Como encontrar valores ausentes em uma sequência com SQL

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.