延續基礎篇,本文聚焦 REPLACE() 在**複雜數據清洗管線**中的組合應用:多字符並行替換、編碼統一 (全角/半角、中文標點)、日期格式標準化管線、大表分批無鎖執行、應用層流式處理架構。通過 es_product.pro_pub_time 真實場景演示從「臟數據」到「標準化日期」的完整 ETL 過程。
UPDATE `t` SET `c` = REPLACE(`c`, 'a', 'x') WHERE INSTR(`c`, 'a')>0;
UPDATE `t` SET `c` = REPLACE(`c`, 'b', 'y') WHERE INSTR(`c`, 'b')>0;
UPDATE `t` SET `c` = REPLACE(`c`, 'c', 'z') WHERE INSTR(`c`, 'c`)>0;
缺點:多次全表掃描、多次鎖衝突、Binlog 放大。
UPDATE `t`
SET `c` = REPLACE(REPLACE(REPLACE(`c`, 'a', 'x'), 'b', 'y'), 'c', 'z')
WHERE INSTR(`c`, 'a')>0 OR INSTR(`c`, 'b')>0 OR INSTR(`c`, 'c')>0;
優勢:單次全表掃描、單次鎖、單次 Binlog 寫入、原子性保證所有替換同時生效或全不生效。
注意:替換順序影響結果 (如先替 'a'→'b' 再替 'b'→'c' 會連鎖),**按業務邏輯精心排序**。
UPDATE `t`
SET `c` = CASE
WHEN `c` LIKE '%pattern1%' THEN REPLACE(`c`, 'pattern1', 'repl1')
WHEN `c` LIKE '%pattern2%' THEN REPLACE(`c`, 'pattern2', 'repl2')
ELSE `c`
END
WHERE `c` LIKE '%pattern1%' OR `c` LIKE '%pattern2%';
-- 統一全角字母數字轉半角 (需自定義函數或應用層處理,MySQL 無內置)
-- 示例:手動列舉常見全角字符
UPDATE `t` SET `c` = REPLACE(`c`, 'a', 'a') WHERE INSTR(`c`, 'a')>0;
-- ... 批量處理 a-z A-Z 0-9
-- 統一中文標點為英文標點 (視業務需求)
UPDATE `t` SET `c` = REPLACE(`c`, ',', ',') WHERE INSTR(`c`, ',')>0;
UPDATE `t` SET `c` = REPLACE(`c`, '。', '.') WHERE INSTR(`c`, '。')>0;
UPDATE `t` SET `c` = REPLACE(`c`, '?', '?') WHERE INSTR(`c`, '?')>0;
UPDATE `t` SET `c` = REPLACE(`c`, '!', '!') WHERE INSTR(`c`, '!')>0;
UPDATE `t` SET `c` = REPLACE(`c`, ':', ':') WHERE INSTR(`c`, ':')>0;
UPDATE `t` SET `c` = REPLACE(`c`, ';', ';') WHERE INSTR(`c`, ';')>0;
UPDATE `t` SET `c` = REPLACE(`c`, '(', '(') WHERE INSTR(`c`, '(')>0;
UPDATE `t` SET `c` = REPLACE(`c`, ')', ')') WHERE INSTR(`c`, ')')>0;
UPDATE `t` SET `c` = REPLACE(`c`, '【', '[') WHERE INSTR(`c`, '【')>0;
UPDATE `t` SET `c` = REPLACE(`c`, '】', ']') WHERE INSTR(`c`, '】')>0;
UPDATE `t` SET `c` = REPLACE(`c`, '「', '"') WHERE INSTR(`c`, '「')>0;
UPDATE `t` SET `c` = REPLACE(`c`, '」', '"') WHERE INSTR(`c`, '」')>0;
工程化建議:將映射表存儲在配置表/代碼常量中,動態生成 SQL 或應用層統一處理,避免硬編碼分散維護。
場景:es_product.pro_pub_time 字段為 VARCHAR,存儲極不規範日期字符串:'2005年04月'、'2005-04-'、'2005-'、'2005'、'2005.04.01'、'2005/04/01'、'05-04-01' 等。
UPDATE `es_product` SET `pro_pub_time` = TRIM(`pro_pub_time`);
UPDATE `es_product` SET `pro_pub_time` = REPLACE(`pro_pub_time`, '[', '') WHERE INSTR(`pro_pub_time`, '[')>0;
UPDATE `es_product` SET `pro_pub_time` = REPLACE(`pro_pub_time`, ']', '') WHERE INSTR(`pro_pub_time`, ']')>0;
UPDATE `es_product` SET `pro_pub_time` = REPLACE(`pro_pub_time`, '.', '-') WHERE INSTR(`pro_pub_time`, '.')>0;
UPDATE `es_product` SET `pro_pub_time` = REPLACE(`pro_pub_time`, '/', '-') WHERE INSTR(`pro_pub_time`, '/')>0;
UPDATE `es_product` SET `pro_pub_time` = REPLACE(`pro_pub_time`, '-', '-') WHERE INSTR(`pro_pub_time`, '-')>0;
UPDATE `es_product` SET `pro_pub_time` = REPLACE(`pro_pub_time`, '年', '-') WHERE INSTR(`pro_pub_time`, '年')>0;
UPDATE `es_product` SET `pro_pub_time` = REPLACE(`pro_pub_time`, '月', '-') WHERE INSTR(`pro_pub_time`, '月')>0;
UPDATE `es_product` SET `pro_pub_time` = REPLACE(`pro_pub_time`, '日', '') WHERE INSTR(`pro_pub_time`, '日')>0;
-- '2005-04-' (長度>5, 以 - 結尾) → '2005-04-01'
UPDATE `es_product`
SET `pro_pub_time` = CONCAT(`pro_pub_time`, '01')
WHERE SUBSTRING_INDEX(`pro_pub_time`, '-', -1) = ''
AND LENGTH(`pro_pub_time`) > 5;
-- '2005-' (長度=5, 含 -) → '2005-01-01'
UPDATE `es_product`
SET `pro_pub_time` = CONCAT(`pro_pub_time`, '01-01')
WHERE INSTR(`pro_pub_time`, '-') > 0
AND LENGTH(`pro_pub_time`) = 5;
-- 含 '-' 但長度 < 8 (如 '2005-04') → 追加 '-01'
UPDATE `es_product`
SET `pro_pub_time` = CONCAT(`pro_pub_time`, '-01')
WHERE INSTR(`pro_pub_time`, '-') > 0
AND LENGTH(`pro_pub_time`) < 8;
-- 純年份 '2005' (長度=4, 無 -) → '2005-01-01'
UPDATE `es_product`
SET `pro_pub_time` = CONCAT(`pro_pub_time`, '-01-01')
WHERE INSTR(`pro_pub_time`, '-') = 0
AND LENGTH(`pro_pub_time`) = 4;
-- 選項 A: 更新為標準字符串 'YYYY-MM-DD'
UPDATE `es_product`
SET `pro_pub_time` = DATE_FORMAT(STR_TO_DATE(`pro_pub_time`, '%Y-%m-%d'), '%Y-%m-%d')
WHERE INSTR(`pro_pub_time`, '-') > 0
AND STR_TO_DATE(`pro_pub_time`, '%Y-%m-%d') IS NOT NULL;
-- 選項 B (推薦): 新增 DATE 列, 回填, 切換, 刪舊列
ALTER TABLE `es_product` ADD `pub_date` DATE;
UPDATE `es_product`
SET `pub_date` = STR_TO_DATE(`pro_pub_time`, '%Y-%m-%d')
WHERE STR_TO_DATE(`pro_pub_time`, '%Y-%m-%d') IS NOT NULL;
-- 驗證無誤後
ALTER TABLE `es_product` DROP `pro_pub_time`, CHANGE `pub_date` `pro_pub_time` DATE;
<?php
/**
* 大表字符串替換/清洗無鎖執行器
* 策略: 主鍵範圍分批 + 只更新需變更行 + 進度持久化 + 監控指標
*/
class LargeTableCleaner {
private PDO $pdo;
private string $table;
private string $pk;
private string $col;
private array $replacements; // [['from'=>'年', 'to'=>'-'], ...]
private int $batchSize = 2000;
private int $sleepUs = 100000;
private string $progressTable = '_cleaner_progress';
public function __construct(PDO $pdo, string $table, string $pk, string $col, array $replacements) {
$this->pdo = $pdo;
$this->table = $table;
$this->pk = $pk;
$this->col = $col;
$this->replacements = $replacements;
$this->initProgressTable();
}
private function initProgressTable() {
$this->pdo->exec("CREATE TABLE IF NOT EXISTS `$this->progressTable` (
`task_name` VARCHAR(100) PRIMARY KEY,
`last_pk` BIGINT UNSIGNED NOT NULL DEFAULT 0,
`total_affected` BIGINT UNSIGNED NOT NULL DEFAULT 0,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB");
}
public function run(string $taskName): array {
$lastPk = $this->getLastPk($taskName);
$totalAffected = 0;
$batch = 0;
while (true) {
$sql = $this->buildBatchSql($lastPk);
$stmt = $this->pdo->prepare($sql);
$params = array_merge(
array_values(array_column($this->replacements, 'from')),
array_values(array_column($this->replacements, 'to')),
array_values(array_column($this->replacements, 'from')), // FOR WHERE INSTR
[$lastPk, $this->batchSize]
);
$stmt->execute($params);
$affected = $stmt->rowCount();
if ($affected === 0) break;
$totalAffected += $affected;
$batch++;
$lastPk = $this->getMaxPkInBatch($lastPk);
$this->saveProgress($taskName, $lastPk, $totalAffected);
// 監控指標上報 (示例: 發送到 StatsD/Prometheus Pushgateway)
$this->reportMetrics($taskName, $batch, $affected, $totalAffected);
usleep($this->sleepUs);
}
return ['batches' => $batch, 'total_affected' => $totalAffected];
}
private function buildBatchSql(int $lastPk): string {
$col = "`{$this->col}`";
$pk = "`{$this->pk}`";
$table = "`{$this->table}`";
// 構建嵌套 REPLACE: REPLACE(REPLACE(col, ? , ?), ?, ?) ...
$nested = $col;
foreach ($this->replacements as $r) {
$nested = "REPLACE($nested, ?, ?)";
}
// WHERE 條件: 任一目標字符串存在 AND pk > lastPk
$whereConds = [];
foreach ($this->replacements as $r) {
$whereConds[] = "INSTR($col, ?) > 0";
}
$where = implode(' OR ', $whereConds) . " AND $pk > ?";
return "UPDATE $table SET $col = $nested WHERE $where ORDER BY $pk LIMIT ?";
}
private function getLastPk(string $taskName): int {
$stmt = $this->pdo->prepare("SELECT `last_pk` FROM `$this->progressTable` WHERE `task_name` = ?");
$stmt->execute([$taskName]);
return (int)($stmt->fetchColumn() ?? 0);
}
private function getMaxPkInBatch(int $lastPk): int {
$where = implode(' OR ', array_map(fn($r) => "INSTR(`{$this->col}`, ?) > 0", $this->replacements));
$params = array_values(array_column($this->replacements, 'from'));
$params[] = $lastPk;
$params[] = $this->batchSize;
$sql = "SELECT MAX(`{$this->pk}`) FROM `{$this->table}` WHERE $where AND `{$this->pk}` > ? LIMIT ?";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return (int)($stmt->fetchColumn() ?? $lastPk);
}
private function saveProgress(string $taskName, int $lastPk, int $total) {
$this->pdo->prepare("REPLACE INTO `$this->progressTable` (`task_name`, `last_pk`, `total_affected`) VALUES (?, ?, ?)")
->execute([$taskName, $lastPk, $total]);
}
private function reportMetrics(string $task, int $batch, int $affected, int $total) {
// 實現發送到監控系統
// file_put_contents("/tmp/$task.metrics", json_encode([...]), FILE_APPEND);
}
}
// 使用示例
$pdo = new PDO("mysql:host=localhost;dbname=test;charset=utf8mb4", "user", "pass");
$cleaner = new LargeTableCleaner($pdo, 'es_product', 'id', 'pro_pub_time', [
['from' => '年', 'to' => '-'],
['from' => '月', 'to' => '-'],
['from' => '日', 'to' => ''],
['from' => '-', 'to' => '-'],
['from' => '.', 'to' => '-'],
['from' => '/', 'to' => '-'],
]);
$result = $cleaner->run('es_product_pub_time_standardize');
print_r($result);
?>
┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌─────────────┐
│ Binlog/CDC │────▶│ Stream Proc │────▶│ Transform │────▶│ Batch Upsert│
│ (Canal/Debez) │ (Flink/KSQL/ │ │ (Regex, │ │ (PK IN ...) │
│ │ │ Custom App) │ │ Lookup, │ │ │
└─────────────┘ └──────────────┘ │ State) │ └─────────────┘
└─────────────┘
優勢:零數據庫負擔、毫秒級延遲、狀態化處理 (會話窗口、去重)、精確一次語義、水平擴展、邏輯復用 (同邏輯服務多下游)。
MySQL REPLACE() 批量替換從「單條 SQL」進化到「分批無鎖框架」再到「流式架構」,折射出數據量級增長對工程模式的倒逼。核心心法:**數據庫只負責存儲與索引,計算邏輯上移應用層/流式層**。對於日期標準化這類確定性轉換,構建「配置驅動的清洗管線」+「冪等分批執行器」+「全鏈路可觀測」,才是應對數據治理長期戰役的正確姿勢。