Eu estava enfrentando esse problema em uma instância de produção e tentei uma recriação local em pequena escala e consegui.
Eu tenho uma tabela de teste simples
CREATE TABLE `test` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`val` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
KEY `index2` (`val`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
Estou usando a versão Mysql 8.0.26
com um innodb
mecanismo de armazenamento
Preenchi esta tabela com cerca de 4 milhões de linhas aleatórias. Selecionar 5000 linhas desta tabela leva cerca de 50 ms. Em seguida, apaguei todas as linhas usandoDELETE FROM test;
Após excluir todas as linhas, se eu executar SELECT * FROM test LIMIT 5000;
essa consulta, ela agora leva mais de 1 minuto para ser executada. Quando a consulta select finalmente termina de ser executada, ela retorna 0 linhas (como esperado).
Antes de prosseguir, quero esclarecer que entendo que executar um optimize table
resolverá o problema, mas estou interessado em entender o que realmente está acontecendo nos bastidores. Aqui estão alguns pensamentos que tive
- Depois de cerca de uma hora, a consulta se torna instantânea novamente. Isso me leva a acreditar que algum tipo de coleta de lixo ou limpeza está acontecendo por baixo dos panos. Não consigo encontrar nada sobre isso nos documentos.
- A consulta fica significativamente mais lenta após a exclusão. Como não estou usando nenhuma cláusula não indexada na minha consulta select, ela foi resolvida em cerca de 50 ms antes da exclusão, então, de alguma forma, é 1500x mais lenta após a exclusão.
- Eu pesquisei o conceito de "registros fantasmas", no entanto, todos os resultados que consigo encontrar estão falando apenas sobre o SQL Server em vez do Mysql, o que me leva a acreditar que o conceito de "registros fantasmas" pode não ser uma coisa no Mysql. Não consigo encontrar nenhuma menção nos documentos do Mysql sobre isso.
Eu tentei o log de consulta lenta e esperava ver um monte de dados escaneados, mesmo que tivessem sido excluídos (registros fantasmas), mas além da duração, não consigo ver nada estranho ou inesperado lá
# Time: 2025-02-11T10:29:24.510606Z
# User@Host: root[root] @ [172.17.0.1] Id: 20978
# Query_time: 69.680895 Lock_time: 0.000135 Rows_sent: 0 Rows_examined: 0 Thread_id: 20978 Errno: 0 Killed: 0 Bytes_received: 0 Bytes_sent: 130 Read_first: 1 Read_last: 0 Read_key: 1 Read_next: 0 Read_prev: 0 Read_rnd: 0 Read_rnd_next: 0 Sort_merge_passes: 0 Sort_range_count: 0 Sort_rows: 0 Sort_scan_count: 0 Created_tmp_disk_tables: 0 Created_tmp_tables: 0 Start: 2025-02-11T10:28:14.829711Z End: 2025-02-11T10:29:24.510606Z
SET timestamp=1739269694;
select * from test
LIMIT 0, 5000;
Alguém pode explicar em detalhes o que exatamente está acontecendo aqui? Idealmente, as duas coisas que eu adoraria saber, se possível, são:
- O que está realmente acontecendo?
- É possível ver/diagnosticar esse problema olhando algumas estatísticas internas do Mysql? Algo como o status/information_schema/performance_schema do mecanismo innodb, por exemplo. Basicamente, algo que eu poderia olhar e dizer que uma tabela está tendo esse problema sem saber que uma exclusão acabou de ser executada
Aqui está a explicação do select -SELECT * FROM test LIMIT 5000
Antes de excluir (rápido 50 ms)
id, select_type, table, partitions, type, possible_keys, key, key_len, ref, rows, filtered, Extra
'1', 'SIMPLE', 'test', NULL, 'index', NULL, 'index2', '1022', NULL, '6958223', '100.00', 'Using index'
Após a exclusão (lento 69s) - Corri analyze table test
para atualizar as estatísticas antes de executar novamente a seleção e a explicação
id, select_type, table, partitions, type, possible_keys, key, key_len, ref, rows, filtered, Extra
'1', 'SIMPLE', 'test', NULL, 'index', NULL, 'index2', '1022', NULL, '1', '100.00', 'Using index'
O InnoDb usa MVCC, o que significa que as exclusões são realmente registradas como uma forma especial de atualização. Então sua tabela 'vazia', na verdade, ainda tem todas as linhas nela imediatamente após a exclusão em massa. Uma varredura de tabela ainda tem que passar por todas essas linhas para verificar se elas são relevantes para sua transação, mesmo que não sejam.
Isso torna sua consulta mais lenta, porque antes da exclusão ela conseguia parar depois de ler 5000 linhas. Depois da exclusão, ela tem que ler todos os 4 milhões em uma tentativa de encontrar 1 que não seja porque o limite não é atingido.
Um thread de segundo plano chamado thread de purga se livrará deles ao longo do tempo como parte da limpeza do log de desfazer. Veja https://dev.mysql.com/doc/refman/8.0/en/innodb-purge-configuration.html para saber como configurá-lo. Você pode querer considerar aumentar temporariamente os threads de purga e o tamanho do lote até que a consulta alcance.