Em um conjunto de valores, como encontro os valores não armazenados na coluna de uma tabela?

12

Eu tenho uma tabela que potencialmente armazena centenas de milhares de números inteiros

desc id_key_table;

+----------------+--------------+------+-----+---------+-------+
| Field          | Type         | Null | Key | Default | Extra |
+----------------+--------------+------+-----+---------+-------+
| id_key         | int(16)      | NO   | PRI | NULL    |       |
+----------------+--------------+------+-----+---------+-------+

Em um programa, eu tenho um grande conjunto de números inteiros. Gostaria de ver quais desses números inteiros NÃO estão na coluna id_key acima.

Até agora, desenvolvi as seguintes abordagens:

1) Itere através de cada número inteiro e execute:

select count(*) count from id_key_table where id_key = :id_key

Quando count é 0, a id_key está ausente na tabela.

Parece uma maneira horrível e horrível de fazer isso.


2) Crie uma tabela temporária, insira cada um dos valores na tabela temporária e execute JOIN nas duas tabelas.

create temporary table id_key_table_temp (id_key int(16) primary key );

insert into id_key_table_temp values (1),(2),(3),...,(500),(501);

select temp.id_key
from id_key_table_temp temp left join id_key_table as main 
         on temp.id_key = main.id_key 
where main.killID is null;

drop table id_key_table_temp;

Essa parece ser a melhor abordagem, no entanto, tenho certeza de que existe uma abordagem muito melhor na qual ainda não pensei. Eu preferiria não precisar criar uma tabela temporária e usar uma consulta para determinar quais números inteiros estão ausentes.

Existe uma consulta adequada para esse tipo de pesquisa?

(MySQL)

Clinton
fonte
2
Eu gosto de como você pediu a sua pergunta (Bem-vindo ao DBA), no entanto, é provavelmente muito mais apropriado em stackoverflow como ele lida com a interação com um programa de algum tipo (não dba per se)
Derek Downey
Obrigado pela recepção, pensei que um lugar como esse poderia ter mais gurus do que stackoverflow. Eu não me importo de perguntar lá, no entanto.
Clinton
2
Como sugerido, reenviei para StackOverflow: stackoverflow.com/questions/5967822/…
Clinton
Situação semelhante foi tratada para o servidor sql nesta questão: Técnica para enviar muitos dados para o processo armazenado . Você deve descobrir que o problema é semelhante em outros ambientes de banco de dados. Enfim, eu vou para a solução não. 2 - envie a lista de ids, analise, coloque na tabela, entre na sua tabela principal. Que se você não pode usar outras soluções, mas aqui você tem que cavar :-).
Marian

Respostas:

7

Sua segunda solução usando o LEFT JOIN é de longe a melhor abordagem. Eu não usaria uma tabela temporária, usaria uma tabela regular e a preencheria com novos valores sempre que você quisesse executar a consulta.

Michael Riley - também conhecido por Gunny
fonte
5

Parece que o "grande conjunto de números inteiros" ainda é consideravelmente menor que a tabela com "centenas de milhares de números inteiros". Com essa suposição e a menos que haja uma maneira no MySQL de usar uma matriz de seus números inteiros como uma tabela em sua instrução SQL, sua segunda opção é provavelmente a melhor. Ele deve fazer uma varredura completa da tabela temporária e do índice na tabela principal. O principal benefício é que ele só precisa varrer o índice que contém centenas de milhares de números inteiros uma vez e apenas enviar os resultados ao cliente. Sua consulta pode (mas não precisa ser) reescrita da seguinte maneira:

SELECT * FROM id_key_table_temp 
WHERE id_key NOT IN (select id_key FROM id_key_table);
Leigh Riffel
fonte
Não estou endossando uma tabela temporária sobre uma tabela regular, pois não tenho conhecimento das diferenças na plataforma MySQL. No Oracle, uma tabela temporária provavelmente seria a melhor, mas, no Oracle, você usaria uma matriz como tabela e se juntaria diretamente a ela.
Leigh Riffel
3

Em vez de uma tabela temporária e inserção com insert into id_key_table_temp values (1),(2),(3),...,(500),(501);, você pode construir uma subconsulta com todos os valores que você está tentando verificar:

