资讯动态

SQL三值逻辑与NULL陷阱:从力扣584题看推荐人查询

发布时间:2026/9/26 17:19:35 来源:尧图企业网站定制
这道题在力扣上属于典型的“一眼就会、一写就错”的SQL题。584题“寻找用户推荐人”题干就一张Customer表让你找出没有被2号用户推荐过的客户名字看起来就是一行WHERE的事但如果你在面试里写的是WHERE referee_id ! 2那基本就踩进去了。原因只有一个NULL。很多人刷题刷得飞快代码跑出来和预期不一致最后发现是空值在捣乱。这题在LeetCode的SQL题库里是个经典Easy题但它考察的东西一点都不“Easy”SQL的三值逻辑、NULL比较、以及真实业务里推荐人场景的数据特征。搞懂这一道题很多关联查询和空值处理的坑都能避开。这篇文章会从表结构拆起把几种常见解法、为什么会有坑、以及延伸到真实业务里的写法都过一遍适合刚开始刷SQL题的新手也适合被NULL坑过几次、想彻底搞明白的开发者。1. 题目拆解一张Customer表找出没有被用户2推荐的人1.1 表结构与示例数据原题给的Customer表结构很简单就三个字段列名类型含义idint客户自身的主键namevarchar客户名字referee_idint推荐该客户的推荐人ID指向Customer.id简单说referee_id记录的是“这位客户是被哪个客户推荐进来的”。比如某行数据的referee_id是2说明这位客户是被id为2的客户推荐注册的。这就是一个最典型的用户邀请注册关系很多产品的“邀请有礼”功能底层都是这个结构。原题的示例数据是这样的idnamereferee_id1Willnull2Janenull3Alex24Billnull5Zack16Mark2题目要求返回“没有被2号用户推荐”的客户名字。直观上看2号用户Jane推荐了Alex和Mark所以剩下的Will、Jane、Bill、Zack四个人都是符合条件的预期输出是这四行。关键在于这里有三行数据的referee_id是NULL意思是这三位客户并没有被任何人推荐或者至少没有被记录推荐来源。在判断“是否被2号推荐”时NULL既不是2也不“等于不是2”这种边界情况就是全部陷阱所在。1.2 这个需求放在业务里是什么样的场景如果把这个查询放到真实的互联网产品中它对应的运营需求是“找出所有不是被某个特定用户推荐来的客户”。比如做电商平台想给“不是从某个大V直播间或某个特殊推广链接进来”的用户发一批优惠券做内容社区运营想统计“自然注册、没有邀请人”的用户占比。这类需求几乎是所有带邀请机制的产品的标配。我见过不止一个初级开发在真实业务里写WHERE referee_id ! 2然后跑出来一份“少了一大批人”的名单。运营拿着名单过来问为什么这些明显不是被2号推荐的用户没有出现在结果里一查才发现这些人的referee_id是NULL它们被SQL的三值逻辑静默过滤掉了。刷题时最容易忽略的NULL在生产环境里就是直接的事故。注意真实业务中的NULL可能有两种含义。一是该用户确实没有被任何人推荐属于自然流量二是历史数据迁移或录入时丢了推荐来源。这两种情况往往都需要归入“非指定推荐人”来处理所以不能简单用不等号跳过。2. 核心考点SQL三值逻辑和NULL比较是这道题的命门2.1 为什么“不等于2”会漏人SQL里的比较逻辑和我们日常认知的“真和假”不一样它有三个值TRUE、FALSE、UNKNOWN。任何值与NULL做比较得到的结果都不是TRUE也不是FALSE而是UNKNOWN。WHERE子句只保留计算结果为TRUE的行UNKNOWN的行会被过滤掉。所以当你写WHERE referee_id ! 2时数据库在碰到referee_id为NULL的那几行时计算出的结果是UNKNOWN于是这些行被丢掉了。明明这几个客户的推荐人不是2号逻辑上应该留下结果因为NULL被排除在外。这不是数据库“不讲道理”而是SQL规范里最基础的三值逻辑。你可以把NULL想象成一个“未知的黑盒子”。这个黑盒子等于2吗不知道。这个黑盒子不等于2吗也不知道。数据库不会替你猜测它只会诚实地给出“不知道”这个结果而WHERE只要“确定是真的”才放行。2.2 标准解法一OR IS NULL 显式补漏最直接的思路就是把空值条件显式加回来SELECT name FROM Customer WHERE referee_id ! 2 OR referee_id IS NULL;这个SQL的逻辑是先找出所有明确不等于2的人再用OR把referee_id为空的人并进来。两个条件取并集NULL行的判断由IS NULL显式兜底结果就完整了。这是最贴近题目语义、也最容易解释的写法。这里有个非常实用的建议在实际项目中只要条件里出现“不等于某个值或为空”的组合我都建议用括号把并列条件包起来比如WHERE (referee_id ! 2 OR referee_id IS NULL)。这道题只有一个OR不写括号也能正确执行但在多条件场景里AND的优先级高于OR一旦混合使用很容易出现逻辑错误。不要挑战自己的记忆力直接加括号最稳。2.3 标准解法二用COALESCE或IFNULL把空值顶出去另一种常见思路是用函数把NULL替换成一个业务上不可能出现的值再进行普通比较。MySQL里常用IFNULL标准SQL里更通用的是COALESCESELECT name FROM Customer WHERE COALESCE(referee_id, -1) ! 2;把NULL变成-1之后-1 ! 2的结果是TRUE空值行就自然保留下来。MySQL里也可以写成IFNULL(referee_id, -1) ! 2或IFNULL(referee_id, 0) ! 2。这个写法的优势在于在更复杂的查询里不用反复惦记“这里会不会漏掉NULL”。只要把NULL统一替换成哨兵值后续就按普通值处理。但前提是替换值绝对不能出现在真实数据中。推荐人ID一般是自增ID不会是负数用-1是安全的如果业务里真的允许负数ID就得换更大的哨兵值或者老老实实用IS NULL。就这道题而言我个人更推荐第一种写法因为它直白、可读性强面试时也好解释。COALESCE写法规避了NULL但会让听的人多一层“为什么要用-1”的疑问需要额外解释。3. 五种可行解法与真实业务场景落地3.1 UNION ALL与OR可读性和性能的权衡除了上面两种力扣题解区还经常出现UNION ALL的写法SELECT name FROM Customer WHERE referee_id IS NULL UNION ALL SELECT name FROM Customer WHERE referee_id ! 2;UNION ALL把两个独立条件分别查询再拼接逻辑非常清晰且每个分支都可以独立使用索引。包含OR的查询有时会因为无法有效利用索引而退化成全表扫描虽然小表无所谓但在线上大表里我一般会优先考虑拆成UNION ALL。这样做执行计划更可控也方便单独排查每个分支的耗时。还要区分一下UNION和UNION ALLUNION会去重UNION ALL不去重。在这个题目里两个分支条件互斥不可能出现重复行用UNION ALL就够而且少一次去重排序效率更高。如果某个业务场景里两个条件可能产生重叠结果才需要用UNION做去重。如果想确认这两种写法在实际执行时的差异可以用EXPLAIN查看执行计划EXPLAIN SELECT name FROM Customer WHERE referee_id ! 2 OR referee_id IS NULL; EXPLAIN SELECT name FROM Customer WHERE referee_id IS NULL UNION ALL SELECT name FROM Customer WHERE referee_id ! 2;如果发现第一种的type是ALL说明走了全表扫描第二种两个分支各自走索引的概率更高。数据量一大这种差异会非常明显。3.2 LEFT JOIN的抗NULL写法还有一个看起来很绕但非常值得掌握的思路用LEFT JOIN加IS NULL过滤SELECT c.name FROM Customer c LEFT JOIN Customer r ON r.id 2 AND c.referee_id r.id WHERE r.id IS NULL;这里的核心是构造一个“只包含推荐人2号”的虚拟表然后用c.referee_id r.id做关联。能匹配上的行说明该客户是被2号推荐的匹配不上的行r.id就是NULL也就是我们要找的人。这个写法的巧妙之处在于它把“值与值比较”的NULL问题转换成了“关联后主键是否存在”的问题完全不依赖三值逻辑。我经常用这个思路处理一类问题“找出没有被某某记录关联的数据”包括“没有订单的用户”“没有被评论的文章”“没有被任何人推荐的用户”本质都是反连接。这道题能写出LEFT JOIN版本基本可以说明你对关联查询的理解到位了。面试的时候如果能把两种写法都讲清楚会比只说一种加分不少。3.3 NOT IN与NOT EXISTS最容易翻车的变体那能不能写WHERE referee_id NOT IN (2)明确告诉你在这个示例数据里它返回的是空结果集。原因还是NULL当referee_id为NULL时NULL NOT IN (2)的结果不是TRUE而是UNKNOWN。更严重的是只要查询的数据里存在哪怕一条NULL整个NOT IN的结果就可能被“污染”最终把所有行都排除掉。这个坑在子查询中更隐蔽。比如问“哪些客户没有被任何推荐人推荐过”一不小心就会写成SELECT name FROM Customer WHERE id NOT IN (SELECT referee_id FROM Customer);如果内层SELECT referee_id返回的结果里包含NULL这条SQL会直接返回空表而不是返回那三个没有推荐人的用户。我见过很多人在真实报表里踩这个坑排查半天发现是子查询带NULL导致的。遇到这种场景更安全的写法是NOT EXISTSSELECT c.name FROM Customer c WHERE NOT EXISTS ( SELECT 1 FROM Customer r WHERE r.id c.referee_id );NOT EXISTS判断的是“关联是否存在”NULL不会影响它的结果因此能正确返回所有没有被任何人推荐的客户。记住一句话当IN、NOT IN的子查询可能包含NULL时优先考虑EXISTS或NOT EXISTS这个习惯能帮你躲掉很多线上事故。3.4 真实业务中叠加更多条件后的写法真实系统里的推荐人查询一般不会只有一张表和一个条件。比如“找出最近30天注册、状态正常、且不是被2号用户推荐来的客户”SQL会变成这样SELECT u.name FROM users u WHERE (u.referee_id ! 2 OR u.referee_id IS NULL) AND u.status active AND u.reg_time CURRENT_DATE - INTERVAL 30 DAY;条件一变多括号的重要性就体现出来了。(u.referee_id ! 2 OR u.referee_id IS NULL)这个整体作为一个条件块再与其他条件AND逻辑就非常清楚。如果漏了括号可能会出现优先级导致的错误判断。另外大规模系统里推荐人关系往往单独拆成一张referral_records表用户表不直接存referee_id。这时查询方式会变成JOIN关系表同时还要处理“没有推荐记录”的用户逻辑本质上和584题一模一样。把这道题吃透你在那个场景里就不会慌。4. 常见错误排查为什么你写的SQL返回了空结果4.1 三大经典错误速查表这里把我见过最频繁的几种错误整理成表格方便拿去对照错误写法错误原因推荐修正WHERE referee_id ! 2NULL参与比较得到UNKNOWN空值行被过滤追加OR referee_id IS NULL或用COALESCEWHERE referee_id NULLNULL不等于任何值包括NULL本身写成referee_id IS NULLWHERE referee_id NOT IN (2, 3)数据里有NULL时NOT IN结果整体失效用NOT EXISTS或显式加IS NULL条件第一种和第三种是重灾区。尤其是第三种很多人以为NOT IN只是“不等于列表里任何一个”完全没有想到空值会让条件整体失效。这种问题在测试数据里碰巧没有NULL时根本发现不了一上生产就出事。4.2 排查技巧先看数据分布再写查询条件我写SQL的习惯是“先摸数据再动手”尤其遇到这种带空值判断的需求。可以先跑一条聚合SQL看一眼空值占比SELECT COUNT(*) AS total_rows, COUNT(referee_id) AS rows_with_referee, COUNT(*) - COUNT(referee_id) AS rows_with_null_referee FROM Customer;核心知识点COUNT(列名)会自动忽略该列的NULLCOUNT(*)不会忽略。用总数减去非空数就能算出空值行数。如果发现空值比例异常高比如只有10%的人填了推荐人ID那这个需求就要格外小心不能按常规思维处理。接下来可以用一条简单的SELECT * ... LIMIT 10肉眼确认数据形态再写正式查询。这些习惯看着不起眼但在处理线上数据、写报表SQL时特别能救命。很多看起来“莫名其妙”的结果其实只要先看一眼数据分布答案自己就出来了。4.3 在力扣上如何高效排查自己的解法力扣的SQL题都有在线执行窗口出错时可以直接看到返回结果和预期结果的差异。如果你写的答案和预期不一致先把预期结果里多出来或丢失的名字对应到示例数据然后看这些行的referee_id是否为空。只要发现丢失的行都是空值行基本就能锁定是三值逻辑问题。如果一时半会儿想不明白去题解区搜“NULL”“三值逻辑”“IS NULL”这几个关键词很快就能找到和自己思路相似的答案。刷题不要太着急AC把一道简单题的前因后果吃透比草草做十道题有用得多。584这道题虽然简单但值得你反复写几遍直到能在不看题解的情况下把三种正确解法都写出来。5. 从584题延伸推荐人场景与同类SQL陷阱5.1 变体一统计每个推荐人带来了多少客户基于同一张Customer表可以顺手练一个GROUP BY聚合。需求改成“统计每个推荐人的获客数量没有推荐人的单独归为一组”SELECT COALESCE(referee_id, -1) AS referee_id, COUNT(*) AS customer_cnt FROM Customer GROUP BY COALESCE(referee_id, -1) ORDER BY customer_cnt DESC;这里同样用COALESCE把NULL归位否则分组时空值会自成一组报表里出现一个referee_id为NULL的组展示起来不够直观并且部分BI工具对NULL的排序和过滤不友好。如果还要JOIN用户表显示推荐人姓名NULL组一般用LEFT JOIN推荐人姓名为空或显示“自然流量”。这种分组统计在运营后台非常常见比如“各渠道带新排行榜”只不过把referee_id换成了渠道编码或活动ID。5.2 变体二找出从没推荐过别人的用户这个需求正好对应前面提到的反连接模式“找出没有被任何其他人作为推荐人的用户”也就是产品的“沉默用户”。SQL可以写成SELECT c.id, c.name FROM Customer c LEFT JOIN Customer r ON r.referee_id c.id WHERE r.id IS NULL;理解这个写法的关键在于“关联右侧找不到任何匹配记录时r.id为NULL”。这是JOIN语义与NULL判断结合的经典套路。类似的还有“找出没有下过订单的用户”“找出没有被评论过的文章”都可以套用这个模式。如果你能不看答案写出这个SQL说明你对LEFT JOIN和反连接已经有感觉了。这个能力在写复杂报表时几乎每天都会用到。5.3 变体三按来源渠道分组统计如果业务里把推荐人ID对应成渠道编码查询就变成渠道维度的漏斗分析SELECT COALESCE(channel, unknown) AS channel, COUNT(*) AS new_user_cnt FROM user_registration GROUP BY COALESCE(channel, unknown);这段SQL用COALESCE把空渠道统一替换成unknown输出报表里不会出现难看的NULL字样。这类写法在数据仓库、BI报表里非常常见。绝大多数报表工具对NULL的过滤和排序都不友好与其在报表层处理不如在SQL层统一替换。5.4 和584题同类的LeetCode热门题目其实力扣SQL题库里不少热门题的核心考点都是空值问题。最典型的是176题“第二高的薪水”如果表里只有一个员工第二高薪水不存在题目要求返回NULL。很多新人写LIMIT 1 OFFSET 1结果一行数据都不返回正确写法要么用子查询套一层取MAX要么用IFNULL在外层兜底。再比如627题“变更性别”用IF和CASE WHEN处理枚举值时同样要区分空字符串和NULL。这些题和584题的底层思维完全一致先问自己“这条SQL里有没有可能遇到NULL遇到之后结果会变成什么”。把这个习惯养成你刷题的正确率会高一大截。我在实际项目中吃过不少NULL的亏。有一回写运营取数脚本上线前的测试数据里恰好没有空值结果跑出来的名单少了一整批自然流量用户运营等了一天才发现数据对不上。自从那次之后凡是我经手的SQL需求都会默认把“空值会不会影响结果”这个问题放在第一位。584这道题虽然难度不高但它把SQL里最容易被忽略的三值逻辑浓缩到了最小场景里非常值得反复咀嚼。如果能把这道题的NULL处理逻辑举一反三后面遇到再复杂的查询心里都会多一杆秤。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