订阅更新

SQL 排名时 NULL 应该放哪里?加一个 DESC 为什么可能选错记录?

面试问 334 约 3 分钟读完

本文目录
  1. 面试怎么回答
  2. 默认行为不是业务口径
  3. 缺失评分不能随便补成零
  4. 排名逻辑要把规则写完整
  5. 并列顺序和业务排名也别混为一谈
  6. 一个典型场景:迁移后推荐名单变了
  7. 一条稳定SQL,还需要可解释的输出

面试怎么回答

我会先确认NULL代表未知还是不适用,再决定是否允许参与排名。允许保留但希望排后,就显式写DESC NULLS LAST,并增加稳定的并列次序;不允许缺失评分参与,则在排名前排除。两者在全空分组上不同,NULLS LAST仍可能选出一条空值记录。需要保留全部分组时,再把结果关联回分组全集,明确没有合格候选。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

某个分组的“最高评分商品”居然是一条评分为NULL的记录。SQL没有报错,排序也写了DESC。问题不是数据库不会排序,而是开发者把空值的默认位置想当然了。

空值的位置和记录的候选资格是两层规则;显式声明NULL排序,并单独决定缺失数据能否参与排名。

默认行为不是业务口径

以 PostgreSQL 17 为例,降序默认把NULL排在前面。其他引擎可能采用不同默认规则,跨引擎迁移时尤其不能只凭熟悉的经验判断。

显式写NULLS LAST可以表达空值排后,但它没有把空值从集合里删除。一个分组全是空值时,仍然会存在排序第一的记录。

所以这道题要先拆成两个问题:谁有资格参与排名,以及有资格的记录之间如何排序。前者属于业务筛选,后者属于排列规则,不能靠一个排序参数同时解决。

缺失评分不能随便补成零

NULL可能代表尚未评分,也可能代表不适用。零则可能是已经评价后的真实分数。把NULL补成零,虽然让排序看起来简单,却改变了字段含义。

若真实分数允许负值,补零甚至会让未知值超过负分记录。即使当前分数只在零到一百分之间,也会混淆“没有评价”和“已经评价为零”。

对外展示可以给出“待评分”标签,内部仍保留缺失状态。展示便利不应反过来决定分析层的数据语义。

排名逻辑要把规则写完整

假设商品表每个商品一行,item_id稳定且唯一,需求是优先选择有评分商品,保留全空分组的一条占位记录:

WITH ranked AS (
  SELECT category_id, item_id, score,
         ROW_NUMBER() OVER (
           PARTITION BY category_id
           ORDER BY score DESC NULLS LAST, item_id
         ) AS rn
  FROM item_score
)
SELECT category_id, item_id, score
FROM ranked
WHERE rn = 1;

如果需求改成“只能推荐已评分商品”,就应在窗口计算前加score IS NOT NULL。这样全空分组不会产生结果,而不是自动挑选一条待评分记录。

需要在看板中显示这些分组时,再从品类全集LEFT JOIN排名结果,用独立状态说明没有合格候选。不要为了让每个分组都有行,就把未知分数改造成真实低分。

并列顺序和业务排名也别混为一谈

增加item_id可以稳定挑选同分商品,但这只是确定性选择,不代表两个同分商品的业务等级不同。

若要求所有并列第一都入选,应按评分使用RANK或DENSE_RANK,再独立控制最终展示次序。如果把唯一商品号也加入业务排名的ORDER BY,就可能把同分拆成不同名次。

同理,“最新记录”按更新时间降序取一条时,时间为空可能意味着无法证明新旧。把它放最后只是展示决策,并不能补出缺失的时间证据。

一个典型场景:迁移后推荐名单变了

假设同一份数据迁移到另一个查询引擎后,部分品类的推荐商品发生变化。开始怀疑同步顺序不同,检查主键、行数、评分总和都一致。

真正变化在空值默认排序:原SQL没有声明NULL位置,新的执行环境把空值放在另一端。旧验数验证了输入集合,却没有验证带有空值的Top一输出。

修正先确认业务究竟允许待评分商品上榜,再分别处理候选筛选与排序。测试补上正常分、零分、负分、部分为空、全部为空及最高分并列的分组,不只对比总行数。

一条稳定SQL,还需要可解释的输出

返回商品号之外,可以附带评分是否缺失、候选数量和规则版本。这样业务看到空结果时,能区分没有商品、商品全部未评分,还是过滤条件排除了全部候选。

排序SQL的价值不只是每次得到同一条记录,还要让这条记录为什么被选中说得通。特别是在推荐、质量评选和最新版本选择中,空值处理就是结果的一部分,而不是数据库默认值的小细节。

刷题进度保存在当前浏览器