LeetCode 196. 删除重复的电子邮箱
题目描述
题意分析
Person表有两列:主键id和邮箱id最小的那一行。注意这题要写的是DELETE而不是SELECT,判定看的是执行之后表里剩下什么,而不是查询返回什么。「保留 id 最小」这条规则把「该删哪些」变成了一个可以逐行独立判定的谓词:一行该被删,当且仅当存在另一行与它邮箱相同且 id 更小。这个表述很关键——它不需要先算出「每组的最小 id」再回头比对,只需要判断「有没有比我更小的同伴」。
id是主键,所以互不相同,「更小」这个比较不会出现平局,每组必然恰好留下一行,不会误删光也不会漏删。另一个必须留意的是 MySQL 的限制:不能在删除一张表的同时,在子查询里直接读取同一张表(报 1093 错误)。这条限制决定了写法只能走连接,或者套两层派生表把子查询的结果先物化。
边界包括:表中没有任何重复(一行都不删);某个邮箱出现三次及以上(要删掉除最小 id 外的全部);以及表只有一行。
解法:SQL 查询建模
核心思路
最直觉的写法是「先查出每个邮箱的最小 id,再删掉 id 不在这个集合里的行」。语义完全正确,但在 MySQL 上直接写
DELETE FROM Person WHERE id NOT IN (SELECT MIN(id) FROM Person GROUP BY email)会被拒绝——子查询读的正是要删的那张表。要救它得再套一层派生表把中间结果物化,写法变得臃肿。换个角度就干净了。前面已经把删除条件重述成「存在另一行与我邮箱相同且 id 更小」,而「存在另一行满足某关系」在 SQL 里的自然表达就是自连接:把同一张表以两个别名引入,让「我」和「那个更小的同伴」在同一行里相遇。
ON中的两个条件各有职责:p1.email = p2.email限定同一邮箱,p1.id > p2.id证明p1存在更小 id 的同伴。凡是能连接成功的p1都应删除。
DELETE p1 FROM ... JOIN ...这个语法的含义是「按连接结果删除p1所代表的那张表里的行」。别名p1出现在DELETE之后,明确指定了删哪一侧——这是多表删除语法的核心,写漏了数据库不知道该动谁。为什么不会误删最小 id 的那一行?因为对它而言不存在 id 更小的同伴,
p1.id > p2.id对所有候选p2都不成立,它压根不会出现在连接结果里。为什么同一行被匹配多次也没问题?一个邮箱出现三次时,最大 id 的那行会同时和另外两行配对,产生两条连接结果,但
DELETE对同一行的重复命中是幂等的——删一次就不在了,不会报错也不会重复计数。比较用严格大于而不是不等号:写成
p1.id != p2.id会让每一行都能找到同伴,整组被删光。
解题步骤
- 确认要删的是哪一侧,并把它的别名写在
DELETE之后。这里删的是「有更小同伴」的那一行,所以是DELETE p1。这一步定错,整条语句的方向就反了。- 用两个别名把
Person引入两次:p1代表「被审视的行」,p2代表「潜在的更小同伴」。同一张表出现两次必须起别名,否则列引用有歧义。- 连接条件同时限定同邮箱和 id 大小:
p1.email = p2.email AND p1.id > p2.id。p1只要能连接到一条p2,就证明它不是本邮箱的最小 id。- 不需要
GROUP BY、MIN()或任何子查询。整个判定是逐行的存在性检查,连接本身已经把它表达完整了。以一张具体的表走一遍:
Person中有(1, 'john@example.com')、(2, 'bob@example.com')、(3, 'john@example.com')。连接阶段按邮箱配对。
john@example.com这一组有 id 为 1 和 3 的两行,两两组合出四对:(p1=1, p2=1)、(p1=1, p2=3)、(p1=3, p2=1)、(p1=3, p2=3)。bob@example.com只有一行,只能自己和自己配成(p1=2, p2=2)。再应用 id 大小条件:
(1,1)、(1,3)、(3,3)、(2,2)都不成立,只有(3,1)满足 $3 > 1$,所以删除侧只命中 id 3。于是只有
p1.id = 3这一行被标记删除。执行后表里剩下(1, 'john@example.com')与(2, 'bob@example.com'),正是预期结果。再看三份重复的情形:邮箱相同的行 id 分别是 1、4、7。满足
p1.id > p2.id的组合有(4,1)、(7,1)、(7,4),被标记的p1是 4 和 7 两行(7 被命中两次,但只删一次)。id 为 1 的行找不到更小的同伴,得以保留。若把条件误写成
p1.id != p2.id,第一个例子里(1,3)与(3,1)都会成立,两行全被删光,正确答案是保留 id 为 1 的那行。
代码实现
-- DELETE 后的 p1 指定删哪一侧;自连接找出「存在更小同伴」的行。
DELETE p1
FROM Person AS p1
JOIN Person AS p2
ON p1.email = p2.email
AND p1.id > p2.id;
复杂度分析
- 时间复杂度:设每个邮箱组大小为 $g_i$,连接匹配数为 $O(\sum g_i^2)$,最坏所有邮箱相同则为 $O(n^2)$。
(email, id)索引能降低定位同组记录的成本,但不能消除同组内实际存在的匹配对。- 空间复杂度:由执行计划决定;流式连接只需连接状态,若物化匹配结果,最坏可达 $O(n^2)$。
关键点总结
- 「每组保留一个」类的删除题,最省事的建模是把它翻成逐行的存在性谓词:「存在另一行与我同组且更优」。这样就不需要先求组内极值再回连,一次自连接即可表达。
DELETE 别名 FROM 表 JOIN ...是 MySQL 多表删除的标准语法,DELETE后的别名指定了删哪一侧。这是本题唯一必须记住的方言细节,写不出来语句直接报错。- MySQL 的目标表限制会拒绝直接从
Person子查询读取后再删除Person;多表自连接删除在同一连接计划中确定目标行,避开了错误 1093。- 比较运算符的方向直接决定保留哪一份。
>保留最小、<保留最大、!=会把整组删光——这三种结果差别巨大,写完务必回读一遍语义。- 同一行被连接结果多次命中不会造成问题,
DELETE天然幂等。理解这一点就不必画蛇添足地加DISTINCT(多表删除语法里也不允许加)。- 面试表达要从删除谓词出发:「存在同邮箱且 id 更小的行就删除」,再说明
DELETE p1指定目标侧,以及为什么该写法不会触发 MySQL 1093 限制。
易错点总结
- 错误写法:条件写成
WHERE p1.id != p2.id。用例:(1, 'a@x.com')、(3, 'a@x.com')→ 两行互相匹配、双双被删,正确答案是保留 id 为 1 的那行。- 错误写法:条件写成
WHERE p1.id < p2.id。用例:(1, 'a@x.com')、(3, 'a@x.com')→ 删掉的是 id 为 1 的行,保留了 3,正确答案是保留最小的 1。- 错误写法:直接写
DELETE FROM Person WHERE id NOT IN (SELECT MIN(id) FROM Person GROUP BY email)。用例:任意数据 → MySQL 报错 1093「You can't specify target table 'Person' for update in FROM clause」,语句无法执行。- 错误写法:写成
SELECT查询返回保留下来的行。用例:任意数据 → 表本身没有被修改,判定按执行后的表内容比对,直接失败。- 错误写法:连接条件漏写,用逗号写成
DELETE p1 FROM Person p1, Person p2 WHERE p1.id > p2.id。用例:(1,'a@x.com')、(2,'b@x.com')、(3,'a@x.com')→ 邮箱不同的行也被配对,id 为 2 和 3 的行都被删掉,正确答案只该删 id 为 3 的行。- 错误写法:连接条件写成
ON p1.id = p2.id。用例:任意数据 → 每行只与自己配对,p1.id > p2.id永远不成立,一行都删不掉,重复行原样保留。- 错误写法:先用
GROUP BY email聚合后再删,把HAVING COUNT(*) > 1当成删除条件。用例:(1,'a@x.com')、(3,'a@x.com')→ 该条件筛出的是「有重复的邮箱组」,按它删除会把该组两行全部删光,正确答案是保留一行。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 182. 查找重复的电子邮箱 | 简单 | 只需查出重复的邮箱值,用 GROUP BY 加 HAVING 即可,不涉及删除语法 |
| 181. 超过经理收入的员工 | 简单 | 同为自连接,但连接的是外键指向的层级关系而非同组内的兄弟行 |
| 178. 分数排名 | 中等 | 用窗口函数为每行附加组内名次,是「每组保留第一」的另一条实现路径 |
| 184. 部门工资最高的员工 | 中等 | 分组内取最大值并保留明细,考察聚合结果如何回连到原表 |
| 185. 部门工资前三高的所有员工 | 困难 | 每组保留前 N 名,用 PARTITION BY 加 DENSE_RANK,是本题的推广形态 |
| 177. 第N高的薪水 | 中等 | 全局取第 N 档,用 DISTINCT 压平并列后按偏移量定位 |
| 1204. 最后一个能进入巴士的人 | 中等 | 自连接用于累加前缀,展示同一技巧如何表达「本行与之前所有行」的关系 |
| 610. 判断三角形 | 简单 | 纯逐行判定,用 CASE WHEN 派生一列,是无连接无聚合的最基础形态 |