千万级大表如何删除数据?

大家好,我是苏三,又跟大家见面了。

前言

今天我们来聊聊一个让很多DBA和开发者头疼的话题------千万级大表的数据删除。

有些小伙伴在工作中,一遇到大表数据删除就手足无措,要么直接DELETE导致数据库卡死,要么畏手畏脚不敢操作。

我见过太多因为大表删除操作不当导致的"血案":数据库长时间锁表、业务系统瘫痪、甚至主从同步延迟。

今天跟大家一起专门聊聊千万级大表数据删除的话题,希望对你会有所帮助。

最近准备面试的小伙伴,可以看一下这个宝藏网站(Java突击队):www.susan.net.cn,里面:面试八股文、场景设计题、面试真题、7个项目实战、工作内推什么都有

一、为什么大表删除这么难?

在深入技术方案之前,我们先搞清楚为什么千万级大表的数据删除会如此困难。

有些小伙伴可能会想:"不就是个DELETE语句吗,有什么难的?"

其实这里面大有学问。

数据库删除操作的底层原理

为了更直观地理解数据库删除操作的工作原理,我画了一个删除操作的底层流程图:

从这张图可以看出,一个简单的DELETE语句背后隐藏着这么多复杂的操作。

让我们详细分析每个环节的挑战:

1. 事务和锁的挑战

sql 复制代码
-- 一个看似简单的删除操作
DELETE FROM user_operation_log 
WHERE create_time < '2023-01-01';

-- 实际上MySQL会这样处理:
-- 1. 获取表的写锁
-- 2. 逐行扫描10,000,000条记录
-- 3. 对每条匹配的记录:
--    - 写入undo log(用于回滚)
--    - 写入redo log(用于恢复)
--    - 更新所有相关索引
--    - 标记记录为删除状态
-- 4. 事务提交后才真正释放空间

2. 资源消耗问题

  • 磁盘I/O:undo log、redo log、数据文件、索引文件的大量写入
  • CPU:索引维护、条件判断、事务管理
  • 内存:Buffer Pool管理、锁信息维护
  • 网络:主从同步数据量巨大

3. 业务影响风险

  • 锁等待超时:其他查询被阻塞
  • 主从延迟:从库同步跟不上
  • 磁盘空间:undo log暴增导致磁盘写满
  • 性能下降:数据库整体性能受影响

有些小伙伴可能会问:"我们用的是云数据库,这些问题还存在吗?"

我的经验是:云数据库只是降低了运维复杂度,但底层原理和限制依然存在

二、方案一:分批删除(最常用)

分批删除是最基础也是最常用的方案,核心思想是"化整为零",将大操作拆分成多个小操作。

实现原理

具体实现

方法1:基于主键分批

sql 复制代码
-- 存储过程实现分批删除
DELIMITER $$
CREATE PROCEDURE batch_delete_by_id()
BEGIN
    DECLARE done INTDEFAULTFALSE;
    DECLARE batch_size INTDEFAULT1000;
    DECLARE max_id BIGINT;
    DECLARE min_id BIGINT;
    DECLARE current_id BIGINTDEFAULT0;
    
    -- 获取需要删除的数据范围
    SELECTMIN(id), MAX(id) INTO min_id, max_id 
    FROM user_operation_log 
    WHERE create_time < '2023-01-01';
    
    WHILE current_id < max_id DO
        -- 每次删除一个批次
        DELETEFROM user_operation_log 
        WHEREidBETWEEN current_id AND current_id + batch_size - 1
        AND create_time < '2023-01-01';
        
        -- 提交事务,释放锁
        COMMIT;
        
        -- 休眠一下,让数据库喘口气
        DOSLEEP(0.1);
        
        -- 更新进度
        SET current_id = current_id + batch_size;
        
        -- 记录日志(可选)
        INSERTINTO delete_progress_log 
        VALUES (NOW(), current_id, batch_size);
    ENDWHILE;
END$$
DELIMITER ;

方法2:基于时间分批

