多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

LeetCode SQL 1378:LEFT JOIN替换员工ID,搞懂关联查询核心语义

LeetCode SQL 1378:LEFT JOIN替换员工ID,搞懂关联查询核心语义 刷LeetCode“高频SQL 50题”的朋友大概率会撞上这道1378。题名“使用唯一标识码替换员工ID”第一眼挺唬人好像要搞UPDATE或者数据清洗但读进去就会发现它考的就是SQL JOIN里最基础也最容易翻车的一个场景以左表为准能匹配就展示关联数据匹配不到就留空。这道题非常适合用来检验自己是不是真的懂LEFT JOIN的语义而不只是会背SQL语法。我平时带团队新人经常拿这道题做入门测试。原因很简单它表面上是“替换ID”实质上是在模拟日常报表里最常见的一种需求——两张表通过ID建立对应关系输出时把业务编号换成另一个编号体系。如果能一眼看出这是LEFT JOIN说明你对连接逻辑有直觉如果上来就写INNER JOIN那后续面对复杂查询时大概率会出问题。下面我把这道题的思考过程、标准解法、生产环境的坑以及类似题目的通用套路完整拆一遍。1. “替换”不是UPDATE先看清两张表在表达什么业务1.1 两张表的数据模型题目给的是两张标准的主从关系表。第一张是员工表Employees结构很简单id是员工主键name是员工姓名。第二张是员工唯一标识表EmployeeUNI存储的是id和unique_id的对应关系而且(id, unique_id)是联合主键。先解释一下联合主键的含义。它表示这张表里同一个id可以出现多次每次对应一个不同的unique_id但id和unique_id的组合不能重复。放到真实业务里这就像公司内部人员编码和外部系统编码两套体系Employees表是HR系统的员工档案EmployeeUNI表则是把内部工号映射到门禁卡号、邮箱唯一标识或外部协作平台账号的对照表。一个员工可能有多张卡片、多个账号所以要靠联合主键才能保证映射关系不冲突。理解到这个层面你就能抓住这道题的真正语义不是要把Employees表里的id字段物理改掉而是在查询展示时优先用EmployeeUNI表中的unique_id去“替换”员工的id作为结果输出。替换失败即对照表里查不到的员工保留为一个空值。1.2 手工推演一遍结果我们看题目给的示例数据。Employees表有五个人Alice的id是1Bob是7Meir是11Winston是90Jonathan是3。EmployeeUNI表里只有三组映射id 3对应unique_id 1id 11对应unique_id 2id 90对应unique_id 3。也就是说Jonathan、Meir、Winston都能在对照表里找到唯一标识码Alice和Bob查不到。最终输出要求把unique_id放在第一列name放在第二列没有唯一标识的员工用null填充于是得到的结果表前三行是null开头的Alice和Bob后面三行才是1对应的Jonathan、2对应的Meir、3对应的Winston。这里有一个非常关键的观察点结果里员工一个都不能少。如果按INNER JOIN做Alice和Bob会直接被过滤掉这就违背了“展示每位用户”的要求。所以主表必须保留全量记录这正是LEFT JOIN的语义。1.3 为什么这道题不涉及UPDATE很多初学者看到“替换”两个字脑子里第一反应是写UPDATE语句把Employees表的id换成unique_id。这是理解上的偏差。UPDATE会物理改写数据一旦执行就是不可逆的操作而且EmployeeUNI表里没有映射的员工会被置成空员工表本身的主键也就被破坏了。在实际业务中“替换”更多是指查询输出层的替换。例如业务系统里存的是内部工号但给客户看报表时要展示客户编号底层数据不会动只是在SELECT层关联后选择展示哪一列。SQL里的SELECT决定了最终呈现的列所以这道题的本质是“查询时替换展示字段”而不是“更新数据”。2. 标准解法LEFT JOIN与三种常见变形别让“多写一个条件”毁掉结果2.1 标准答案与等价写法这道题的标准解法很简洁SELECT u.unique_id, e.name FROM Employees e LEFT JOIN EmployeeUNI u ON e.id u.id;关键点有两个。第一连接方向是以Employees为驱动表左连接EmployeeUNI保证所有人都在结果里第二SELECT列的书写顺序必须是unique_id在前、name在后因为题目示例的输出列顺序就是unique_id第一列。如果你把列顺序写成e.name, u.unique_id结果集内容没错但列顺序和预期不符在某些自动判题系统里会被判定为错误。LEFT JOIN是标准答案但不是唯一答案。把连接方向反一下用RIGHT JOIN同样能得到结果SELECT u.unique_id, e.name FROM EmployeeUNI u RIGHT JOIN Employees e ON e.id u.id;这段SQL里EmployeeUNI变成了驱动表RIGHT JOIN以右边的Employees为准语义和LEFT JOIN完全等价。虽然RIGHT JOIN在各大数据库里都能正常运行但从可读性和团队规范角度我建议统一使用LEFT JOIN。绝大多数开发者在阅读SQL时习惯“从左往右”看主表RIGHT JOIN容易让人多花半秒确认哪边是保留全量的表属于不必要的认知负担。2.2 INNER JOIN为什么是错误答案手滑写成INNER JOIN是这道题最常见的错误SELECT u.unique_id, e.name FROM Employees e INNER JOIN EmployeeUNI u ON e.id u.id;这段SQL执行后结果只包含三行Jonathan、Meir、Winston。Alice和Bob因为没有对应的unique_id直接被INNER JOIN过滤。题目要求“如果员工没有唯一标识码则展示null”而INNER JOIN的语义是“只保留两边都能匹配上的行”两者天然冲突。我从面试官视角说一句这道题问的就是能不能分清INNER JOIN和LEFT JOIN的区别。能写出LEFT JOIN并说明理由说明你理解连接类型写出INNER JOIN的人往往只记住了“两张表通过ID关联”这一层没有继续追问关联后哪些行该保留。2.3 一个隐蔽的坑在WHERE里过滤NULL还有一种错误写法看似合理实则会导致结果和INNER JOIN一模一样SELECT u.unique_id, e.name FROM Employees e LEFT JOIN EmployeeUNI u ON e.id u.id WHERE u.unique_id IS NOT NULL;问题出在WHERE的执行顺序上。SQL语句的逻辑执行顺序大致是FROM、ON、JOIN、WHERE、GROUP BY、HAVING、SELECT、ORDER BY。LEFT JOIN在ON阶段已经把Alice和Bob的行保留下来并且u.unique_id为NULL但紧接着WHERE条件对所有行做了一遍过滤把NULL行删掉了。最终结果和INNER JOIN完全一致。这是一个非常经典的SQL陷阱。很多人以为“我加了LEFT JOIN就万事大吉”结果在WHERE里多加一个判断不小心把保留的行过滤没了。正确做法是如果要求在关联阶段保留不匹配行就不能在WHERE里对该右表字段做非空过滤。类似的需求如果需要过滤应该把条件写在ON子句里比如ON e.id u.id AND u.unique_id IS NOT NULL但这道题不需要这么做。2.4 画蛇添足的COALESCE还有一部分人会纠结NULL展示问题在SELECT里套一层COALESCESELECT COALESCE(u.unique_id, 0) AS unique_id, e.name FROM Employees e LEFT JOIN EmployeeUNI u ON e.id u.id;这属于没读懂题目。题目明确说“如果员工没有唯一标识码则展示null”这里的null是有业务意义的表示该员工尚未分配唯一标识。你把它替换成0语义就变了用户会误以为所有人的唯一标识都是有效数字甚至0还可能和某些真实ID冲突。在实际项目中“保留NULL”和“用默认值填充NULL”是两种截然不同的需求必须按照业务要求来不能凭个人喜好处理。3. 从刷题到写生产报表列名、NULL与重复数据三个坑3.1 列名冲突为什么一定要起表别名LeetCode的测试环境比较干净两张表除了id之外列名没有重叠。但生产环境不是这样。员工表里有name对照表里很可能也有remark、name或者created_at等相同字段。如果你在SELECT里直接写name很多数据库会直接报“ambiguous column”错误因为优化器不知道你指的是哪张表的name。规范做法就是像标准答案那样给表起别名并且在SELECT和ON里都用别名限定列SELECT u.unique_id, e.name FROM Employees e LEFT JOIN EmployeeUNI u ON e.id u.id;这个习惯看起来微不足道但在关联三张以上表时能救命。我见过不少同事因为省了别名在字段冲突时排查半天最后发现是查重复了。别名不只是缩写它是在明确告诉数据库和你自己这个字段来自哪张表。3.2 各种数据库对NULL的显示差异如果你把这道题的SQL拿到不同数据库里跑会发现NULL的显示方式五花八门。MySQL的命令行里显示为NULLSQL Server里显示为空白PostgreSQL里显示为nullOracle里直接是空字符串一样的效果。这不影响数据本身但如果你把结果导出成Excel或者嵌入到报表前端就必须考虑空值在前端的展示策略。很多接口开发者在拿到查询结果后会做一次序列化处理。如果字段值是NULLJSON输出里就是unique_id: null前端拿到后要决定渲染成“未分配”“-”还是空。这个过程如果没沟通好就会出现报表里白茫茫一片业务方以为数据丢了。这道简单的LeetCode题背后其实反映了从SQL结果到业务展示的完整链路里NULL语义的一致性是很容易被忽略的环节。3.3 对照表出现重复ID时结果会翻倍题目里EmployeeUNI表有联合主键保证每个(id, unique_id)组合唯一。但真实数据源往往没这么规范。假设EmployeeUNI表里id为3的员工被录入了两条记录一条unique_id是1另一条是2。此时LEFT JOIN的执行逻辑是对Employees表里id为3的行逐条匹配EmployeeUNI表里id为3的所有行结果就会出现两行Jonathan分别是unique_id 1和2。这在业务上可能是合理的一个人确实可能有两个外部标识但如果业务预期是“一个员工最多输出一个唯一标识”那数据重复就会导致报表行数异常膨胀。解决思路通常是在连接前对右表做去重按业务规则取一条。比如用ROW_NUMBER窗口函数按id分组按unique_id排序保留最小的一条WITH ranked_uni AS ( SELECT id, unique_id, ROW_NUMBER() OVER (PARTITION BY id ORDER BY unique_id) AS rn FROM EmployeeUNI ) SELECT u.unique_id, e.name FROM Employees e LEFT JOIN ranked_uni u ON e.id u.id AND u.rn 1;这样既能保留Employees表全量数据又能避免右表重复记录把结果撑大。窗口函数在联动查询里的这个用法很实用值得顺手掌握。3.4 唯一标识码为空时的隐藏业务判断最后再提醒一点。EmployeeUNI表里的unique_id本身也可能为NULL这是很多人容易忽略的。如果unique_id字段允许空值那么即使LEFT JOIN匹配上了结果里照样是NULL。这时你无法区分“没匹配上”和“匹配上但值为空”两种情况。如果业务上必须区分这两种状态就要在SELECT里加一个辅助判断字段比如SELECT u.unique_id, e.name, CASE WHEN u.unique_id IS NULL THEN NO_MATCH ELSE MATCHED END AS match_status FROM Employees e LEFT JOIN EmployeeUNI u ON e.id u.id;但在LeetCode这道题里不需要也无法区分因为题目假设EmployeeUNI中的unique_id非空。实际开发时这种字段级的空值判断最好提前和数据owner确认清楚避免上线后才发现数据对不上。4. 同类替换/补全题的通用套路与面试表达4.1 这类题的核心模式主表保留全量副表补充属性“使用唯一标识码替换员工ID”并不是一道孤立的题。LeetCode上的SQL题里类似模式的高频题非常多。比如175题“组合两个表”要求不管Person表里的人有没有地址都要输出姓名、城市、州没有地址的显示NULL解法同样是LEFT JOIN。再比如1581题“进店却未进行交易的顾客”本质上是先找出所有进店顾客再去匹配交易记录匹配不到就统计为0核心思路也是一样的。这类题有一个共同的分析框架先确定哪张表是“主表”结果集必须保留全量的表再确定哪张表是“补全表”用于提供额外属性或状态最后根据业务要求选择连接类型。如果要求主表全量保留就选LEFT JOIN或RIGHT JOIN如果要求只输出两边匹配的数据就选INNER JOIN。这套框架不仅能解LeetCode拿到真实业务场景里一样适用。4.2 升级变体匹配不上时补默认值有时候业务要求“匹配不到就显示0”或者“匹配不到就显示未知”这时候需要在LEFT JOIN基础上再包一层COALESCE。假设这道题改成“如果员工没有唯一标识码则展示0”SQL就变成SELECT COALESCE(u.unique_id, 0) AS unique_id, e.name FROM Employees e LEFT JOIN EmployeeUNI u ON e.id u.id;区别只在最后展示层做了空值兜底。从这里可以提炼出一个更通用的记忆方式LEFT JOIN解决“有没有行”的问题COALESCE解决“行里有NULL值怎么展示”的问题两者各管一摊。面试时遇到这类问题你可以把这两个知识点分开讲面试官会觉得你思路清晰。4.3 面试时怎么表达这道题的解题思路如果你在面试中被问到这道题不要急着甩SQL。建议按这个顺序回答先说明业务目标是“每位员工都要出现在结果里”所以必须保留Employees全表再说唯一标识来自EmployeeUNI表属于可选的补充信息匹配不上时应显示NULL最后落到连接类型上选择LEFT JOIN并把SELECT列顺序按题目要求写好。回答时最好提一句INNER JOIN的区别“如果用INNER JOIN匹配不上的员工会被过滤掉不符合每位员工都要展示的题目要求。”这一句话就能体现出你理解连接类型差异而不是单纯背答案。4.4 刷题阶段的额外训练方向如果你已经能顺畅写出这道题的标准答案我建议再做两个变体训练。第一个变体把两张表互换要求“展示所有唯一标识码及其对应员工姓名如果某个唯一标识码没有对应员工则保留null”这时候主表变成EmployeeUNI需要LEFT JOIN Employees思考一下结果有什么不同。第二个变体给EmployeeUNI表加一个生效时间字段要求“每位员工只取最近一条生效记录”这就要用到窗口函数和子查询复杂度瞬间提升到中高级别。这两个变体练完你对LEFT JOIN和表间关系的理解会扎实很多。实际上我面试时也喜欢用这个思路考候选人先出一道1378原题再让候选人自己改动需求方向看他们能不能根据需求变化调整连接方向。能把简单题变着花样做出来的人通常对SQL的理解都不是死记硬背型的。写在最后这道题我刷过很多遍每次带新人还是会用。它真正的价值不在于答案有多难而在于它强迫你想清楚一个最基本的问题当两张表关联时你到底想让哪张表的每一行都出现在结果里。想清楚这一点LEFT JOIN、RIGHT JOIN、INNER JOIN的区别就不需要背了全是顺着语义自然推导出来的。如果你现在看这道题还需要犹豫几秒才能判断用哪种连接建议别急着背答案先拿着示例数据手工跑一遍JOIN的过程。把“逐行匹配、保留主表、NULL填充”这三个动作在纸上画出来比刷十道类似题都管用。SQL里的连接类型本来就是一套逻辑规则理解了规则题目怎么变都能应对。
返回列表