目录

题目描述

197. 上升的温度

题意分析

Weather 每行是某一天的记录,包含唯一 id、日期 recordDate 和当天温度 temperature。要求找出所有「与前一天相比温度更高」的日期,输出这些行的 id,顺序不限。

「前一天」这三个字是全题的重心。表里只有一行一天的孤立记录,行与行之间没有任何指针把今天和昨天串起来,所以必须自己造出这种关联。关系代数里能把同一张表的两行摆到同一行上比较的手段只有一个:把表和它自己连接起来,用连接条件规定「左边那行的日期比右边那行晚一天」。

约束还透露了一个容易被忽略的信号:recordDate 只保证唯一,不保证连续。也就是说数据里完全可能出现 2015-01-01 之后直接跳到 2015-01-05 的情况。这意味着不能用「按日期排序后比较相邻行」这种朴素思路,必须真正按「日期差恰好为 1 天」去匹配,日期不连续的那一对根本不该配上。

另一个信号是日期是 date 类型而不是整数,所以「差一天」不能写成减法,得用数据库提供的日期差函数;同时也不能对日期做字符串比较后加一,跨月跨年会直接错。

边界有三处:整张表只有一行时没有任何一对能配上,结果为空集,这是合法输出而不是异常;温度相等的两天不算上升,条件必须是严格大于;某一天没有前一天记录时,它自然不会出现在连接结果里,无需额外处理。

解法:SQL 查询建模

核心思路

把全表拉到应用层排序也能处理,但除了比较温度,还必须校验两行日期恰好相差一天;它增加了数据传输和应用代码,而这条跨行关系可以直接交给数据库完成。

关键观察是:「今天」和「昨天」是同一张表的两行。把 Weather 起别名 todayyesterday,一次自连接就把原本分散在两行里的日期和温度放到同一行中比较。

查询不变量是:连接结果中的每一行都恰好代表一对日历日期相隔一天的记录,today 是较晚日期,yesterday 是较早日期。因此温度条件比较的一定是今天与真正的前一天。

把「差一天」放进 ON 而不是 WHERE,是因为它属于「哪两行该配对」的配对规则;把温度比较放进 WHERE,是因为它属于「配好的对里保留哪些」的过滤规则。对 INNER JOIN 而言两者结果等价,但按语义分开写,读起来才是「先配对、再筛选」。

日期配对写成 yesterday.recordDate = DATE_SUB(today.recordDate, INTERVAL 1 DAY):对每个今天精确计算日历上的前一天,再按日期等值连接。日期缺失时匹配不到,跨月跨年则由日期函数正确处理;同时被连接的 yesterday.recordDate 保持为裸列,存在索引时可以用于查找。

解题步骤

  • 确定输出粒度:结果一行对应一个升温的 today,所以只选择 today.id
  • 让表自连接today 表示待输出的今天,yesterday 表示它的前一天。
  • 写配对条件yesterday.recordDate = DATE_SUB(today.recordDate, INTERVAL 1 DAY)。它匹配的是日历上恰好相差一天,而不是排序后相邻或 id 相邻。
  • 写过滤条件today.temperature > yesterday.temperature。严格大于才表示升温,持平不能输出。
  • 确认无需去重recordDate 唯一,所以每个 today 最多匹配一个 yesterday
  • 确认无需排序:题目明确说明结果顺序任意,不加 ORDER BY 可以省掉一次排序开销。

以下面这组用例走一遍。表内四行:(1, 2015-01-01, 10)(2, 2015-01-02, 25)(3, 2015-01-03, 20)(4, 2015-01-04, 30)

自连接按日期条件留下三对:today=2 / yesterday=1today=3 / yesterday=2today=4 / yesterday=3id=1 因为没有 2014-12-31 的记录,找不到前一天并自然被排除。

再比较温度:25 > 10,输出 id=2;20 > 25 不成立;30 > 20,输出 id=4

最终结果为 {2, 4}

再换一组带缺口的数据验证配对规则:(1, 2015-01-01, 10)(2, 2015-01-05, 40)。两行日期相差 4 天,ON 条件不成立,连接结果为空,最终输出空集——尽管温度确实从 10 涨到了 40,但它们不是相邻两天,正确地没有被计入。