ini 复制代码
// Java代码实现基于时间的分批删除
@Service
@Slf4j
public class BatchDeleteService {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    /**
     * 基于时间范围的分批删除
     */
    public void batchDeleteByTime(String tableName, String timeColumn, 
                                  Date startTime, Date endTime, 
                                  int batchDays) {
        
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(startTime);
        
        int totalDeleted = 0;
        long startMs = System.currentTimeMillis();
        
        while (calendar.getTime().before(endTime)) {
            Date batchStart = calendar.getTime();
            calendar.add(Calendar.DAY_OF_YEAR, batchDays);
            Date batchEnd = calendar.getTime();
            
            // 确保不超过结束时间
            if (batchEnd.after(endTime)) {
                batchEnd = endTime;
            }
            
            String sql = String.format(
                "DELETE FROM %s WHERE %s BETWEEN ? AND ? LIMIT 1000",
                tableName, timeColumn
            );
            
            int deleted = jdbcTemplate.update(sql, batchStart, batchEnd);
            totalDeleted += deleted;
            
            log.info("批次删除完成: {}-{}, 删除{}条, 总计{}条",
                    batchStart, batchEnd, deleted, totalDeleted);
            
            // 控制删除频率,避免对数据库造成过大压力
            if (deleted > 0) {
                try {
                    Thread.sleep(500); // 休眠500ms
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            } else {
                // 没有数据可删,跳到下一个时间段
                continue;
            }
            
            // 每删除10000条记录一次进度
            if (totalDeleted % 10000 == 0) {
                logProgress(totalDeleted, startMs);
            }
        }
        
        log.info("删除任务完成! 总计删除{}条记录, 耗时{}秒",
                totalDeleted, (System.currentTimeMillis() - startMs) / 1000);
    }
    
    private void logProgress(int totalDeleted, long startMs) {
        long costMs = System.currentTimeMillis() - startMs;
        double recordsPerSecond = totalDeleted * 1000.0 / costMs;
        
        log.info("删除进度: {}条, 速率: {}/秒, 耗时: {}秒",
                totalDeleted, String.format("%.2f", recordsPerSecond), costMs / 1000);
    }
}

方法3:使用LIMIT分批删除

sql 复制代码
-- 简单的LIMIT分批删除
DELIMITER $$
CREATE PROCEDURE batch_delete_with_limit()
BEGIN
    DECLARE done INT DEFAULT 0;
    DECLARE batch_size INT DEFAULT 1000;
    DECLARE total_deleted INT DEFAULT 0;
    
    WHILE done = 0 DO
        -- 每次删除1000条
        DELETE FROM user_operation_log 
        WHERE create_time < '2023-01-01'
        LIMIT batch_size;
        
        -- 检查是否还有数据
        SET done = ROW_COUNT() = 0;
        SET total_deleted = total_deleted + ROW_COUNT();
        
        -- 提交释放锁
        COMMIT;
        
        -- 休眠控制频率
        DOSLEEP(0.1);
        
        -- 每删除10000条输出日志
        IF total_deleted % 10000 = 0 THEN
            SELECT CONCAT('已删除: ', total_deleted, ' 条记录') AS progress;
        END IF;
    END WHILE;
    
    SELECT CONCAT('删除完成! 总计: ', total_deleted, ' 条记录') ASresult;
END$$
DELIMITER ;

分批删除的最佳实践

