PostgreSQL 如何按行顺序对多列执行并集式去重?
数据阿明
结论
可以用递归 CTE 按指定顺序逐行扫描,并分别记录已接受行中的 col1 和 col2 值。只有当前行的两个值都没有出现过,才接受这一行。这样可以保留原始行的配对关系,不会把不同原始行中的值重新组合成一行。
这是贪心筛选规则:接受一行后,该行的两个值都会占用名额。之后只要任一列的值重复,该行就会被丢弃。结果取决于输入顺序,也不保证得到匹配数量最多的结果。
PostgreSQL 示例
以下查询假设 t 有一个名为 id 的列,用来确定行顺序。请将 id 换成实际的排序列。如果表中没有能确定顺序的列,需要先定义排序规则,因为关系表本身不保证行顺序。
WITH RECURSIVE numbered AS (
SELECT
row_number() OVER (ORDER BY id) AS rn,
col1,
col2
FROM t
),
walk (rn, col1, col2, seen_col1, seen_col2, keep_row) AS (
SELECT
rn,
col1,
col2,
ARRAY[col1],
ARRAY[col2],
true
FROM numbered
WHERE rn = 1
UNION ALL
SELECT
n.rn,
n.col1,
n.col2,
CASE
WHEN array_position(w.seen_col1, n.col1) IS NULL
AND array_position(w.seen_col2, n.col2) IS NULL
THEN array_append(w.seen_col1, n.col1)
ELSE w.seen_col1
END,
CASE
WHEN array_position(w.seen_col1, n.col1) IS NULL
AND array_position(w.seen_col2, n.col2) IS NULL
THEN array_append(w.seen_col2, n.col2)
ELSE w.seen_col2
END,
array_position(w.seen_col1, n.col1) IS NULL
AND array_position(w.seen_col2, n.col2) IS NULL
FROM walk AS w
JOIN numbered AS n
ON n.rn = w.rn + 1
)
SELECT col1, col2
FROM walk
WHERE keep_row
ORDER BY rn;
array_position 比较数组元素时使用与 IS NOT DISTINCT FROM 相同的语义,因此也能识别数组中已记录的 NULL。如果当前行任一列的值重复,查询会保留原有的已用值数组。只有整行被接受时,才会把两个值都追加进去。
按题目所示的输入顺序扫描,结果为 (a, 1)、(f, 3)、(d, 4)、(b, 2) 和 (g, NULL)。例如,(g, 3) 会因为 3 已出现在前面的 col2 中而被排除,(b, 3) 也会被排除。
顺序和性能
row_number() 必须使用明确且稳定的排序条件。如果排序列有重复值,还应增加次级排序列来确定行的先后顺序。没有排序键时,不能依赖数据库返回的行顺序。
这种数组写法容易表达规则,也能处理 NULL,但每一步都要检查和更新已见值数组。数据量较大时,数组查找和复制可能开销较高。可以改用按顺序处理数据的过程,并通过带索引的已见值表记录两列的值,同时明确处理 NULL。递归数组方案不能保证适用于大数据量场景。
备注:内容仅供参考。
