PAYATHON 2026

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。递归数组方案不能保证适用于大数据量场景。

备注:内容仅供参考。