  1. 批次大小选择
    • 小表:1000-5000条/批次
    • 大表:100-1000条/批次
    • 需要根据实际情况调整
  2. 休眠时间控制
    • 业务高峰期:休眠1-2秒
    • 业务低峰期:休眠100-500毫秒
    • 夜间维护:可不休眠或短暂休眠
  3. 监控和调整
    • 监控数据库负载
    • 观察主从同步延迟
    • 根据实际情况动态调整参数

三、方案二:创建新表+重命名

当需要删除表中大部分数据时,创建新表然后重命名的方式往往更高效。

实现原理

具体实现

sql 复制代码
-- 步骤1: 创建新表(结构同原表)
CREATE TABLE user_operation_log_new LIKE user_operation_log;

-- 步骤2: 导入需要保留的数据
INSERT INTO user_operation_log_new 
SELECT * FROM user_operation_log 
WHERE create_time >= '2023-01-01';

-- 步骤3: 创建索引(在数据导入后创建,效率更高)
ALTER TABLE user_operation_log_new ADDINDEX idx_create_time(create_time);
ALTER TABLE user_operation_log_new ADDINDEX idx_user_id(user_id);

-- 步骤4: 数据验证
SELECT
    (SELECT COUNT(*) FROM user_operation_log_new) as new_count,
    (SELECT COUNT(*) FROM user_operation_log WHERE create_time >= '2023-01-01') as expected_count;

-- 步骤5: 原子切换(需要很短的表锁)
RENAME TABLE
    user_operation_log TO user_operation_log_old,
    user_operation_log_new TO user_operation_log;

-- 步骤6: 删除旧表(可选立即删除或延后删除)
DROP TABLE user_operation_log_old;

Java代码辅助实现

typescript 复制代码
@Service  
@Slf4j
public class TableRebuildService {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    /**
     * 重建表方式删除数据
     */
    public void rebuildTableForDeletion(String sourceTable, String condition) {
        String newTable = sourceTable + "_new";
        String oldTable = sourceTable + "_old";
        
        try {
            // 1. 创建新表
            log.info("开始创建新表: {}", newTable);
            jdbcTemplate.execute("CREATE TABLE " + newTable + " LIKE " + sourceTable);
            
            // 2. 导入需要保留的数据
            log.info("开始导入保留数据");
            String insertSql = String.format(
                "INSERT INTO %s SELECT * FROM %s WHERE %s", 
                newTable, sourceTable, condition
            );
            int keptCount = jdbcTemplate.update(insertSql);
            log.info("成功导入{}条保留数据", keptCount);
            
            // 3. 创建索引(可选,在导入后创建索引效率更高)
            log.info("开始创建索引");
            createIndexes(newTable);
            
            // 4. 数据验证
            log.info("开始数据验证");
            if (!validateData(sourceTable, newTable, condition)) {
                throw new RuntimeException("数据验证失败");
            }
            
            // 5. 原子切换
            log.info("开始表切换");
            switchTables(sourceTable, newTable, oldTable);
            
            // 6. 删除旧表(可选立即或延后)
            log.info("开始删除旧表");
            dropTableSafely(oldTable);
            
            log.info("表重建删除完成!");
            
        } catch (Exception e) {
            log.error("表重建过程发生异常", e);
            // 清理临时表
            cleanupTempTable(newTable);
            throw e;
        }
    }
    
    private void createIndexes(String tableName) {
        // 根据业务需要创建索引
        String[] indexes = {
            "CREATE INDEX idx_create_time ON " + tableName + "(create_time)",
            "CREATE INDEX idx_user_id ON " + tableName + "(user_id)"
        };
        
        for (String sql : indexes) {
            jdbcTemplate.execute(sql);
        }
    }
    
    private boolean validateData(String sourceTable, String newTable, String condition) {
        // 验证新表数据量是否正确
        Integer newCount = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM " + newTable, Integer.class);
        
        Integer expectedCount = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM " + sourceTable + " WHERE " + condition, Integer.class);
        
        return newCount.equals(expectedCount);
    }
    
    private void switchTables(String sourceTable, String newTable, String oldTable) {
        // 原子性的表重命名操作
        String sql = String.format(
            "RENAME TABLE %s TO %s, %s TO %s", 
            sourceTable, oldTable, newTable, sourceTable
        );
        jdbcTemplate.execute(sql);
    }
    
    private void dropTableSafely(String tableName) {
        try {
            jdbcTemplate.execute("DROP TABLE " + tableName);
        } catch (Exception e) {
            log.warn("删除表失败: {}, 需要手动清理", tableName, e);
        }
    }
    
