ROW_NUMBER()不能直接对无主键表去重,因其ORDER BY缺乏稳定排序依据,相同数据多次执行会分配不同行号,导致去重结果不一致;须借助md5哈希或ctid等确定性锚点实现稳定排序与安全去重。

为什么不能直接用 ROW_NUMBER() 对无主键表去重
因为没主键,ROW_NUMBER() 的 ORDER BY 子句无法稳定排序,相同数据多次执行可能分配不同行号,导致去重结果不一致。这不是语法错误,而是逻辑风险——你删掉的那行,下次跑可能就变成另一行。
实际场景里,常见于日志表、爬虫临时表、ETL中间表,字段全为 TEXT 或 VARCHAR,且存在完全重复的整行记录。
- 别指望靠
ORDER BY (SELECT NULL)或ORDER BY 1来“随便排”,SQL 标准不保证稳定性,PostgreSQL 和 SQL Server 会报错,MySQL 8.0+ 虽允许但行为不可靠 - 用
GROUP BY+MIN(id)?不行——表根本没id - 加自增列?DDL 操作在生产环境常被禁用,且无法解决“当前已存在的重复”问题
用子查询模拟稳定行号:基于字段组合的伪主键
核心思路是:把所有字段拼成一个确定性哈希值(或直接拼接),再用该值作为 ORDER BY 锚点。这样即使原始行无序,只要字段内容相同,生成的排序键就一定相同。
以 PostgreSQL 为例(MySQL/SQL Server 类似,仅函数名微调):
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY col1, col2, col3
ORDER BY md5(col1::text || col2::text || col3::text)
) AS rn
FROM raw_table
) t WHERE rn = 1;说明:
-
PARTITION BY按业务上“算重复”的字段分组,不是全字段 —— 否则每行都是独立分区,起不到去重作用 -
ORDER BY md5(...)确保相同内容必然生成相同排序值,避免随机性;不用random()或无序字段 - 若字段含
NULL,||拼接会变NULL,改用COALESCE(col1, '')显式转空字符串 - SQLite 不支持
md5(),可用quote()或json_object()(3.38+)替代
更安全的写法:子查询先去重再关联原表
上面方法在大数据量时可能因 md5 计算拖慢性能,且部分数据库对表达式索引支持弱。换一种更可控的方式:用子查询先提取每组“代表行”的唯一标识(如最小物理偏移),再关联原表。
PostgreSQL 中可用 ctid(物理行号)做稳定锚点:
SELECT DISTINCT ON (col1, col2, col3) * FROM raw_table ORDER BY col1, col2, col3, ctid;
等价于子查询写法(兼容性更好):
SELECT t1.* FROM raw_table t1
WHERE t1.ctid = (
SELECT MIN(t2.ctid)
FROM raw_table t2
WHERE t2.col1 = t1.col1
AND t2.col2 = t1.col2
AND t2.col3 = t1.col3
);注意点:
-
ctid是 PostgreSQL 特有,SQL Server 用%%physloc%%,MySQL 无直接等价物(需依赖INFORMATION_SCHEMA.INNODB_BUFFER_PAGE,不推荐) -
MIN(ctid)比MAX(ctid)更合理:通常老数据在前,保留最早插入的那条更符合业务预期 - 务必给
(col1, col2, col3)加联合索引,否则子查询会全表扫描,性能崩盘
真正要小心的边界:BLOB、JSON、数组字段怎么处理
如果去重字段包含 BYTEA、JSONB 或 TEXT 超长字段,md5(col::text) 可能失败(如 JSONB 的 key 顺序不保证)或极慢。
对策分情况:
- JSON 字段:用
jsonb_normalize()(PostgreSQL 扩展)或先jsonb_pretty()再哈希,避免因格式空格/换行导致哈希不等 - BLOB / BYTEA:用
encode(data, 'hex')转字符串再哈希,别直接 cast to text - 超长 TEXT:截断到前 1000 字符哈希(
left(col, 1000)),前提是业务允许“前缀相同即视为重复” - 千万避开
TO_CHAR(NOW(), 'YYYYMMDDHH24MISS')这类时间函数——它会让每行哈希都不同,彻底失效
没有银弹。字段越复杂,越得先验证哈希碰撞率;线上执行前,永远用 SELECT COUNT(*) 和 COUNT(DISTINCT ...) 对比确认去重效果是否符合预期。

















