他的回复:
你踩的这三个点(NO PAD、filesort、DDL 锁)是同一件事的三个面。分开答。 **一、怎么高效捞出「PAD SPACE 语义下重复」的数据** `GROUP BY account COLLATE utf8mb4_general_ci` 在 800 万行上必然全表扫 + 临时表,跑不动很正常。换个思路——**尾空格重复只可能是「trim 之后相等」,那就把 trim 之后的值算出来比**: ```sqlSELECT TRIM(account) AS t, COUNT(*) c, MIN(id) keep_idFROM user_accountGROUP BY t HAVING c > 1;``` 这条能走 `account` 上的索引吗?不一定(函数作用于列,索引失效)。所以实操要用**分批 + 覆盖索引**: ```sql-- 按主键区间分批,每批 20 万SELECT id, account FROM user_accountWHERE id BETWEEN ? AND ? AND account TRIM(account);``` `account TRIM(account)` 只筛出「带空格」的行——这一类通常极少(几十到几百条),先在应用层或小表里比对,成本立刻降下来。**别一上来就全表 GROUP BY**,先用这个条件把候选集缩到几百行,逻辑在脚本里做。 如果非要纯 SQL 找出重复,加一个虚拟列 + 索引是最省事的: ```sqlALTER TABLE user_account ADD COLUMN account_norm VARCHAR(64) GENERATED ALWAYS AS (TRIM(account)) VIRTUAL, ADD INDEX idx_norm (account_norm);``` 虚拟列在 8.0 上加索引是 INPLACE 的(不需要 copy 表),建完直接 `GROUP BY account_norm HAVING count(*)>1`,秒级。**这是我推荐的做法**:先加虚拟列+索引排查,查完再决定要不要留。 **二、要不要改回 general_ci** 我不建议为了这一个行为把整列改回 `general_ci`,理由: 1. **改 collation 是 copy 表 DDL**。8.0 支持 `ALGORITHM=INSTANT` 的场景是「加列/改默认值」这类,改字符集或排序规则**不在 INSTANT 范围内**,只能 INPLACE(重建表 + 重建索引),800 万行会锁写一段时间。RDS 上有无锁变更工具可以用,但要看你的版本支持。2. `general_ci` 是 UCA 之前的简化实现,排序结果在中文/多语言场景下本来就不准(按字节权重排,不是按拼音)。为了「尾空格相等」退回去,等于用一个更老的坑换一个新坑。3. **尾空格相等这件事,本质上不该由数据库来兜**。应用层 trim 是必须做的,数据库唯一索引只是最后一道防线。 我的建议:**保留 0900_ai_ci,做两件事**——① 清一次历史脏数据(用上面的虚拟列方案找);② 应用层入口统一 `trim()`,并在写库前做长度/字符校验。如果确实需要「PAD SPACE」语义,可以用虚拟列 `TRIM(account)` 建唯一索引,语义精确可控,也不用改整表的 collation: ```sqlALTER TABLE user_account ADD UNIQUE KEY uk_account_norm (account_norm); -- account_norm 是 TRIM 后的虚拟列``` 这个方案最合我意:不改 collation、不锁表、语义明确。 **三、ORDER BY 变成 filesort 怎么办** 迁移后 filesort 常见两个原因: 1. **collation 混用**。JOIN/UNION 两边列的 collation 不一致时会发生隐式转换,索引直接失效。查: ```sqlSELECT table_name, column_name, collation_nameFROM information_schema.columnsWHERE table_schema = DATABASE() AND collation_name IS NOT NULL AND collation_name 'utf8mb4_0900_ai_ci';``` 2. **索引的排序权重变了**。0900 用的是 UCA 9.0.0,同一批字符串的排序结果可能和 general_ci 不同,但**索引本身仍然可用**——filesort 一般不是权重变化导致的,而是上面第 1 条或统计信息问题。先跑 `ANALYZE TABLE` 再看 `EXPLAIN`。 **四、不停业务改唯一索引** 你的想法(先降级成普通索引 → 清数据 → 再升级回唯一索引)可行,顺序是: ```sqlALTER TABLE user_account DROP INDEX uk_account, ADD INDEX idx_account (account), ALGORITHM=INPLACE, LOCK=NONE;-- 清数据(业务低峰)ALTER TABLE user_account DROP INDEX idx_account, ADD UNIQUE INDEX uk_account (account), ALGORITHM=INPLACE, LOCK=NONE;``` 注意两点:① 加回唯一索引时如果还有重复会直接失败,先确认清干净;② 加唯一索引期间会有短暂的元数据锁,主库上建议在低峰做,且用 `lock_wait_timeout` 控制排队时间,别让它把后续 DDL 全堵住。 读流量在从库上不影响这个操作,但从库会重放 DDL,**从库延迟会短时上升**,监控一下 `Seconds_Behind_Master`。