    private void cleanupTempTable(String tableName) {
        try {
            jdbcTemplate.execute("DROP TABLE IF EXISTS " + tableName);
        } catch (Exception e) {
            log.warn("清理临时表失败: {}", tableName, e);
        }
    }
}

适用场景

  • 需要删除表中超过50%的数据
  • 业务允许短暂的写停顿(重命名时需要)
  • 有足够的磁盘空间存储新旧两个表

四、方案三:分区表删除

如果表已经做了分区,或者可以改造为分区表,那么删除数据就会变得非常简单。

实现原理

具体实现

方法1:使用现有分区表

sql 复制代码
-- 查看表的分区情况
SELECT table_name, partition_name, table_rows
FROM information_schema.partitions 
WHERE table_name = 'user_operation_log';

-- 直接删除整个分区(秒级完成)
ALTER TABLE user_operation_log DROPPARTITION p202201, p202202;

-- 定期删除过期分区的存储过程
DELIMITER $$
CREATE PROCEDURE auto_drop_expired_partitions()
BEGIN
    DECLARE expired_partition VARCHAR(64);
    DECLARE done INT DEFAULT FALSE;
    
    -- 查找需要删除的分区(保留最近12个月)
    DECLARE cur CURSOR FOR
    SELECT partition_name 
    FROM information_schema.partitions 
    WHERE table_name = 'user_operation_log'
    AND partition_name LIKE'p%'
    AND STR_TO_DATE(REPLACE(partition_name, 'p', ''), '%Y%m') < DATE_SUB(NOW(), INTERVAL12MONTH);
    
    DECLARE CONTINUE HANDLER FOR NOT FOUNDS ET done = TRUE;
    
    OPEN cur;
    
    read_loop: LOOP
        FETCH cur INTO expired_partition;
        IF done THEN
            LEAVE read_loop;
        ENDIF;
        
        -- 删除过期分区
        SET @sql = CONCAT('ALTER TABLE user_operation_log DROP PARTITION ', expired_partition);
        PREPARE stmt FROM @sql;
        EXECUTE stmt;
        DEALLOCATE PREPARE stmt;
        
        -- 记录日志
        INSERT INTO partition_clean_log 
        VALUES (NOW(), expired_partition, 'DROPPED');
    END LOOP;
    
    CLOSE cur;
END$$
DELIMITER ;

方法2:改造普通表为分区表

sql 复制代码
-- 将普通表改造成分区表
-- 步骤1: 创建分区表
CREATE TABLE user_operation_log_partitioned (
    id BIGINT AUTO_INCREMENT,
    user_id BIGINT,
    operation VARCHAR(100),
    create_time DATETIME,
    PRIMARY KEY (id, create_time)  -- 分区键必须包含在主键中
) PARTITION BY RANGE (YEAR(create_time)*100 + MONTH(create_time)) (
    PARTITION p202201 VALUES LESS THAN (202202),
    PARTITION p202202 VALUES LESS THAN (202203),
    PARTITION p202203 VALUES LESS THAN (202204),
    PARTITION p202204 VALUES LESS THAN (202205),
    PARTITION pfuture VALUES LESS THAN MAXVALUE
);

-- 步骤2: 导入数据
INSERTINTO user_operation_log_partitioned 
SELECT * FROM user_operation_log;

-- 步骤3: 切换表
RENAME TABLE
    user_operation_log TO user_operation_log_old,
    user_operation_log_partitioned TO user_operation_log;

-- 步骤4: 定期维护:添加新分区
ALTER TABLE user_operation_log REORGANIZE PARTITION pfuture INTO (
    PARTITION p202205 VALUESLESSTHAN (202206),
    PARTITION p202206 VALUESLESSTHAN (202207),
    PARTITION pfuture VALUESLESSTHAN MAXVALUE
);

Java代码实现分区管理

typescript 复制代码
@Service
@Slf4j
public class PartitionManagerService {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    /**
     * 自动管理分区
     */
    @Scheduled(cron = "0 0 2 * * ?")  // 每天凌晨2点执行
    public void autoManagePartitions() {
        log.info("开始分区维护任务");
        
        try {
            // 1. 删除过期分区(保留最近12个月)
            dropExpiredPartitions();
            
            // 2. 创建未来分区
            createFuturePartitions();
            
            log.info("分区维护任务完成");
            
        } catch (Exception e) {
            log.error("分区维护任务失败", e);
        }
    }
    
