返回

SQL 找出两个表中未出现的最小数值(三种方法)

mysql

找出两个 SQL 表中未出现的最小数值

咱今天要聊的是一个数据库查询的难题:如何从两个表里找出没有出现的最小数值。举个例子,表 1 里有 1、2、3、8,表 2 里有 4、10、11,咱想找出 5 这个数,因为它是两个表中都没出现的最小正整数。

一、问题难在哪儿?

直接想可能有点绕,你会发现简单的查询只能搞定单个表。 要想找出两个表里共同缺失的最小数字,就有点棘手了。 初始的代码虽然尝试找到缺失值,却无法保证它是两个表里都没出现的最小数,它找的是一个表里没出现而可能出现在另一表的数字。

二、解决思路:合、并、查!

咱的核心思路就三个字:合、并、查!

  1. 合: 把两个表的数据先合并到一起。
  2. 并: 把合并后的数据整理成一个包含所有可能缺失数值的序列。
  3. 查: 从这个序列里找出最小的、且不在两个表中出现的那个数。

三、实战演练:三种方法

下面,咱就一步步来实现这个思路,而且提供三种不同的方法,各有千秋。

1. 方法一:UNION ALL + GENERATE_SERIES (PostgreSQL)

这种方法利用了PostgreSQL的 GENERATE_SERIES 函数,它可以生成一个数字序列,非常适合咱们的需求。

原理:

  • UNION ALL:把两个表的目标列合并成一个结果集,不去重。
  • GENERATE_SERIES(start, stop): 产生从 startstop 的一系列整数。
  • LEFT JOIN:找到序列中缺失的数字。
  • MIN():返回最小缺失值。

代码:

WITH CombinedValues AS (
    SELECT columna AS value FROM Table1
    UNION ALL
    SELECT columnb AS value FROM Table2
),
MaxValues AS (
     SELECT GREATEST((SELECT MAX(columna) FROM Table1),(SELECT MAX(columnb) FROM Table2)) as max_value
),
Series AS (
    SELECT generate_series(1, (SELECT max_value from MaxValues)) AS num
)
SELECT MIN(s.num)
FROM Series s
LEFT JOIN CombinedValues cv ON s.num = cv.value
WHERE cv.value IS NULL;

解释:

  1. CombinedValues CTE:将 Table1columnaTable2columnb 合并。
  2. MaxValues CTE:找出两个表中各自的最大值。
  3. Series CTE: 通过两个表中找出的各自最大值,再用 GREATEST函数找到两者中更大的那一个作为 GENERATE_SERIES的stop参数。
  4. 最后的主查询:用 LEFT JOINSeriesCombinedValues 关联,WHERE cv.value IS NULL 筛选出 Series 中存在但 CombinedValues 中不存在的数字,最后用 MIN() 找出最小的那个。

安全性建议: 无特别安全风险。

进阶用法: 可以灵活调整GENERATE_SERIES中的起始和结束数字来适应各种场景。如果确定缺失的值在一个更小的范围内, 可以手动缩小范围提高效率。

2. 方法二:UNION ALL + 递归 CTE (通用方法)

这种方法利用递归 CTE 生成一个数字序列,兼容性更好,适用于多种数据库系统。

原理:

  • 递归公用表表达式 (CTE): 建立一个基础的 case(第一个数字)。 并在循环里每次把这个数字 +1 直到循环条件达成(大于最大值)停止.

代码:

WITH RECURSIVE NumberSeries AS (
    SELECT 1 AS num
    UNION ALL
    SELECT num + 1
    FROM NumberSeries
    WHERE num < (SELECT GREATEST(MAX(columna), (SELECT MAX(columnb) FROM Table2)) FROM Table1)
),
CombinedValues AS (
    SELECT columna AS value FROM Table1
    UNION ALL
    SELECT columnb AS value FROM Table2
)
SELECT MIN(num)
FROM NumberSeries
WHERE num NOT IN (SELECT value FROM CombinedValues);

解释:

  1. NumberSeries CTE:递归生成从 1 开始的数字序列,上限是两个表中的最大值。
  2. CombinedValues CTE:同上,合并两个表的数据。
  3. 主查询:从 NumberSeries 中找出不在 CombinedValues 中的最小数字。

安全性建议: 无特别安全风险。

进阶用法: 可以通过调整递归 CTE 中的初始值和递增步长来控制生成的序列。 如果最小值绝对不会从 1 开始, 可以改动 SELECT 1 AS num 来缩短循环时间。

3. 方法三:NOT EXISTS (更简洁的方法)

这个办法不用生成数字序列,直接通过NOT EXISTS来排除两个表内的已有数值,寻找第一个符合条件的最小值。

原理:

从1开始循环测试每个数字是不是在两个表内. 用NOT EXISTS找到符合条件的数字,循环达成条件是两个NOT EXISTS都通过.

代码:

SELECT MIN(num)
FROM (SELECT 1 AS num
      UNION ALL
      SELECT num + 1
      FROM (SELECT 1 AS num) t --这个 FROM + (SELECT xxx) 组合只是确保有个表, 不让 sql 报错而已.
      WHERE num < (SELECT GREATEST((SELECT MAX(columna) FROM Table1), (SELECT MAX(columnb) from Table2)))
     ) n
WHERE NOT EXISTS (SELECT 1 FROM Table1 WHERE columna = n.num)
  AND NOT EXISTS (SELECT 1 FROM Table2 WHERE columnb = n.num);

解释:
子查询 (最里面的那个SELECT 1 as num),我们得到一个包含初始值 1 的集合。
外部的SELECT num + 1 创建下一个数字(利用内部表的num),外部通过WHERE 设置循环最大值,即两个表中最大数值。
然后最外部,对所有n内的数字都测试一轮。通过则表示找到符合要求的数值.
MIN取最小值.

安全性建议: 无特别安全风险。

进阶用法: 此方法避免了完整的序列生成,在某些情况效率也许不错(如果最小缺失值很小)。 如果已知缺失值靠近理论最小值,则表现会更好.

总结

这三种方法各有侧重。第一种在PostgreSQL里头用着方便。第二种有更好通用性,大多数数据库都能跑。第三种最简短,如果符合条件的值出现在较小的范围,可以更快的找到答案. 要用哪个,看你具体的情况来定就好!