精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

千萬級大表如何刪除數據?

數據庫 其他數據庫
今天我們來聊聊一個讓很多DBA和開發者頭疼的話題——千萬級大表的數據刪除。有些小伙伴在工作中,一遇到大表數據刪除就手足無措,要么直接DELETE導致數據庫卡死,要么畏手畏腳不敢操作。

前言

今天我們來聊聊一個讓很多DBA和開發者頭疼的話題——千萬級大表的數據刪除。

有些小伙伴在工作中,一遇到大表數據刪除就手足無措,要么直接DELETE導致數據庫卡死,要么畏手畏腳不敢操作。

我見過太多因為大表刪除操作不當導致的"血案":數據庫長時間鎖表、業務系統癱瘓、甚至主從同步延遲。

今天跟大家一起專門聊聊千萬級大表數據刪除的話題,希望對你會有所幫助。

一、為什么大表刪除這么難?

在深入技術方案之前,我們先搞清楚為什么千萬級大表的數據刪除會如此困難。

有些小伙伴可能會想:"不就是個DELETE語句嗎,有什么難的?"

其實這里面大有學問。

數據庫刪除操作的底層原理

為了更直觀地理解數據庫刪除操作的工作原理,我畫了一個刪除操作的底層流程圖:

圖片圖片

從這張圖可以看出,一個簡單的DELETE語句背后隱藏著這么多復雜的操作。

讓我們詳細分析每個環節的挑戰:

1. 事務和鎖的挑戰

-- 一個看似簡單的刪除操作
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:基于主鍵分批

-- 存儲過程實現分批刪除
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:基于時間分批

// 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分批刪除

-- 簡單的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條/批次
  • 需要根據實際情況調整
  1. 休眠時間控制
  • 業務高峰期:休眠1-2秒
  • 業務低峰期:休眠100-500毫秒
  • 夜間維護:可不休眠或短暫休眠
  1. 監控和調整
  • 監控數據庫負載
  • 觀察主從同步延遲
  • 根據實際情況動態調整參數

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

當需要刪除表中大部分數據時,創建新表然后重命名的方式往往更高效。

實現原理

圖片圖片

具體實現

-- 步驟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代碼輔助實現

@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:使用現有分區表

-- 查看表的分區情況
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:改造普通表為分區表

-- 將普通表改造成分區表
-- 步驟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代碼實現分區管理

@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. 查詢優化:分區裁剪提升查詢性能

五、方案四:使用臨時表同步

對于需要在線刪除且不能停止服務的場景,可以使用臨時表同步的方式。

實現原理

圖片圖片

具體實現

@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)

# 安裝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. 自定義工具類

@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. 經驗要積累:每次操作后都要總結經驗教訓

記住:沒有最好的方案,只有最適合的方案

責任編輯:武曉燕 來源: 蘇三說技術
相關推薦

2025-07-28 02:00:00

2025-08-04 04:15:00

2025-03-31 01:55:00

2019-11-19 07:56:30

MySQL壓測數據表

2010-09-30 09:42:22

DB2刪除數據

2010-09-10 13:37:30

表分區SQL Server

2014-04-14 13:05:41

RedisDBA服務器

2011-02-21 10:35:00

查詢刪除數據

2011-05-18 15:08:03

mysql刪除修改數據

2022-01-02 08:38:22

Redis數據單線程

2023-09-05 08:40:57

刪除數據庫Oracle

2018-07-26 14:50:00

數據庫MySQL大表優化

2012-11-14 11:18:29

Mysql

2009-09-04 17:56:22

C#刪除數據

2019-12-20 14:56:50

批量刪除數據數據刪除

2009-11-26 16:49:27

PHP刪除數據庫表

2020-02-11 08:02:26

千萬級大表優化

2019-09-22 21:34:59

數據庫平滑變更表結構

2009-11-25 16:29:08

PHP刪除數組元素

2010-09-02 10:15:46

SQL刪除
點贊
收藏

51CTO技術棧公眾號

