☰
LeetCode SQL 1378:LEFT JOIN替换员工ID,搞懂关联查询核心语义
2026/10/4 2:51:02 网站建设 项目流程

刷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是1,Bob是7,Meir是11,Winston是90,Jonathan是3。EmployeeUNI表里只有三组映射:id 3对应unique_id 1,id 11对应unique_id 2,id 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里套一层COALESCE:

SELECT 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的命令行里显示为NULL,SQL Server里显示为空白,PostgreSQL里显示为null,Oracle里直接是空字符串一样的效果。这不影响数据本身,但如果你把结果导出成Excel或者嵌入到报表前端,就必须考虑空值在前端的展示策略。

很多接口开发者在拿到查询结果后,会做一次序列化处理。如果字段值是NULL,JSON输出里就是"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里的连接类型本来就是一套逻辑规则,理解了规则,题目怎么变都能应对。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询