    private void dropExpiredPartitions() {
        String sql = "SELECT partition_name " +
                    "FROM information_schema.partitions " +
                    "WHERE table_name = 'user_operation_log' " +
                    "AND partition_name LIKE 'p%' " +
                    "AND STR_TO_DATE(REPLACE(partition_name, 'p', ''), '%Y%m') < DATE_SUB(NOW(), INTERVAL 12 MONTH)";
        
        List<String> expiredPartitions = jdbcTemplate.queryForList(sql, String.class);
        
        for (String partition : expiredPartitions) {
            try {
                jdbcTemplate.execute("ALTER TABLE user_operation_log DROP PARTITION " + partition);
                log.info("成功删除分区: {}", partition);
                
                // 记录操作日志
                logPartitionOperation("DROP", partition, "SUCCESS");
                
            } catch (Exception e) {
                log.error("删除分区失败: {}", partition, e);
                logPartitionOperation("DROP", partition, "FAILED: " + e.getMessage());
            }
        }
    }
    
    private void createFuturePartitions() {
        // 创建未来3个月的分区
        for (int i = 1; i <= 3; i++) {
            LocalDate futureDate = LocalDate.now().plusMonths(i);
            String partitionName = "p" + futureDate.format(DateTimeFormatter.ofPattern("yyyyMM"));
            int partitionValue = futureDate.getYear() * 100 + futureDate.getMonthValue();
            int nextPartitionValue = partitionValue + 1;
            
            try {
                String sql = String.format(
                    "ALTER TABLE user_operation_log REORGANIZE PARTITION pfuture INTO (" +
                    "PARTITION %s VALUES LESS THAN (%d), " +
                    "PARTITION pfuture VALUES LESS THAN MAXVALUE)",
                    partitionName, nextPartitionValue
                );
                
                jdbcTemplate.execute(sql);
                log.info("成功创建分区: {}", partitionName);
                logPartitionOperation("CREATE", partitionName, "SUCCESS");
                
            } catch (Exception e) {
                log.warn("创建分区失败(可能已存在): {}", partitionName, e);
            }
        }
    }
    
    private void logPartitionOperation(String operation, String partition, String status) {
        jdbcTemplate.update(
            "INSERT INTO partition_operation_log(operation, partition_name, status, create_time) VALUES (?, ?, ?, NOW())",
            operation, partition, status
        );
    }
}

分区表的优势

  1. 删除效率极高:直接删除分区文件
  2. 不影响业务:无锁表风险
  3. 管理方便:可以自动化管理
  4. 查询优化:分区裁剪提升查询性能

最近为了帮助大家找工作,专门建了一些工作内推群,各大城市都有,欢迎各位HR和找工作的小伙伴进群交流,群里目前已经收集了不少的工作内推岗位。加苏三的微信:li_su223,备注:掘金+所在城市,即可进群。

五、方案四:使用临时表同步

对于需要在线删除且不能停止服务的场景,可以使用临时表同步的方式。

实现原理

具体实现

typescript 复制代码
@Service
@Slf4j
public class OnlineTableMigrationService {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    /**
     * 在线表迁移删除
     */
    public void onlineMigrationDelete(String sourceTable, String condition) {
        String newTable = sourceTable + "_new";
        String tempTable = sourceTable + "_temp";
        
        try {
            // 阶段1: 准备阶段
            log.info("=== 阶段1: 准备阶段 ===");
            prepareMigration(sourceTable, newTable, tempTable);
            
            // 阶段2: 双写阶段
            log.info("=== 阶段2: 双写阶段 ===");
            enableDoubleWrite(sourceTable, newTable);
            
            // 阶段3: 数据同步阶段
            log.info("=== 阶段3: 数据同步阶段 ===");
            syncExistingData(sourceTable, newTable, condition);
            
            // 阶段4: 验证阶段
            log.info("=== 阶段4: 验证阶段 ===");
            if (!validateDataSync(sourceTable, newTable)) {
                thrownew RuntimeException("数据同步验证失败");
            }
            
            // 阶段5: 切换阶段
            log.info("=== 阶段5: 切换阶段 ===");
            switchToNewTable(sourceTable, newTable, tempTable);
            
            // 阶段6: 清理阶段
            log.info("=== 阶段6: 清理阶段 ===");
            cleanupAfterSwitch(sourceTable, tempTable);
            
            log.info("在线迁移删除完成!");
            
        } catch (Exception e) {
            log.error("在线迁移过程发生异常", e);
            // 回滚双写
            disableDoubleWrite();
            throw e;
        }
    }
    