select id_key
from ( select @row := @row + 1 as id_key 
       from (select 0 union all select 1 union all select 3 union all select 4 union all select 5 union all select 6 union all select 6 union all select 7 union all select 8 union all select 9) s1,
            (select 0 union all select 1 union all select 3 union all select 4 union all select 5 union all select 6 union all select 6 union all select 7 union all select 8 union all select 9) s2,
            (select 0 union all select 1 union all select 3 union all select 4 union all select 5 union all select 6 union all select 6 union all select 7 union all select 8 union all select 9) s3,
            (select 0 union all select 1 union all select 3 union all select 4 union all select 5 union all select 6 union all select 6 union all select 7 union all select 8 union all select 9) s4,
            (select @row:=0) s5 ) s
where id_key in(1, 2, 3, 500, 501)
      and id_key not in (select id_key from main);
Jack diz que tenta topanswers.xyz
fonte
2

Como observado no meu comentário, isso provavelmente é mais adequado ao stackoverflow. No entanto, acho que essas duas soluções não são as melhores:

A solução 1 requer várias chamadas selecionadas, muito ineficientes

A solução 2 é melhor, mas não tenho certeza de que o custo de inserir tantos valores seja a melhor solução.

Uma possível solução 3 seria fazer uma consulta:

SELECT DISTINCT id_key FROM id_key_table

e programaticamente obtenha a diferença do seu conjunto inteiro e do conteúdo do banco de dados. Na pior das hipóteses, (como são muitos números inteiros). Essa rota deve ser melhor que a Solução 1. A solução 2 também pode retornar muitos números inteiros (se a tabela tiver muitos que não estão no seu conjunto de dados), portanto depende ™!

Derek Downey
fonte
Não sou fã desta solução, pois o conjunto de resultados seria muito grande.
Clinton
@Clinton é verdade, mas também pode ser muito grande na sua segunda solução, se você não fornecer números inteiros suficientes para filtrá-la.
Derek Downey
2

Abordei isso bastante no StackOverflow , mas gostaria de elaborar mais sobre o uso da tabela permanente temp (PermTemp). ( temperatura permanente, isso não é um oxímoro ?)

No StackOverflow , eu tinha o procedimento armazenado test.CreateSampleTable e test.GetMissingIntegers fazem uma tabela de amostra e, em seguida, criam uma tabela temporária dinâmica para preencher antes de fazer o grande JOIN para encontrar diferenças.

Desta vez, vamos criar a tabela de amostra junto com a tabela de tabela permanente.

Aqui está test.LoadSampleTables:

DELIMITER $$

DROP PROCEDURE IF EXISTS `LoadSampleTables` $$
CREATE DEFINER=`lwdba`@`127.0.0.1` PROCEDURE `LoadSampleTables`(maxinttoload INT)
BEGIN

  DECLARE X,OKTOUSE,MAXLOOP INT;

  DROP TABLE IF EXISTS test.id_key_table;
  DROP TABLE IF EXISTS test.id_key_table_keys;
  CREATE TABLE test.id_key_table (id_key INT(16)) ENGINE=MyISAM;
  CREATE TABLE test.id_key_table_keys (id_key INT(16)) ENGINE=MyISAM;

  SET X=1;
  WHILE X <= maxinttoload DO
    INSERT INTO test.id_key_table VALUES (X);
    SET X = X + 1;
  END WHILE;
  ALTER TABLE test.id_key_table ADD PRIMARY KEY (id_key);

  SET MAXLOOP = FLOOR(SQRT(maxinttoload));
  SET X = 2;
  WHILE X <= MAXLOOP DO
    DELETE FROM test.id_key_table WHERE MOD(id_key,X) = 0 AND id_key > X;
    SELECT MIN(id_key) INTO OKTOUSE FROM test.id_key_table WHERE id_key > X;
    SET X = OKTOUSE;
  END WHILE;
  OPTIMIZE TABLE test.id_key_table;

  INSERT INTO test.id_key_table_keys SELECT id_key FROM test.id_key_table;
  ALTER TABLE test.id_key_table_keys ADD PRIMARY KEY (id_key);
  OPTIMIZE TABLE test.id_key_table_keys;

END $$

DELIMITER ;

Depois de executar isso, aqui estão as tabelas e seu conteúdo:

mysql> call test.loadsampletables(25);
+-------------------+----------+----------+----------+
| Table             | Op       | Msg_type | Msg_text |
+-------------------+----------+----------+----------+
| test.id_key_table | optimize | status   | OK       |
+-------------------+----------+----------+----------+
1 row in set (0.20 sec)

