Pergunta

Recentemente encontrei e corrigi um bug em um site em que estava trabalhando que resultava em milhões de linhas duplicadas de dados em uma tabela que será bastante grande mesmo sem elas (ainda na casa dos milhões).Posso encontrar facilmente essas linhas duplicadas e executar uma única consulta de exclusão para eliminar todas elas.O problema é que tentar excluir tantas linhas de uma só vez trava a tabela por um longo tempo, o que eu gostaria de evitar, se possível.As únicas maneiras que vejo de me livrar dessas linhas, sem derrubar o site (travando a tabela) são:

  1. Escreva um script que execute milhares de consultas de exclusão menores em um loop.Teoricamente, isso contornará o problema da tabela bloqueada porque outras consultas poderão entrar na fila e ser executadas entre as exclusões.Mas ainda assim aumentará bastante a carga do banco de dados e levará muito tempo para ser executado.
  2. Renomeie a tabela e recrie a tabela existente (agora estará vazia).Em seguida, faça minha limpeza na tabela renomeada.Renomeie a nova tabela, nomeie a antiga novamente e mescle as novas linhas na tabela renomeada.Essa maneira exige consideravelmente mais etapas, mas deve realizar o trabalho com o mínimo de interrupção.A única parte complicada aqui é que a tabela em questão é uma tabela de relatórios, portanto, uma vez que ela é renomeada e a tabela vazia é colocada em seu lugar, todos os relatórios históricos desaparecem até que eu a coloque de volta no lugar.Além disso, o processo de fusão pode ser um pouco complicado devido ao tipo de dados armazenados.No geral, esta é minha escolha provável agora.

Eu só queria saber se alguém já teve esse problema antes e, em caso afirmativo, como você lidou com isso sem derrubar o site e, esperançosamente, com o mínimo de interrupção para os usuários?Se eu escolher o número 2, ou uma abordagem diferente e semelhante, posso programar o material para ser executado tarde da noite e fazer a mesclagem na manhã seguinte e apenas avisar os usuários com antecedência, então isso não é um grande negócio.Só estou querendo ver se alguém tem alguma idéia de uma maneira melhor ou mais fácil de fazer a limpeza.

Foi útil?

Solução

DELETE FROM `table`
WHERE (whatever criteria)
ORDER BY `id`
LIMIT 1000

Lave, enxágue, repita até zero linhas afetadas. Talvez em um script que dorme por um segundo ou três entre iterações.

Outras dicas

Eu também recomendo adicionar algumas restrições à sua mesa para garantir que isso não aconteça com você novamente. Um milhão de linhas, a 1000 por tiro, levará 1000 repetições de um script para concluir. Se o script funcionar uma vez a cada 3,6 segundos, você será feito em uma hora. Sem problemas. É improvável que seus clientes percebam.

O seguinte exclui 1.000.000 de registros, um de cada vez.

 for i in `seq 1 1000`; do 
     mysql  -e "select id from table_name where (condition) order by id desc limit 1000 " | sed 's;/|;;g' | awk '{if(NR>1)print "delete from table_name where id = ",$1,";" }' | mysql; 
 done

Você pode agrupá -los e excluir tabela_name onde em (id1, id2, .. idn) Tenho certeza que também sem dificuldade

Eu tinha um caso de uso de exclusão de 1M+ linhas na tabela de linhas de 25m+ no MySQL. Tentei abordagens diferentes, como exclusão de lote (descrita acima).
Descobri que a maneira mais rápida (cópia dos registros necessários para a nova tabela):

  1. Crie uma tabela temporária que possua apenas IDs.

Criar tabela id_temp_table (temp_id int);

  1. Insira IDs que devem ser removidos:

inserir em id_temp_table (temp_id) selecione .....

  1. Crie nova tabela Table_new

  2. Insira todos os registros de tabela para tabela_new sem linhas desnecessárias que estão em id_temp_table

insira na tabela_new .... onde tabela_id não está (selecione distinto (temp_id) de id_temp_table);

  1. Renomear tabelas

Todo o processo levou ~ 1hr. No meu caso de uso, o exclusão simples de lote em 100 registros levou 10 minutos.