代码实现

-- today 与日历上的前一天等值连接,再比较温度。
SELECT today.id
FROM Weather AS today
JOIN Weather AS yesterday
ON yesterday.recordDate = DATE_SUB(today.recordDate, INTERVAL 1 DAY)
WHERE today.temperature > yesterday.temperature;

复杂度分析

  • 时间复杂度:有 recordDate 索引时,每个今天可做一次索引等值查找,约为 $O(n \log n)$;没有可用索引时,执行计划最坏可能退化为 $O(n^2)$ 扫描。
  • 空间复杂度:查询无需分组、排序或窗口缓冲,流式执行时额外空间为 $O(1)$;具体仍取决于连接计划。

关键点总结

  • 只要题目要求「本行与另一行比较」,第一反应就该是自连接:SQL 无法在单行视角看到别的行,把表和自己连起来是把跨行关系拍平成同行关系的通用手段。
  • 配对条件放 ON、筛选条件放 WHERE,即便在 INNER JOIN 下结果相同,也要按语义分开——换成 LEFT JOIN 时两者不再等价,养成习惯能避免以后踩坑。
  • 日期运算一律走日期函数,不要退化成整数加减或字符串比较,跨月跨年是这类写法的必然反例。
  • 「相邻」在数据里未必是「连续」,不能假设日期没有缺口,这是本题拒绝「排序后比较相邻行」思路的根本原因。
  • 将函数放在已知的 today.recordDate 一侧、让被查找的 yesterday.recordDate 保持裸列,能给索引等值查找留下机会。

易错点总结

  • 把日期当整数写 today.recordDate = yesterday.recordDate + 12015-01-312015-02-01 的跨月边界不能靠数值加一表达,应使用 DATE_SUBDATE_ADD
  • 日期方向写反:若写成 today.recordDate = DATE_SUB(yesterday.recordDate, INTERVAL 1 DAY)today 实际成了较早的一天,温度比较也随之反向。
  • 把比较写成 >=:用例 (1, 01-01, 20)(2, 01-02, 20),温度持平不算上升,>= 会多输出 id=2
  • 输出 yesterday.id 而不是 today.id(1,01-01,10)(2,01-02,25) 会返回 1,正确答案是升温当天的 id 2。
  • 误用 LEFT JOIN 且把温度条件也写进 ON:没有前一天或没有升温的 today 仍会被保留,若外层不再过滤就会输出不合格日期。
  • ORDER BY recordDate 后比较相邻行(如用 LAG 但不校验日期差):用例 (1, 01-01, 10)(2, 01-05, 40),排序后 id=2 的上一行是 id=1LAG 认为温度从 10 升到 40 而输出 id=2,但这两天相隔 4 天,正确答案是空集。
  • 给两个别名起了相同的名字或干脆不起别名FROM Weather, Weather 会报「表名重复」,而只写一个 Weather 则无法引用两行,WHERE temperature > temperature 恒为假返回空集。
  • id 相差 1 配对:id 只保证唯一,不代表日期连续;id=1 的 01-01 与 id=2 的 01-05 不能比较为相邻两天。
  • 漏掉日期配对条件:查询会在所有日期的笛卡尔积上比较温度,既产生重复 id,也会比较相隔多天的记录。

相似题目

题目 难度 考察点
181. 超过经理收入的员工 简单 同样是自连接,但配对条件来自显式外键 managerId,不需要构造日期关系
182. 查找重复的电子邮箱 简单 跨行关系用 GROUP BY + HAVING COUNT(*) > 1 表达,比自连接更省一次扫描
196. 删除重复的电子邮箱 简单 自连接的结果不是查询而是 DELETE,要处理「保留 id 最小那行」的不对称条件
180. 连续出现的数字 中等 需要三表自连接凑出连续三行,考察把「相邻」推广到「连续 k 个」
176. 第二高的薪水 中等 重点在无结果时必须返回 NULL 而非空集,考察子查询与 LIMIT OFFSET
178. 分数排名 中等 并列不跳号的排名,考察 DENSE_RANK 与自连接计数两种写法的取舍
184. 部门工资最高的员工 中等 分组内取最大值且允许并列,考察相关子查询与 IN (SELECT ...) 的组合