+------------------------+----------+----------+----------+
| Table                  | Op       | Msg_type | Msg_text |
+------------------------+----------+----------+----------+
| test.id_key_table_keys | optimize | status   | OK       |
+------------------------+----------+----------+----------+
1 row in set (0.28 sec)

Query OK, 0 rows affected (0.29 sec)

mysql> select * from test.id_key_table;
+--------+
| id_key |
+--------+
|      1 |
|      2 |
|      3 |
|      5 |
|      7 |
|     11 |
|     13 |
|     17 |
|     19 |
|     23 |
+--------+
10 rows in set (0.00 sec)

mysql> select * from test.id_key_table_keys;
+--------+
| id_key |
+--------+
|      1 |
|      2 |
|      3 |
|      5 |
|      7 |
|     11 |
|     13 |
|     17 |
|     19 |
|     23 |
+--------+
10 rows in set (0.00 sec)

Aqui estão os gatilhos da tabela PermTemp

mysql> DELIMITER $$
mysql>
mysql> CREATE TRIGGER test.AddPermTempKey AFTER INSERT ON test.id_key_table
    -> FOR EACH ROW
    -> BEGIN
    ->     INSERT IGNORE INTO test.id_key_table_keys VALUES (NEW.id_key);
    -> END $$
Query OK, 0 rows affected (0.09 sec)

mysql>
mysql> CREATE TRIGGER test.DeletePermTempKey AFTER DELETE ON test.id_key_table
    -> FOR EACH ROW
    -> BEGIN
    ->     DELETE FROM test.id_key_table_keys WHERE id_key = OLD.id_key;
    -> END $$
Query OK, 0 rows affected (0.08 sec)

mysql>
mysql> DELIMITER ;

Agora, vamos importar um novo lote de registros, tabela test.weekly_batch, algumas chaves usadas antes, outras chaves com a marca nova:

mysql> CREATE TABLE test.weekly_batch (id_key INT(16)) ENGINE=MyISAM;
Query OK, 0 rows affected (0.04 sec)

mysql> INSERT INTO test.weekly_batch VALUES (17),(19),(23),(29),(31),(37),(41);
Query OK, 7 rows affected (0.00 sec)
Records: 7  Duplicates: 0  Warnings: 0

mysql> ALTER TABLE test.weekly_batch ADD PRIMARY KEY (id_key);
Query OK, 7 rows affected (0.08 sec)
Records: 7  Duplicates: 0  Warnings: 0

Vamos pegar test.weekly_batch e mesclar com segurança em test.id_key_table_keys e formar a tabela test.new_keys_to_load:

DELIMITER $$

DROP PROCEDURE IF EXISTS `test`.`ImportWeeklyBatch` $$
CREATE PROCEDURE `test`.`ImportWeeklyBatch` ()
TheStoredProcedure:BEGIN

  DECLARE RCOUNT INT;

  SELECT COUNT(1) INTO RCOUNT FROM information_schema.tables
  WHERE table_schema='test' AND table_name='weekly_batch';
  IF RCOUNT = 0 THEN
    LEAVE TheStoredProcedure;
  END IF;
  SELECT COUNT(1) INTO RCOUNT FROM test.weekly_batch;
  IF RCOUNT = 0 THEN
    LEAVE TheStoredProcedure;
  END IF;
  DROP TABLE IF EXISTS test.new_keys_to_load;
  CREATE TABLE test.new_keys_to_load (id_key INT(16));
  INSERT INTO test.new_keys_to_load (id_key)
  SELECT id_key FROM test.weekly_batch A
  LEFT JOIN test.id_key_table_keys B USING (id_key)
  WHERE B.id_key IS NULL;

  SELECT * FROM test.new_keys_to_load;

END $$

DELIMITER ;

Aqui está o resultado:

mysql> call test.importweeklybatch;
+--------+
| id_key |
+--------+
|     29 |
|     31 |
|     37 |
|     41 |
+--------+
4 rows in set (0.14 sec)

A partir desse ponto, basta usar a tabela new_keys_to_load como a lista de novas chaves de marca que serão importadas. Como new_keys_to_load é menor que a tabela PermTemp, você sempre deve usar new_keys_to_load no lado esquerdo do LEFT JOIN.

RolandoMySQLDBA
fonte
Eu respondi a essa no SO
RolandoMySQLDBA