Eu usaria MK-Archiver do excelente Maatkit Pacote de utilitários (um monte de scripts Perl para gerenciamento do MySQL) Maatkit é de Baron Schwartz, o autor do livro O'Reilly "High Performance MySQL".

O objetivo é um trabalho de baixo impacto e apenas para a frente para morder dados antigos da tabela sem afetar muito as consultas OLTP. Você pode inserir os dados em outra tabela, que não precisam estar no mesmo servidor. Você também pode escrever em um arquivo em um formato adequado para o Infile de dados de carga. Ou você não pode fazer, nesse caso, é apenas uma exclusão incremental.

Ele já está construído para arquivar suas linhas indesejadas em pequenos lotes e, como bônus, pode salvar as linhas excluídas em um arquivo, caso você estrague a consulta que seleciona as linhas para remover.

Nenhuma instalação necessária, basta pegar http://www.maatkit.org/get/mk-archiver e execute o PerlDoc nele (ou leia o site) para documentação.

Eu enfrentei um problema semelhante.Tínhamos uma tabela muito grande, com cerca de 500 GB de tamanho, sem particionamento e um único índice na coluna Primary_Key.Nosso mestre era uma máquina enorme, 128 núcleos e 512 GB de RAM e tínhamos vários escravos também.Tentamos algumas técnicas para lidar com a exclusão de linhas em grande escala.Vou listá-los todos aqui, do pior ao melhor que encontramos-

  1. Buscando e excluindo uma linha por vez.Este é o pior que você poderia fazer.Então, nós nem tentamos isso.
  2. Buscando as primeiras linhas 'X' do banco de dados usando uma consulta de limite na coluna Primary_Key, verificando os IDs de linha a serem excluídos no aplicativo e disparando uma única consulta de exclusão com uma lista de IDs de Primary_Key.Portanto, 2 consultas por linhas 'X'.Agora, essa abordagem foi boa, mas fazer isso usando um trabalho em lote excluiu cerca de 5 milhões de linhas em cerca de 10 minutos, devido ao qual os escravos de nosso banco de dados MySQL ficaram atrasados ​​em 105 segundos.Atraso de 105 segundos em atividade de 10 minutos.Então, tivemos que parar.
  3. Nesta técnica, introduzimos um atraso de 50 ms entre a busca em lote subsequente e as exclusões de tamanho 'X' cada.Isso resolveu o problema de atraso, mas agora estávamos excluindo 1,2-1,3 milhões de linhas por 10 minutos, em comparação com 5 milhões na técnica nº 2.
  4. Particionar a tabela do banco de dados e excluir todas as partições quando não for necessário.Esta é a melhor solução que temos, mas requer uma tabela pré-particionada.Seguimos a etapa 3 porque tínhamos uma tabela muito antiga não particionada, com indexação apenas na coluna Primary_Key.Criar uma partição teria levado muito tempo e estávamos em crise.Aqui estão alguns links relacionados ao particionamento que achei úteis- Referência oficial do MySQL, Particionamento diário do Oracle DB.

Então, IMO, se você pode se dar ao luxo de criar uma partição em sua tabela, escolha a opção nº 4, caso contrário, você ficará preso na opção nº 3.

Faça isso em lotes de, digamos, 2000 linhas por vez. Comprometa-se intermediário. Um milhão de linhas não é muito e isso será rápido, a menos que você tenha muitos índices na tabela.

De acordo com Documentação do MySQL, TRUNCATE TABLE é uma alternativa rápida a DELETE FROM. Experimente isso:

TRUNCATE TABLE table_name

Eu tentei isso em linhas de 50m e foi feito dentro de dois minutos.

Nota: as operações truncadas não são seguras de transação; Ocorre um erro ao tentar um no decorrer de uma transação ativa ou bloqueio de tabela ativa

Para nós, o DELETE WHERE %s ORDER BY %s LIMIT %d A resposta não era uma opção, porque os critérios onde era lento (uma coluna não indexada) e atingiria o mestre.

Selecione em uma leitura-replica uma lista de chaves primárias que você deseja excluir. Exportar com este tipo de formato:

00669163-4514-4B50-B6E9-50BA232CA5EB
00679DE5-7659-4CD4-A919-6426A2831F35

Use o script Bash a seguir para pegar esta entrada e coloque -o em declarações de exclusão requer festa ≥ 4 por causa de mapfile construídas em]:

sql-chunker.sh (lembrar de chmod +x eu, e mude o shebang para apontar para o seu Bash 4 executável):

#!/usr/local/Cellar/bash/4.4.12/bin/bash

# Expected input format:
: <<!
00669163-4514-4B50-B6E9-50BA232CA5EB
00669DE5-7659-4CD4-A919-6426A2831F35
!

if [ -z "$1" ]
  then
    echo "No chunk size supplied. Invoke: ./sql-chunker.sh 1000 ids.txt"
fi

if [ -z "$2" ]
  then
    echo "No file supplied. Invoke: ./sql-chunker.sh 1000 ids.txt"
fi

function join_by {
    local d=$1
    shift
    echo -n "$1"
    shift
    printf "%s" "${@/#/$d}"
}

while mapfile -t -n "$1" ary && ((${#ary[@]})); do
    printf "DELETE FROM my_cool_table WHERE id IN ('%s');\n" `join_by "','" "${ary[@]}"`
done < "$2"

Invoque assim:

./sql-chunker.sh 1000 ids.txt > batch_1000.sql

Isso lhe dará um arquivo com a saída formatada como assim (eu usei um tamanho de lote de 2):

DELETE FROM my_cool_table WHERE id IN ('006CC671-655A-432E-9164-D3C64191EDCE','006CD163-794A-4C3E-8206-D05D1A5EE01E');
DELETE FROM my_cool_table WHERE id IN ('006CD837-F1AD-4CCA-82A4-74356580CEBC','006CDA35-F132-4F2C-8054-0F1D6709388A');

Em seguida, execute as declarações como assim:

mysql --login-path=master billing < batch_1000.sql

Para aqueles que não estão familiarizados com login-path, é apenas um atalho para fazer login sem digitar senha na linha de comando.

Eu acho que a lentidão se deve ao "índice cluster" do MySQL, onde os registros reais são armazenados no índice de chave primária - na ordem do índice de chave primária. Isso significa que o acesso a um registro através da chave primária é extremamente rápido, pois requer apenas uma busca de disco porque o registro no disco ali mesmo, onde encontrou a chave primária correta no índice.

Em outros bancos de dados sem índices agrupados, o próprio índice não mantém o registro, mas apenas um "deslocamento" ou "local" indicando onde o registro está localizado no arquivo da tabela e, em seguida, uma segunda busca deve ser feita nesse arquivo para recuperar os dados reais .

Você pode imaginar ao excluir um recorde em um índice em cluster que todos os registros acima desse registro na tabela devem ser movidos para baixo para evitar enormes orifícios sendo criados no índice (bem, é isso que me lembro de alguns anos atrás, pelo menos - versões posteriores pode ter mudado isso).

Saber o acima que descobrimos que realmente acelera o MySQL era realizar as exclusões em ordem inversa. Isso produz a menor quantidade de movimento de registro, porque você está excluindo registros do final, o que significa que os exclusão subsequente têm menos objetos para se mudar.

Não escrevi nada para fazer isso, e fazê -lo corretamente exigiria absolutamente um script, mas outra opção é criar uma nova tabela duplicada e selecionar todas as linhas que você deseja manter nela. Use um gatilho para mantê-lo atualizado enquanto esse processo é concluído. Quando estiver em sincronia (menos as linhas que você deseja cair), renomeie as duas tabelas em uma transação, para que o novo substitua o antigo. Abaixe a mesa antiga e pronto!

Isso (obviamente) requer muito espaço de disco extra e pode tributar seus recursos de E/S, mas, caso contrário, pode ser muito mais rápido.

Dependendo da natureza dos dados ou de emergência, você pode renomear a tabela antiga e criar uma nova tabela vazia em seu lugar e selecionar as fileiras de "manter" na nova tabela à sua lazer ...

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top