    private void prepareMigration(String sourceTable, String newTable, String tempTable) {
        // 备份原表
        jdbcTemplate.execute("CREATE TABLE " + tempTable + " LIKE " + sourceTable);
        jdbcTemplate.execute("INSERT INTO " + tempTable + " SELECT * FROM " + sourceTable);
        
        // 创建新表
        jdbcTemplate.execute("CREATE TABLE " + newTable + " LIKE " + sourceTable);
    }
    
    private void enableDoubleWrite(String sourceTable, String newTable) {
        // 这里需要修改应用层代码,实现双写
        // 或者在数据库层使用触发器(不推荐,影响性能)
        log.info("请配置应用层双写: 同时写入 {} 和 {}", sourceTable, newTable);
        
        // 等待双写配置生效
        sleep(5000);
    }
    
    private void syncExistingData(String sourceTable, String newTable, String condition) {
        log.info("开始同步存量数据");
        
        // 同步符合条件的数据到新表
        String syncSql = String.format(
            "INSERT IGNORE INTO %s SELECT * FROM %s WHERE %s", 
            newTable, sourceTable, condition
        );
        
        int syncedCount = jdbcTemplate.update(syncSql);
        log.info("存量数据同步完成: {} 条记录", syncedCount);
        
        // 等待双写追平增量数据
        log.info("等待增量数据追平...");
        sleep(30000); // 等待30秒,根据业务调整
        
        // 检查数据一致性
        checkDataConsistency(sourceTable, newTable);
    }
    
    private void checkDataConsistency(String sourceTable, String newTable) {
        // 检查关键业务数据的一致性
        Integer sourceCount = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM " + sourceTable, Integer.class);
        
        Integer newCount = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM " + newTable, Integer.class);
        
        log.info("数据一致性检查: 原表{}条, 新表{}条", sourceCount, newCount);
        
        // 这里可以添加更详细的一致性检查
    }
    
    private boolean validateDataSync(String sourceTable, String newTable) {
        // 验证数据同步的正确性
        // 这里可以实现更复杂的验证逻辑
        
        log.info("数据同步验证通过");
        returntrue;
    }
    
    private void switchToNewTable(String sourceTable, String newTable, String tempTable) {
        // 短暂停写(根据业务情况,可能不需要)
        log.info("开始停写切换...");
        sleep(5000); // 停写5秒
        
        // 原子切换
        jdbcTemplate.execute("RENAME TABLE " + 
            sourceTable + " TO " + sourceTable + "_backup, " +
            newTable + " TO " + sourceTable);
        
        log.info("表切换完成");
    }
    
    private void cleanupAfterSwitch(String sourceTable, String tempTable) {
        // 关闭双写
        disableDoubleWrite();
        
        // 延迟删除备份表(保留一段时间)
        log.info("备份表保留: {}_backup", sourceTable);
        log.info("临时表已删除: {}", tempTable);
        
        jdbcTemplate.execute("DROP TABLE " + tempTable);
    }
    
    private void disableDoubleWrite() {
        log.info("请关闭应用层双写配置");
    }
    
