EXISTS替代LEFT JOIN+IS NULL更高效:NOT EXISTS仅需索引查找并短路退出,避免全表扫描与笛卡尔积,内存占用低;多列去重须建对应联合索引,否则性能退化。

用 EXISTS 替代 LEFT JOIN + IS NULL 做去重判断
直接用 LEFT JOIN ... WHERE ... IS NULL 在百万级表上做“A中存在但B中不存在”的去重,往往触发全表扫描和临时表膨胀,执行时间可能从秒级跳到分钟级。本质是优化器容易为 IS NULL 生成低效执行计划,尤其当B表没在关联字段建索引时。
更稳更快的做法是改用 EXISTS 子查询:
SELECT a.* FROM table_a a WHERE NOT EXISTS ( SELECT 1 FROM table_b b WHERE b.id = a.id );
-
EXISTS只需找到一条匹配即短路退出,对B表能充分利用id上的索引(必须有) - 避免了
JOIN产生的中间笛卡尔积行,内存占用更低 - 若去重依据是多列(如
(user_id, event_time)),确保B表上有对应联合索引,否则EXISTS也退化
用 INSERT ... SELECT ... ON DUPLICATE KEY UPDATE 实现增量去重写入
如果目标不是查出差异,而是把A表数据“去重后写入C表”,别先 SELECT 再 INSERT —— 网络传输+客户端内存+重复解析开销巨大。MySQL下优先走服务端原生去重逻辑。
前提是C表有唯一约束(如 UNIQUE KEY (id)):
INSERT INTO table_c SELECT * FROM table_a ON DUPLICATE KEY UPDATE id = id;
-
ON DUPLICATE KEY UPDATE id = id是空更新,仅触发去重逻辑,比IGNORE更可控(IGNORE会静默吞掉所有冲突错误) - 该语句在开启
autocommit=1时仍能保证原子性:每行冲突都单独处理,不因某行失败中断整体 - 注意:如果C表有自增主键且未在
SELECT中显式指定值,重复行不会改变原记录的自增值,但新插入行会正常递增
用 ROW_NUMBER() 窗口函数在PostgreSQL/SQL Server中按业务规则去重
当“去重”不是简单取首次出现,而是要保留最新时间、最高分数等业务优先级记录时,GROUP BY + 聚合函数容易丢失非分组字段,而 DISTINCT ON(PostgreSQL)或 ROW_NUMBER() 更精准。
例如:每个 user_id 只留 created_at 最大的那条
SELECT user_id, action, created_at
FROM (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY created_at DESC
) AS rn
FROM table_a
) ranked
WHERE rn = 1;
- 必须给
PARTITION BY和ORDER BY字段建复合索引,比如(user_id, created_at DESC),否则排序开销爆炸 - PostgreSQL 可用
DISTINCT ON (user_id) ORDER BY user_id, created_at DESC语义等价,但可读性略高;SQL Server 必须用ROW_NUMBER() - 这个方案不适合超大结果集直接返回——窗口函数需缓存全部分区数据,内存压力大;建议配合
LIMIT或先过滤再计算
JOIN前务必确认统计信息是否准确 & 索引是否覆盖
再好的写法,遇到过期的表统计信息或缺失索引,也会被优化器拖垮。百万级表上,以下检查不能跳过:
- 执行
ANALYZE TABLE table_a, table_b;(MySQL)或VACUUM ANALYZE table_a;(PostgreSQL),让优化器知道真实行数和数据分布 - 用
EXPLAIN看执行计划:重点确认type是ref/range而非ALL,key列显示实际使用的索引名 - 如果JOIN条件含函数(如
UPPER(a.name) = UPPER(b.name)),普通索引失效,得建函数索引:CREATE INDEX idx_name_upper ON table_a (UPPER(name));
真正卡住性能的,往往不是语法本身,而是索引没建对、统计信息没更新、或者误以为字段有索引其实没有——这些地方一错,再精巧的SQL也救不回来。










