
1. 面试官为什么总爱问“重复记录怎么删”SQL 面试里有一类题几乎逢考必出给你一张用户表里面有几条姓名和手机号完全一样的记录让你先查出来再删掉多余的只保留一条。看起来简单但真正写起来很多人会卡在“保留哪一条”“怎么保证不误删”“删完怎么验证”这几个点上。这个问题的核心检索词就是SQL 查询重复记录和SQL 删除重复记录。它考的不是你会不会写DELETE而是你对GROUP BY、HAVING、窗口函数、子查询这几块的理解是否扎实。面试官想看到的是你能不能先定位重复再安全地删除最后用行数对比证明结果正确。我试过在真实项目里处理一张 200 万行的订单表重复数据来自上游同步任务重跑当时用ROW_NUMBER()配合 CTE 一次性清理干净整个过程不到 3 秒。所以这类题不是纸上谈兵它是实打实的生产技能。先明确一个概念重复记录分两种。第一种是完全重复所有字段的值都一模一样第二种是关键字段重复比如name相同但age不同业务上认为这算重复需要保留一条。两种情况的处理方式不一样面试时最好主动区分这样能加分。下面我用一张具体的表来演示。假设表名是users结构如下CREATE TABLE users ( id INT PRIMARY KEY, name VARCHAR(50), phone VARCHAR(20), city VARCHAR(50) ); INSERT INTO users VALUES (1, 张三, 13800001111, 北京), (2, 张三, 13800001111, 北京), (3, 李四, 13900002222, 上海), (4, 李四, 13900002222, 上海), (5, 王五, 13700003333, 广州), (6, 张三, 13800001111, 北京);这张表里id1,2,6是完全重复的id3,4也是完全重复的。接下来所有操作都基于这张表展开。2. 用 GROUP BY HAVING 定位重复记录2.1 查询完全重复的记录第一步永远是“先查再删”。直接删是危险的你得先看清楚有多少重复、重复在哪。用GROUP BY加HAVING是最直观的方式SELECT name, phone, city, COUNT(*) AS cnt FROM users GROUP BY name, phone, city HAVING COUNT(*) 1;执行结果namephonecitycnt张三13800001111北京3李四13900002222上海2这里GROUP BY把所有字段都列上HAVING COUNT(*) 1筛出出现次数大于 1 的组合。注意WHERE不能替代HAVING因为聚合函数的结果必须在分组之后才能过滤这是面试常问的细节。2.2 查询关键字段重复的记录如果业务只关心name重复不管其他字段那就只对name分组SELECT name, COUNT(*) AS cnt FROM users GROUP BY name HAVING COUNT(*) 1;结果会显示张三 3 条、李四 2 条。这时候你拿到的是“哪些 name 重复了”但还没拿到具体是哪几行。要拿到完整行需要把子查询套回去SELECT * FROM users WHERE name IN ( SELECT name FROM users GROUP BY name HAVING COUNT(*) 1 ) ORDER BY name, id;这样就能看到所有重复 name 对应的完整记录方便你决定保留哪一条。面试时如果只写前半段面试官可能会追问“那具体是哪几行重复”所以这一步最好主动补上。2.3 用窗口函数标记重复行GROUP BY适合“查有多少”但如果你想知道“每一行是不是重复的”窗口函数更合适SELECT *, ROW_NUMBER() OVER (PARTITION BY name, phone, city ORDER BY id) AS rn FROM users;结果里rn1的是每组的第一条rn1的就是多余记录。这个rn字段是后面删除操作的关键。PARTITION BY后面跟的就是你判断重复的依据字段ORDER BY id决定保留哪一条——这里保留 id 最小的。3. 可复制的去重配置与删除语句3.1 删除完全重复、保留最小 id最稳妥的写法是用 CTE 加ROW_NUMBER()先标记再删除WITH ranked AS ( SELECT id, ROW_NUMBER() OVER (PARTITION BY name, phone, city ORDER BY id) AS rn FROM users ) DELETE FROM users WHERE id IN (SELECT id FROM ranked WHERE rn 1);这条语句的逻辑是按name, phone, city分组组内按id升序编号编号大于 1 的就是要删的。执行后id2,4,6会被删除剩下id1,3,5。如果你用的是 MySQL 5.7 以下版本不支持 CTE可以改成自连接DELETE u1 FROM users u1 JOIN users u2 ON u1.name u2.name AND u1.phone u2.phone AND u1.city u2.city AND u1.id u2.id;这条语句的意思是只要存在一条 id 更小的相同记录当前这条就删掉。效果和上面一致。3.2 删除关键字段重复、保留最小 id如果只按name去重把PARTITION BY改成name即可WITH ranked AS ( SELECT id, ROW_NUMBER() OVER (PARTITION BY name ORDER BY id) AS rn FROM users ) DELETE FROM users WHERE id IN (SELECT id FROM ranked WHERE rn 1);执行后每个 name 只保留 id 最小的那条张三保留 id1李四保留 id3王五保留 id5。3.3 在 TaoToken 通道下让模型帮你生成和校验 SQL写 SQL 的时候尤其是窗口函数嵌套删除这种很容易漏掉边界条件。我习惯用 TaoToken 的统一 API 通道调模型来帮我生成初稿再自己审一遍。配置很简单三件套是 Base URL、API Key、Model ID。如果你用的是 Cline 这类支持 MCP 的编辑器插件配置片段如下{ mcpServers: { taotoken: { url: https://taotoken.net/api, headers: { Authorization: Bearer 你的API_KEY }, model: claude-sonnet-4-20250514 } } }如果你用的是 Claude Code可以在项目根目录的.claude/settings.json里写{ apiBaseUrl: https://taotoken.net/api, apiKey: 你的API_KEY, model: claude-sonnet-4-20250514 }Codex 用户则在~/.codex/auth.json里配置{ base_url: https://taotoken.net/api, api_key: 你的API_KEY, model: claude-sonnet-4-20250514 }配好之后你可以直接把表结构和需求丢给模型“这张表按 name 去重保留 id 最小的给我一条 PostgreSQL 的 DELETE 语句并解释为什么不能用 GROUP BY 直接删。”模型会给你一条带 CTE 的语句还会提醒你DELETE不能直接跟聚合函数。拿到之后你在测试库跑一遍确认行数变化符合预期再上生产。API Key 在控制台的 API Keys 页面生成模型对话入口可以先用模型对话页面快速验证通道是否通。4. 验证请求与执行前后行数对比4.1 执行前先记录行数删除之前一定要先查总数这是验证的基础SELECT COUNT(*) AS total_before FROM users;假设结果是 6。删除之后再查一次SELECT COUNT(*) AS total_after FROM users;如果结果是 3说明删掉了 3 条重复记录符合预期。这个前后对比是面试时展示你严谨性的好机会很多人只写删除语句忘了验证。4.2 用查询确认剩余记录光看行数还不够还要确认剩下的是不是每组最小 idSELECT * FROM users ORDER BY id;预期结果idnamephonecity1张三13800001111北京3李四13900002222上海5王五13700003333广州如果结果和这个一致说明去重成功。如果发现某个 name 一条都没了那说明PARTITION BY或ORDER BY写错了需要回滚重来。4.3 用模型辅助校验 SQL 逻辑有时候你自己写的 SQL 跑出来结果对但逻辑上可能有隐患。比如ROW_NUMBER()的ORDER BY如果用了非唯一字段可能导致每次执行保留的行不一样。这时候可以让模型帮你审一遍请检查这条 SQL 是否存在不确定性ROW_NUMBER() OVER (PARTITION BY name ORDER BY id)如果 id 有重复会怎样模型会告诉你如果id有重复ROW_NUMBER()的结果可能不稳定建议加一个唯一字段做 tie-breaker。这种校验在面试里也能体现你的深度。5. 本篇常见报错与排查5.1 报错You cant specify target table for update in FROM clause这是 MySQL 的经典报错。你写DELETE FROM users WHERE id NOT IN ( SELECT MIN(id) FROM users GROUP BY name );MySQL 不允许在DELETE的子查询里直接引用被删的表。解决办法是套一层派生表DELETE FROM users WHERE id NOT IN ( SELECT id FROM ( SELECT MIN(id) AS id FROM users GROUP BY name ) AS tmp );或者直接用 CTE 加ROW_NUMBER()绕开这个问题。5.2 报错local proxy failed 或 401如果你在 TaoToken 通道下调模型生成 SQL 时报local proxy failed先检查 Base URL 是不是写成了https://taotoken.net/api有没有多写斜杠或漏掉/api。报401则是 API Key 无效或过期去控制台的 API Keys 页面重新生成一个替换配置里的 Key。注意 Key 不要提交到 Git用环境变量或本地配置文件。5.3 报错reading choices 时解析失败这个通常出现在流式响应里模型返回的 JSON 结构和你客户端预期的不一致。检查你的 Model ID 是否写对比如claude-sonnet-4-20250514不要写成claude-sonnet-4。如果用的是 Cline确认 MCP 配置里的model字段和实际调用的模型一致。改完配置后重启编辑器插件再试。5.4 删除后行数没变如果DELETE执行了但行数没变先看是不是没有COMMIT。有些客户端默认手动提交你需要显式执行COMMIT;。另外检查WHERE条件是不是写成了rn 1那样删的是要保留的行结果会反过来。执行前先用SELECT把要删的 id 列出来确认无误再改成DELETE。5.5 OAuth 相关报错如果你在 Claude Code 里配置了 TaoToken 但仍然走 OAuth 流程报错检查settings.json里是否同时存在旧的 OAuth 配置。把冲突的字段删掉只保留apiBaseUrl、apiKey、model三项。保存后重启 Claude Code让它重新读取配置。6. 把去重能力接到你的日常开发流里面试题只是入口真正有价值的是把“查重—去重—验证”这套流程固化下来。我的做法是在 TaoToken 的 Coding Plan 里建一个常用提示词模板每次遇到重复数据问题直接调模型生成 SQL 初稿然后自己在测试库跑一遍行数对比。这样既快又不容易出错。如果你经常处理数据清洗建议把ROW_NUMBER()这套写法记牢它比GROUP BY加自连接更直观也更容易扩展到“保留最新一条”“保留金额最大一条”这类变体。需要长期做编码和 Agent 任务的话Coding Plan 的额度比按次调用更划算。最后留一个实用技巧删除前先CREATE TABLE users_backup AS SELECT * FROM users;万一删错了还能恢复。这个习惯在面试里说出来面试官会觉得你有生产意识。