PAYATHON 2026

PostgreSQL 如何按名称和原始顺序合并连续索引范围?

数据小许

结论

先按 WITH ORDINALITY 生成的原始行号排序,再用 LAG 检查当前名称与上一行是否不同。对变化标记累计求和,就能给每段连续记录分配组号。最后按组取索引的最小值和最大值。

应比较相邻行。按 name 排序会打乱输入顺序,也会合并原本分开的同名记录。IS DISTINCT FROM 可以正确比较包含 NULL 的名称。

查询

WITH dat AS (
  SELECT name, idx
  FROM unnest(ARRAY[
    NULL, NULL,
    'foo', 'foo', 'foo',
    NULL, NULL, NULL,
    'bar', 'bar',
    NULL,
    'foo', 'foo'
  ]) WITH ORDINALITY AS _(name, idx)
), marked AS (
  SELECT
    name,
    idx,
    CASE
      WHEN name IS DISTINCT FROM LAG(name) OVER (ORDER BY idx) THEN 1
      ELSE 0
    END AS starts_group
  FROM dat
), grouped AS (
  SELECT
    name,
    idx,
    SUM(starts_group) OVER (
      ORDER BY idx
      ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS group_id
  FROM marked
)
SELECT
  name,
  CASE
    WHEN MIN(idx) = MAX(idx) THEN MIN(idx)::text
    ELSE MIN(idx)::text || '-' || MAX(idx)::text
  END AS index_range
FROM grouped
GROUP BY group_id, name
ORDER BY MIN(idx);

结果中的范围按示例格式显示为 1-2、3-5;只有一个索引时显示为 11。每段连续记录都有自己的 group_id,因此后面再次出现的 foo 会形成新组,连续出现的 NULL 则归为一组。

如果需要 PostgreSQL range 类型

如果下游需要可计算的范围值,可以将选择表达式替换为:

int8range(MIN(idx), MAX(idx) + 1, '[)') AS index_range

这种左闭右开的表示法会将索引 1 到 2 表示为 [1,3)。PostgreSQL 的 range 上界不包含在范围内。

注意事项

idx 来自 WITH ORDINALITY,表示输入行的顺序。如果实际数据来自表,应使用能够确定顺序的列替代它。没有稳定顺序,连续分组的结果也无法稳定确定。文本形式的 index_range 适合展示;进行范围运算时,使用 int8range 结果更合适。

备注:内容仅供参考。