亚洲免费一级片| 国产无遮挡又黄又爽在线观看| 久久影视精品| 亚洲一区二区中文在线| 老司机精品福利在线观看| 奴色虐av一区二区三区| 欧美一区二区三区久久精品茉莉花| 日韩欧美一区中文| 日韩精品一区二区三区色欲av| av电影在线网| 成人av电影在线| 国产精品视频久久| 国产午夜福利片| 日韩精品诱惑一区?区三区| 精品少妇一区二区三区在线视频| 苍井空浴缸大战猛男120分钟| 国产调教视频在线观看| 久久一区二区三区国产精品| 91青草视频久久| 欧产日产国产69| 国内精品久久久久久久97牛牛| 亚洲天堂男人天堂| 国产十八熟妇av成人一区| 婷婷久久免费视频| 色综合久久久网| 男女激情免费视频| 国产网友自拍视频导航网站在线观看| 久久久久久久久蜜桃| 成人情视频高清免费观看电影| 亚洲男人天堂网址| 香蕉av777xxx色综合一区| 欧美日韩成人网| 91ts人妖另类精品系列| 九九视频免费观看视频精品 | 欧美国产一级片| 亚洲精选在线| 久久久久久久爱| 校园春色 亚洲| 国产精品久久久久蜜臀| 在线看国产精品| 性少妇bbw张开| 五月国产精品| 亚洲国内精品视频| 亚洲午夜久久久久久久久| 欧美成年网站| 91精品国产色综合久久不卡蜜臀 | 亚洲 欧美 日韩 在线| 国语自产精品视频在线看8查询8| 久久精品国产2020观看福利| 天天摸日日摸狠狠添| 精品国产视频| 国产亚洲a∨片在线观看| 91精彩刺激对白露脸偷拍| 欧洲亚洲成人| 精品亚洲aⅴ在线观看| 亚洲观看黄色网| 久久久久久毛片免费看| 亚洲国产精品女人久久久| 人妻换人妻a片爽麻豆| 精品一区二区三区亚洲| 91麻豆精品国产91久久久久久| 亚洲一级免费在线观看| 欧美日韩卡一| 欧美另类高清zo欧美| 99re精彩视频| 精品国产18久久久久久二百| 日韩你懂的电影在线观看| 香蕉视频在线观看黄| 91精品啪在线观看国产手机| 精品处破学生在线二十三| xxxxxx黄色| 亚洲黄页在线观看| 伊人久久久久久久久久久久久 | 亚洲综合免费观看高清完整版| 日韩一级免费看| 1234区中文字幕在线观看| 欧美日韩国产中文精品字幕自在自线 | 日本一卡二卡四卡精品| 国产日韩欧美精品综合| 伊人av成人| 日韩精品亚洲人成在线观看| 精品动漫一区二区| 日韩 欧美 高清| 午夜不卡一区| 欧美精品一区二区三区很污很色的 | 99视频精品全部免费在线| 蜜桃视频在线观看91| 91亚洲精选| 亚洲韩国精品一区| av免费网站观看| 精品午夜视频| 亚洲精品自拍第一页| 国产成人精品视频免费| 亚洲一级网站| 国产免费观看久久黄| 免费的黄色av| 中文字幕亚洲欧美在线不卡| 成人免费播放器| 黑人一区二区三区| 精品美女一区二区三区| 亚洲精品国产熟女久久久| 中文精品电影| 国产精品高清网站| 亚洲av永久无码国产精品久久 | 亚洲自拍的二区三区| 美女尤物在线视频| 欧美在线一区二区| 老司机免费视频| 亚洲九九在线| 国产成人亚洲综合91精品| 亚洲AV无码精品自拍| 国产欧美日韩久久| 国产3p露脸普通话对白| 91久久青草| 亚洲人a成www在线影院| 久久久综合久久久| 久久电影网站中文字幕| 久久久免费看| 狂野欧美性猛交xxxxx视频| 精品视频一区二区三区免费| 亚洲中文字幕一区| 国产真实久久| 亚洲永久在线观看| 日韩成人影视| 欧美亚洲免费在线一区| 一区二区三区少妇| 在线观看视频免费一区二区三区| 91精品视频大全| av在线播放av| 欧美私人免费视频| jizz中文字幕| 久久精品免费| 蜜桃传媒视频第一区入口在线看| 男女在线观看视频| 日韩一区二区三区视频在线观看| 熟女av一区二区| 日本视频一区二区| 欧美一区二区视频17c| 综合日韩av| 亚洲精品美女网站| 日本道在线观看| 91一区一区三区| 欧美日韩性生活片| 久久亚洲道色| 国产91成人video| 天堂а在线中文在线无限看推荐| 亚洲va欧美va天堂v国产综合| 国产探花一区二区三区| 午夜精彩国产免费不卡不顿大片| 91久久国产精品| 永久免费网站在线| 日韩欧美色综合| 久久久久久久久久久97| 成人在线视频一区二区| 黄色成人在线看| 日韩精品社区| 日韩美女在线播放| av在线电影免费观看| 欧美日韩aaaaa| 欧美另类videoxo高潮| 国产精品自拍一区| 国产在线视频在线| 国偷自产视频一区二区久| 91精品国产高清久久久久久91 | 日韩在线免费视频观看| 亚洲自拍第二页| 一区二区三区在线免费视频| 中文字幕天堂网| 国产麻豆综合| 亚洲春色在线视频| 日韩精品成人在线观看| 久久久免费高清电视剧观看| 日韩美女一级视频| 欧美影视一区二区三区| 国产福利视频网站| 成人av电影在线| 一区二区三区国产免费| 中文字幕免费精品| 国产日韩一区欧美| 日本一区二区三区视频在线| 理论片在线不卡免费观看| 免费激情视频网站| 在线看不卡av| 久草中文在线视频| 久久久久国产精品麻豆ai换脸| 日本中文字幕观看| 亚洲茄子视频| 亚洲日本精品| 黄色欧美在线| 国产精品自产拍在线观| heyzo在线欧美播放| 一道本无吗dⅴd在线播放一区 | 国产91国语对白在线| 中文字幕在线观看不卡视频| 国产国语老龄妇女a片| 男人的天堂亚洲| 99久久久无码国产精品性色戒| 粉嫩的18在线观看极品精品| 国产精品久久97| 国产精品探花在线| 视频直播国产精品| 亚洲成人中文字幕在线| 欧美亚洲国产bt| 精品午夜福利在线观看| 国产精品久久久久久久第一福利| 人妻无码中文久久久久专区| 狠狠色丁香婷婷综合久久片| 精品99在线视频| 欧美午夜电影在线观看 | www.色天使| 国产风韵犹存在线视精品| 亚洲精品怡红院| 日韩天天综合| 欧美一区二区视频在线播放| 日韩极品一区| 欧美国产一二三区| 粉嫩一区二区三区四区公司1| 成人亚洲激情网| 99re久久| 国产成人高潮免费观看精品| aa级大片免费在线观看| 久久99青青精品免费观看| 97在线观看免费观看高清 | 日韩欧美一区二区视频| 伊人亚洲综合网| 91高清在线观看| 区一区二在线观看| 亚洲成人免费观看| 久久久久亚洲av无码专区| 亚洲欧洲精品天堂一级| 日本一级免费视频| 久久影院午夜片一区| 中文视频在线观看| 成人永久免费视频| 日本黄色一级网站| 国产精品18久久久久久久网站| 中文字幕66页| 狠狠色狠狠色合久久伊人| 亚洲免费999| 美女在线视频一区| 亚洲色图 在线视频| 日韩精彩视频在线观看| 成人黄色片视频| 亚洲一区二区网站| 69sex久久精品国产麻豆| 欧美午夜久久| 2018中文字幕第一页| 国产精品观看| 蜜臀精品一区二区| 激情综合亚洲| 国产3p露脸普通话对白| 亚洲一区欧美二区| 激情六月丁香婷婷| 日韩高清电影一区| 最新天堂中文在线| 久久99精品视频| 久久久久久综合网| 国产电影一区二区三区| 又色又爽又黄18网站| 成人精品免费视频| 最新在线黄色网址| 国产日产欧美一区二区三区 | 日本成人小视频| 中文字幕日韩精品一区二区| 亚欧美无遮挡hd高清在线视频| 国产又黄又爽免费视频| 极品裸体白嫩激情啪啪国产精品| 国产特级淫片高清视频| 久久精品一区二区国产| 久久婷五月综合| 国产盗摄女厕一区二区三区| 国产69视频在线观看| 国产亚洲一本大道中文在线| 99久久久无码国产精品不卡| 一区二区三区在线影院| 日韩精品1区2区| 欧美日韩一区高清| 成人1区2区3区| 精品无人区太爽高潮在线播放| 午夜激情在线观看| 久久久免费观看| 亚洲第一会所| 97神马电影| 嫩草一区二区三区| 超碰成人在线免费观看| av成人黄色| 中文字幕 日韩 欧美| 成人精品免费视频| 亚洲欧美精品久久| 精品久久久久久久久久久| 在线观看视频中文字幕| 精品久久一区二区| a√资源在线| 91国自产精品中文字幕亚洲| 欧洲亚洲精品| 久久精品日产第一区二区三区| 婷婷激情图片久久| 国模吧无码一区二区三区| 国产制服丝袜一区| 9.1成人看片免费版| 亚洲欧美欧美一区二区三区| 国产精品视频一区在线观看| 日韩一本二本av| 粉嫩av一区| 91精品国产色综合久久不卡98| 久久麻豆视频| 欧美性色黄大片人与善| 亚洲性人人天天夜夜摸| 五月天婷婷亚洲| 久久久精品2019中文字幕之3| 精品99在线观看| 欧美老年两性高潮| 岛国视频免费在线观看| 久久久久久久网站| 99视频这里有精品| 天堂一区二区三区| 欧美一级二区| 日韩成人av一区二区| 亚洲理论在线观看| 亚洲字幕av一区二区三区四区| 精品香蕉在线观看视频一| 538在线观看| 动漫美女被爆操久久久| 一区二区不卡| 中文字幕在线观看日| 国产日产欧美一区| 久久精品国产成人av| 精品国产乱码久久久久久闺蜜 | 精品人妻二区中文字幕| 最新热久久免费视频| 日批视频免费观看| 亚洲人成在线电影| 中文日产幕无线码一区二区| 精品亚洲一区二区三区四区五区高| 午夜精品视频| 波多野结衣中文字幕在线播放| 国产精品福利一区二区三区| 中文字幕在线观看免费| 中文字幕日韩欧美| 91p九色成人| 一区二区三区四区五区精品| 日韩精品欧美成人高清一区二区| 91国模少妇一区二区三区| 欧美性少妇18aaaa视频| 偷拍自拍在线视频| 欧美资源在线观看| 精品中文一区| 日韩av手机版| 中文字幕日韩一区| 国产手机视频在线| 欧美老女人性视频| 一本色道69色精品综合久久| 国产二区视频在线| 91网站在线观看视频| 91在线视频在线观看| 亚洲欧美中文日韩在线| 日韩经典一区| 四虎永久免费网站| 国产91在线|亚洲| 国产尤物在线视频| 国产亚洲欧美日韩一区二区| 人人精品久久| 国产乱子伦精品无码专区| 不卡的av中国片| 欧美超碰在线观看| 日韩在线高清视频| 91精品导航| 狠狠热免费视频| 亚洲色图视频免费播放| 国产18精品乱码免费看| 欧美在线一区二区视频| heyzo久久| 丰满人妻一区二区三区53视频| 亚洲高清一区二区三区| 日韩欧美亚洲系列| 成人网址在线观看| 99视频+国产日韩欧美| 婷婷色一区二区三区| 91精品国模一区二区三区| 成年人视频免费在线播放| 日本最新一区二区三区视频观看| 黑人精品欧美一区二区蜜桃| 在线看成人av| 视频直播国产精品| 老牛影视av一区二区在线观看| 亚洲精品一二三四五区| 一区二区三区中文字幕| 国产午夜在线视频| aa日韩免费精品视频一| 日韩专区中文字幕一区二区| 男人操女人的视频网站| 日韩精品在线私人| 亚洲二区av| 日本久久久精品视频| 亚洲精品成人少妇| av在线电影播放| 美女视频久久| 国产精品性做久久久久久| 人人妻人人爽人人澡人人精品| 久久久久久久久国产精品|