    private void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

六、方案五:使用专业工具

对于特别大的表或者复杂的删除需求,可以使用专业的数据库工具。

1. pt-archiver(Percona Toolkit)

bash 复制代码
# 安装Percona Toolkit
# Ubuntu/Debian: 
sudo apt-get install percona-toolkit

# 使用pt-archiver归档删除数据
pt-archiver \
    --source h=localhost,D=test,t=user_operation_log \
    --where"create_time < '2023-01-01'" \
    --limit 1000 \
    --commit-each \
    --sleep 0.1 \
    --statistics \
    --progress 10000 \
    --why-not \
    --dry-run  # 先试运行,确认无误后移除此参数

# 实际执行删除
pt-archiver \
    --source h=localhost,D=test,t=user_operation_log \
    --where"create_time < '2023-01-01'" \
    --limit 1000 \
    --commit-each \
    --sleep 0.1 \
    --purge

2. 自定义工具类

java 复制代码
@Component
@Slf4j
public class SmartDeleteTool {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    /**
     * 智能删除决策
     */
    public void smartDelete(String tableName, String condition) {
        try {
            // 1. 分析表状态
            TableAnalysisResult analysis = analyzeTable(tableName, condition);
            
            // 2. 根据分析结果选择最佳方案
            DeleteStrategy strategy = chooseBestStrategy(analysis);
            
            // 3. 执行删除
            executeDelete(strategy, tableName, condition);
            
        } catch (Exception e) {
            log.error("智能删除失败", e);
            throw e;
        }
    }
    
    private TableAnalysisResult analyzeTable(String tableName, String condition) {
        TableAnalysisResult result = new TableAnalysisResult();
        
        // 分析表大小
        result.setTotalRows(getTableRowCount(tableName));
        result.setDeleteRows(getDeleteRowCount(tableName, condition));
        result.setDeleteRatio(result.getDeleteRows() * 1.0 / result.getTotalRows());
        
        // 分析表结构
        result.setHasPartition(isTablePartitioned(tableName));
        result.setHasPrimaryKey(hasPrimaryKey(tableName));
        result.setIndexCount(getIndexCount(tableName));
        
        // 分析系统负载
        result.setSystemLoad(getSystemLoad());
        
        return result;
    }
    
    private DeleteStrategy chooseBestStrategy(TableAnalysisResult analysis) {
        if (analysis.isHasPartition() && analysis.getDeleteRatio() > 0.3) {
            return DeleteStrategy.PARTITION_DROP;
        }
        
        if (analysis.getDeleteRatio() > 0.5) {
            return DeleteStrategy.TABLE_REBUILD;
        }
        
        if (analysis.getTotalRows() > 10_000_000) {
            return DeleteStrategy.BATCH_DELETE_WITH_PAUSE;
        }
        
        return DeleteStrategy.BATCH_DELETE;
    }
    
    private void executeDelete(DeleteStrategy strategy, String tableName, String condition) {
        switch (strategy) {
            case PARTITION_DROP:
                executePartitionDrop(tableName, condition);
                break;
            case TABLE_REBUILD:
                executeTableRebuild(tableName, condition);
                break;
            case BATCH_DELETE_WITH_PAUSE:
                executeBatchDeleteWithPause(tableName, condition);
                break;
            default:
                executeBatchDelete(tableName, condition);
        }
    }
    
    // 各种策略的具体实现...
    
    private long getTableRowCount(String tableName) {
        String sql = "SELECT COUNT(*) FROM " + tableName;
        return jdbcTemplate.queryForObject(sql, Long.class);
    }
    
    private long getDeleteRowCount(String tableName, String condition) {
        String sql = "SELECT COUNT(*) FROM " + tableName + " WHERE " + condition;
        return jdbcTemplate.queryForObject(sql, Long.class);
    }
    
    private boolean isTablePartitioned(String tableName) {
        String sql = "SELECT COUNT(*) FROM information_schema.partitions " +
                    "WHERE table_name = ? AND partition_name IS NOT NULL";
        Integer count = jdbcTemplate.queryForObject(sql, Integer.class, tableName);
        return count != null && count > 0;
    }
    
