Aula: Limpeza de Banco de Dados com MU-Plugin – Substituindo o Advanced Database Cleaner
Objetivo: Substituir um plugin robusto como o Advanced Database Cleaner por um MU-Plugin enxuto, que mantém o banco de dados limpo removendo revisões antigas, transients expirados, rascunhos automáticos, lixeira, comentários spam e outros resíduos deixados por temas e plugins desinstalados.
Contexto: O Advanced Database Cleaner é um plugin popular com interface gráfica e opções avançadas, mas muitas de suas funções podem ser reproduzidas com código eficiente e sem sobrecarga. Vamos criar um MU-Plugin que realiza a limpeza automática via WP-Cron, sem necessidade de interface administrativa, tornando o processo transparente e de fácil manutenção.
Funcionalidades implementadas no nosso MU-Plugin
- Remoção de revisões antigas de posts/páginas (padrão: 30 dias)
- Remoção de rascunhos automáticos (auto-drafts)
- Remoção de posts na lixeira (trash)
- Remoção de comentários spam, pendentes e na lixeira
- Remoção de pingbacks e trackbacks
- Remoção de transients expirados (lixo temporário do WordPress)
- Remoção de meta dados órfãos (postmeta, commentmeta, usermeta, termmeta) – dados deixados por plugins desinstalados
- Remoção de relacionamentos órfãos (term_relationships)
- Remoção de opções órfãs na tabela wp_options (resíduos de plugins removidos)
- Remoção de cache oEmbed
- Execução automática via WP-Cron (diária)
- Log das operações para depuração (opcional, ativado via constante)
Código do MU-Plugin
Crie a pasta wp-content/mu-plugins/neodoor-db-cleaner/ e dentro dela o arquivo neodoor-db-cleaner.php com o conteúdo abaixo:
<?php
/**
* Plugin Name: Neodoor Database Cleaner
* Description: Limpeza automática e segura do banco de dados: revisões antigas, transients expirados, lixeira, spam, metadados órfãos e resíduos de plugins.
* Version: 1.0
* Author: Reinaldo (Neodoor.neo)
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Neodoor_DB_Cleaner {
private static $instance = null;
// Configurações (podem ser alteradas aqui ou via constantes)
private $keep_revisions_days = 30;
private $log_enabled = false;
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
// Carrega configurações de constantes, se definidas
if ( defined( 'NEODOOR_DB_CLEANER_KEEP_REVISIONS_DAYS' ) ) {
$this->keep_revisions_days = intval( NEODOOR_DB_CLEANER_KEEP_REVISIONS_DAYS );
}
if ( defined( 'NEODOOR_DB_CLEANER_LOG_ENABLED' ) ) {
$this->log_enabled = (bool) NEODOOR_DB_CLEANER_LOG_ENABLED;
}
// Agenda a limpeza diária (se não estiver agendada)
add_action( 'wp', [ $this, 'schedule_cleanup' ] );
add_action( 'neodoor_db_daily_cleanup', [ $this, 'run_full_cleanup' ] );
}
/**
* Agenda o evento de limpeza diária (hook: neodoor_db_daily_cleanup)
*/
public function schedule_cleanup() {
if ( ! wp_next_scheduled( 'neodoor_db_daily_cleanup' ) ) {
wp_schedule_event( time(), 'daily', 'neodoor_db_daily_cleanup' );
$this->log( 'Evento de limpeza diária agendado.' );
}
}
/**
* Executa todas as rotinas de limpeza
*/
public function run_full_cleanup() {
global $wpdb;
$this->log( 'Iniciando limpeza completa do banco de dados.' );
// 1. Remove revisões antigas
$this->delete_old_revisions();
// 2. Remove rascunhos automáticos
$this->delete_auto_drafts();
// 3. Remove posts na lixeira
$this->delete_trashed_posts();
// 4. Remove comentários spam, pendentes e na lixeira
$this->delete_spam_comments();
$this->delete_pending_comments();
$this->delete_trashed_comments();
// 5. Remove pingbacks e trackbacks
$this->delete_pingbacks_trackbacks();
// 6. Remove transients expirados
$this->delete_expired_transients();
// 7. Remove metadados órfãos
$this->delete_orphaned_post_meta();
$this->delete_orphaned_comment_meta();
$this->delete_orphaned_user_meta();
$this->delete_orphaned_term_meta();
// 8. Remove relacionamentos órfãos
$this->delete_orphaned_term_relationships();
// 9. Remove opções órfãs (resíduos de plugins desinstalados)
$this->delete_orphaned_options();
// 10. Remove cache oEmbed
$this->delete_oembed_cache();
// 11. Otimiza as tabelas (opcional)
$this->optimize_tables();
$this->log( 'Limpeza completa finalizada.' );
}
/**
* Remove revisões de posts/páginas com mais de X dias
*/
private function delete_old_revisions() {
global $wpdb;
$sql = $wpdb->prepare(
"DELETE FROM {$wpdb->posts}
WHERE post_type = 'revision'
AND post_date < NOW() - INTERVAL %d DAY", $this->keep_revisions_days
);
$deleted = $wpdb->query( $sql );
$this->log( "Revisões antigas removidas: {$deleted}" );
}
/**
* Remove rascunhos automáticos
*/
private function delete_auto_drafts() {
global $wpdb;
$deleted = $wpdb->query(
"DELETE FROM {$wpdb->posts}
WHERE post_status = 'auto-draft'"
);
$this->log( "Rascunhos automáticos removidos: {$deleted}" );
}
/**
* Remove posts na lixeira
*/
private function delete_trashed_posts() {
global $wpdb;
$deleted = $wpdb->query(
"DELETE FROM {$wpdb->posts}
WHERE post_status = 'trash'"
);
$this->log( "Posts na lixeira removidos: {$deleted}" );
}
/**
* Remove comentários com status 'spam'
*/
private function delete_spam_comments() {
global $wpdb;
$deleted = $wpdb->query(
"DELETE FROM {$wpdb->comments}
WHERE comment_approved = 'spam'"
);
$this->log( "Comentários spam removidos: {$deleted}" );
}
/**
* Remove comentários pendentes (moderação)
*/
private function delete_pending_comments() {
global $wpdb;
$deleted = $wpdb->query(
"DELETE FROM {$wpdb->comments}
WHERE comment_approved = '0'"
);
$this->log( "Comentários pendentes removidos: {$deleted}" );
}
/**
* Remove comentários na lixeira
*/
private function delete_trashed_comments() {
global $wpdb;
$deleted = $wpdb->query(
"DELETE FROM {$wpdb->comments}
WHERE comment_approved = 'trash'"
);
$this->log( "Comentários na lixeira removidos: {$deleted}" );
}
/**
* Remove pingbacks e trackbacks
*/
private function delete_pingbacks_trackbacks() {
global $wpdb;
$deleted = $wpdb->query(
"DELETE FROM {$wpdb->comments}
WHERE comment_type IN ('pingback', 'trackback')"
);
$this->log( "Pingbacks/trackbacks removidos: {$deleted}" );
}
/**
* Remove transients expirados (dados temporários)
*/
private function delete_expired_transients() {
global $wpdb;
$timestamp = time();
// Remove transients expirados em lote para evitar timeout
$batch = 50;
$total_deleted = 0;
while ( true ) {
// Busca chaves de timeout expiradas
$timeout_keys = $wpdb->get_col( $wpdb->prepare(
"SELECT option_name FROM {$wpdb->options}
WHERE option_name LIKE '_transient_timeout_%'
AND CAST(option_value AS UNSIGNED) < %d LIMIT %d", $timestamp, $batch ) ); if ( empty( $timeout_keys ) ) { break; } // Prepara as chaves dos transients correspondentes $transient_keys = []; foreach ( $timeout_keys as $key ) { $transient_keys[] = '_transient_' . substr( $key, 19 ); } // Junta todas as chaves para remoção $all_keys = array_merge( $timeout_keys, $transient_keys ); $placeholders = implode( ', ', array_fill( 0, count( $all_keys ), '%s' ) ); $deleted = $wpdb->query( $wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name IN ({$placeholders})",
$all_keys
) );
$total_deleted += $deleted;
if ( count( $timeout_keys ) < $batch ) { break; } } $this->log( "Transients expirados removidos: {$total_deleted}" );
}
/**
* Remove postmeta órfão (sem post correspondente)
*/
private function delete_orphaned_post_meta() {
global $wpdb;
$deleted = $wpdb->query(
"DELETE pm FROM {$wpdb->postmeta} pm
LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE p.ID IS NULL"
);
$this->log( "Postmeta órfão removido: {$deleted}" );
}
/**
* Remove commentmeta órfão (sem comment correspondente)
*/
private function delete_orphaned_comment_meta() {
global $wpdb;
$deleted = $wpdb->query(
"DELETE cm FROM {$wpdb->commentmeta} cm
LEFT JOIN {$wpdb->comments} c ON c.comment_ID = cm.comment_id
WHERE c.comment_ID IS NULL"
);
$this->log( "Commentmeta órfão removido: {$deleted}" );
}
/**
* Remove usermeta órfão (sem user correspondente)
*/
private function delete_orphaned_user_meta() {
global $wpdb;
$deleted = $wpdb->query(
"DELETE um FROM {$wpdb->usermeta} um
LEFT JOIN {$wpdb->users} u ON u.ID = um.user_id
WHERE u.ID IS NULL"
);
$this->log( "Usermeta órfão removido: {$deleted}" );
}
/**
* Remove termmeta órfão (sem term correspondente)
*/
private function delete_orphaned_term_meta() {
global $wpdb;
$deleted = $wpdb->query(
"DELETE tm FROM {$wpdb->termmeta} tm
LEFT JOIN {$wpdb->terms} t ON t.term_id = tm.term_id
WHERE t.term_id IS NULL"
);
$this->log( "Termmeta órfão removido: {$deleted}" );
}
/**
* Remove relacionamentos term_relationships órfãos (sem post ou term correspondente)
*/
private function delete_orphaned_term_relationships() {
global $wpdb;
// Remove relacionamentos sem post
$deleted1 = $wpdb->query(
"DELETE tr FROM {$wpdb->term_relationships} tr
LEFT JOIN {$wpdb->posts} p ON p.ID = tr.object_id
WHERE p.ID IS NULL"
);
// Remove relacionamentos sem term_taxonomy
$deleted2 = $wpdb->query(
"DELETE tr FROM {$wpdb->term_relationships} tr
LEFT JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
WHERE tt.term_taxonomy_id IS NULL"
);
$total = $deleted1 + $deleted2;
$this->log( "Relacionamentos term_relationships órfãos removidos: {$total}" );
}
/**
* Remove opções órfãs (resíduos de plugins desinstalados)
* Lista negra básica de prefixos comuns de plugins.
*/
private function delete_orphaned_options() {
global $wpdb;
// Lista de prefixos de opções que geralmente são resíduos
$prefixes = [
'widget_', 'cron', 'recently_activated', 'wpseo_', '_transient_',
'_site_transient_', 'rewrite_rules', 'auto_update_', 'wp_user_roles'
];
// Constrói a cláusula NOT LIKE para cada prefixo (mantém os essenciais)
$not_like = "";
foreach ( $prefixes as $prefix ) {
$not_like .= $wpdb->prepare( " AND option_name NOT LIKE %s", $prefix . '%' );
}
// Busca opções que não correspondem a plugins ativos ou temas (simplificado)
$sql = "SELECT option_name FROM {$wpdb->options}
WHERE (option_name LIKE 'wp_%' OR option_name LIKE 'woocommerce_%'
OR option_name LIKE 'elementor_%' OR option_name LIKE 'jetpack_%')
AND autoload = 'no' {$not_like}
LIMIT 200";
$options_to_delete = $wpdb->get_col( $sql );
if ( ! empty( $options_to_delete ) ) {
$placeholders = implode( ', ', array_fill( 0, count( $options_to_delete ), '%s' ) );
$deleted = $wpdb->query( $wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name IN ({$placeholders})",
$options_to_delete
) );
$this->log( "Opções órfãs removidas: {$deleted}" );
} else {
$this->log( "Nenhuma opção órfã encontrada para remoção." );
}
}
/**
* Remove cache oEmbed (transients específicos)
*/
private function delete_oembed_cache() {
global $wpdb;
$deleted = $wpdb->query(
"DELETE FROM {$wpdb->postmeta}
WHERE meta_key LIKE '_oembed_%'"
);
$this->log( "Cache oEmbed removido: {$deleted} registros" );
}
/**
* Otimiza todas as tabelas do banco de dados (opcional)
* Pode ser pesado em sites grandes; desabilite se necessário.
*/
private function optimize_tables() {
global $wpdb;
$tables = $wpdb->get_col( "SHOW TABLES" );
$optimized = 0;
foreach ( $tables as $table ) {
if ( $wpdb->query( "OPTIMIZE TABLE {$table}" ) ) {
$optimized++;
}
}
$this->log( "Tabelas otimizadas: {$optimized} de " . count( $tables ) );
}
/**
* Função de log simples (escreve em arquivo se habilitado)
*/
private function log( $message ) {
if ( ! $this->log_enabled ) {
return;
}
$log_file = WP_CONTENT_DIR . '/db-cleaner-log.txt';
$timestamp = current_time( 'mysql' );
$entry = "[{$timestamp}] {$message}" . PHP_EOL;
error_log( $entry, 3, $log_file );
}
}
Neodoor_DB_Cleaner::get_instance();
Instruções de instalação
- Certifique-se de que o autoloader
loader.phpesteja presente em/mu-plugins/(conforme aula anterior). - Crie a pasta
neodoor-db-cleanerdentro de/mu-plugins/. - Dentro dela, crie o arquivo
neodoor-db-cleaner.phpe cole o código acima. - Desative e remova o plugin
advanced-database-cleanerda lista de plugins. - O MU-Plugin começará a funcionar imediatamente e agendará a limpeza diária automaticamente.
Configuração avançada (via constantes no wp-config.php)
Você pode ajustar o comportamento do plugin definindo as seguintes constantes no arquivo wp-config.php:
// Mantém revisões dos últimos 60 dias (padrão: 30)
define( 'NEODOOR_DB_CLEANER_KEEP_REVISIONS_DAYS', 60 );
// Habilita logs de depuração (arquivo /wp-content/db-cleaner-log.txt)
define( 'NEODOOR_DB_CLEANER_LOG_ENABLED', true );
Teste manual da limpeza
Para executar a limpeza imediatamente (sem aguardar o cron diário), adicione o seguinte trecho temporariamente no tema ou em um plugin de teste:
do_action( 'neodoor_db_daily_cleanup' );
Após executar, remova o trecho para não rodar duas vezes.
Verificação e monitoramento
- phpMyAdmin: Acesse seu banco de dados e verifique a redução no tamanho das tabelas
wp_posts,wp_postmetaewp_options. - Logs: Se habilitado, consulte o arquivo
/wp-content/db-cleaner-log.txtpara ver o que foi removido. - Performance: Monitore o tempo de carregamento do site antes e depois da limpeza.
Limitações e cuidados
- Sempre faça backup do banco de dados antes de executar limpezas agressivas. Em ambiente local isso é menos crítico, mas é uma boa prática.
- A remoção de opções órfãs é conservadora (mantém opções com prefixos conhecidos como
widget_,cron, etc.) para não quebrar funcionalidades. Se precisar de limpeza mais profunda, revise a lista de exclusão. - Em sites muito grandes (milhares de posts), a limpeza de transients e metadados pode levar alguns segundos. O código usa batch para evitar timeout.
- Se você usa cache externo (Redis, Memcached), os transients podem não estar na base de dados – nesse caso, a remoção via SQL não terá efeito.
- Para uma limpeza ainda mais completa (tabelas órfãs, detecção de plugins inativos), recomenda-se executar manualmente scripts SQL específicos ou usar o WP-CLI.
Comparação com o plugin original
Nosso MU-Plugin substitui as principais funções do Advanced Database Cleaner, com as seguintes diferenças:
- Sem interface administrativa: Tudo ocorre em segundo plano, sem botões ou telas para configurar.
- Mais leve: Carrega apenas o necessário, sem CSS/JS desnecessários.
- Transparente: O código está visível e pode ser ajustado conforme sua necessidade.
- Agendamento fixo: Executa diariamente. Para outras frequências, altere a constante em
wp_schedule_event.
Registro no caderno
Data:
Funcionalidade: Limpeza automática do banco de dados (revisões, transients, metadados órfãos, etc.)
Plugins substituídos: advanced-database-cleaner
Tempo de implementação: ~30 minutos (configuração e testes)
Observação: Em produção, execute a limpeza manualmente antes de ativar o cron para evitar picos de carga.
Explicação técnica sobre o ‘prefixo’
O prefixo NEODOOR (ou Neodoor_) é apenas um identificador único que usamos para:
-
Nomear classes (ex:
Neodoor_Disable_RightClick) – para evitar conflito com outras classes que por acaso tenham o mesmo nome. -
Nomear constantes (ex:
NEODOOR_DB_CLEANER_KEEP_REVISIONS_DAYS) – para evitar conflitos globais. -
Nomear hooks/custom actions (ex:
neodoor_db_daily_cleanup) – para garantir que nenhum outro plugin use o mesmo nome de hook.
O WordPress não exige nenhum prefixo específico. A única regra é: use um prefixo único e consistente para tudo que você criar (classes, funções, constantes, hooks). Isso é uma boa prática de programação para evitar colisões com temas/plugins de terceiros.
O que acontece se você trocar NEODOOR por PORTANOVA?
-
O plugin continua funcionando normalmente – pois todas as referências internas estarão consistentes.
-
Nada quebra – desde que você substitua todas as ocorrências do antigo prefixo pelo novo no código.
Cuidados ao trocar
-
Use “pesquisar e substituir” no seu editor de código (VS Code, Notepad++, etc.) para garantir que nenhuma ocorrência seja esquecida.
-
Atenção a letras maiúsculas/minúsculas –
Neodooré diferente deNEODOOR. No código temos:-
Neodoor_(classe) -
neodoor_(hooks, funções) -
NEODOOR_(constantes) -
Ao trocar, mantenha a mesma capitalização:
Portanova_,portanova_,PORTANOVA_.
-
-
Recomendo manter um padrão – por exemplo, sempre usar o nome do seu projeto (ex:
portanova) em minúsculas para hooks e funções, e maiúsculas para constantes.
Vantagem de usar um prefixo personalizado
-
Identifica facilmente que aquele código foi criado por você.
-
Evita conflitos caso instale um plugin que use o mesmo nome de classe (ex: outra pessoa também criou uma classe chamada
Disable_RightClick). -
Mais fácil de dar manutenção em projetos grandes.