    // 其他分析方法...
}

enum DeleteStrategy {
    BATCH_DELETE,           // 普通分批删除
    BATCH_DELETE_WITH_PAUSE, // 带休眠的分批删除
    TABLE_REBUILD,          // 重建表
    PARTITION_DROP,         // 删除分区
    ONLINE_MIGRATION        // 在线迁移
}

@Data
class TableAnalysisResult {
    private long totalRows;
    private long deleteRows;
    private double deleteRatio;
    private boolean hasPartition;
    private boolean hasPrimaryKey;
    private int indexCount;
    private double systemLoad;
}

七、方案对比与选择指南

为了帮助大家选择合适的方案,我整理了详细的对比表:

方案对比矩阵

方案 适用场景 优点 缺点 风险等级
分批删除 小批量删除, 删除比例<30% 实现简单, 无需停服 执行时间长, 可能锁表
重建表 删除比例>50%, 可接受短暂停写 执行速度快, 整理表碎片 需要停写, 需要额外空间
分区删除 表已分区或可分区 秒级完成, 无性能影响 需要前期规划, 改造成本
在线同步 要求零停机, 重要业务表 业务无感知, 安全可靠 实现复杂, 周期较长
专业工具 复杂场景, 超大表操作 功能强大, 自动优化 学习成本, 依赖外部工具

选择决策流程图

实战建议

  1. 测试环境验证:任何删除方案都要先在测试环境验证
  2. 备份优先:删除前一定要备份数据
  3. 业务低峰期:选择业务低峰期执行删除操作
  4. 监控告警:实时监控数据库状态,设置告警阈值
  5. 回滚预案:准备完善的回滚方案

总结

经过上面的详细分析,我们来总结一下千万级大表数据删除的核心要点。

核心原则

  1. 安全第一:任何删除操作都要确保数据安全
  2. 影响最小:尽量减少对业务的影响
  3. 效率优先:选择最适合的高效方案
  4. 可监控:整个过程要可监控、可控制

技术选型口诀

根据多年的实战经验,我总结了一个简单的选型口诀:

看分区,判比例,定方案

  • 有分区:直接删除分区最快
  • 删的少:分批删除最稳妥
  • 删的多:重建表最高效
  • 不能停:在线同步最安全

最后的建议

大表数据删除是一个需要谨慎对待的操作,我建议大家:

  1. 预防优于治疗:通过数据生命周期管理,定期清理数据
  2. 架构要合理:在设计阶段就考虑数据清理策略
  3. 工具要熟练:掌握各种删除工具的使用方法
  4. 经验要积累:每次操作后都要总结经验教训

记住:没有最好的方案,只有最适合的方案

最近准备面试的小伙伴,可以看一下这个宝藏网站(Java突击队):www.susan.net.cn,里面:面试八股文、场景设计题、面试真题、7个项目实战、工作内推什么都有

相关推荐
John_ToDebug5 小时前
架构的尺度:从单机到分布式,服务端技术的深度演进
后端·程序人生
调试人生的显微镜5 小时前
Fastlane 结合 开心上架 命令行版本实现跨平台上传发布 iOS App
后端
weixin_545019325 小时前
Spring Boot 项目开启 HTTPS 完整指南:从原理到实践
spring boot·后端·https
掘金一周6 小时前
第一台 Andriod XR 设备发布,Jetpack Compose XR 有什么不同?对原生开发有何影响? | 掘金一周 10.30
前端·人工智能·后端
张乔246 小时前
spring boot项目快速整合xxl-job实现定时任务
spring boot·后端·xxl-job
程序定小飞6 小时前
基于springboot的论坛网站设计与实现
java·开发语言·spring boot·后端·spring
PFinal社区_南丞6 小时前
测试驱动开发(TDD):以测试为引擎的软件工程实践
后端
初学者,亦行者6 小时前
Rust 模式匹配的穷尽性检查:从编译器证明到工程演进
后端·rust·django
IT_陈寒6 小时前
React性能翻倍!3个90%开发者不知道的Hooks优化技巧 🚀
前端·人工智能·后端