滑动窗口去重本质是保留每组最新一条记录,通过row_number()按分组字段和时间字段排序编号后取rn=1实现;需建匹配的复合索引并确保排序字段符合业务语义。

滑动窗口去重的本质是“保留每组内最新的一条”
PostgreSQL 没有原生的 DEDUPLICATE OVER 语法,所谓“滑动窗口去重”,实际是利用窗口函数对数据排序后打序号,再用外部过滤保留每组第一条(或最后一条)。关键不在于“滑动”,而在于按业务逻辑定义“一组”——通常是按某个分组字段(如 user_id)+ 时间/顺序字段(如 created_at 或 id)联合判断“最新”。
ROW_NUMBER() 是最稳妥的选择
用 ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...) 给每组内的行编号,然后取 rn = 1 即可。它严格按 ORDER BY 确定唯一顺序,不会因重复值导致序号不可控。
- ✅ 推荐写法:
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY user_id ORDER BY created_at DESC, id DESC ) AS rn FROM events ) t WHERE rn = 1; - ❌ 避免用
RANK()或DENSE_RANK():当created_at相同时,它们会给多行分配相同排名,导致去重失效 - ⚠️ 注意
NULL:如果created_at可为空,ORDER BY created_at DESC会把NULL排最前(默认行为),可能误选脏数据;显式加NULLS LAST更安全
性能和索引必须匹配窗口定义
窗口函数本身不走索引,但 PARTITION BY 和 ORDER BY 字段的组合,正是最佳复合索引的依据。没索引时,大表上 ROW_NUMBER() 可能触发全表排序,I/O 和内存开销陡增。
- 对应上面例子,建索引:
CREATE INDEX idx_events_user_time ON events (user_id, created_at DESC, id DESC);
- 如果查询只按
user_id分组、但总想取最新一条,却只在user_id上建单列索引,那ORDER BY created_at DESC仍要排序,索引利用率低 - 时间字段类型影响排序效率:用
TIMESTAMP WITH TIME ZONE比字符串存时间快且可靠;避免在ORDER BY中用函数(如TO_CHAR(created_at, 'YYYY-MM-DD')),否则索引失效
去重逻辑必须和业务语义对齐
“最新”不等于“最大 ID”或“最晚时间戳”。比如事件表里可能有重试记录,id 大但业务时间早;或者软删除标记未清理,created_at 相同但需按 updated_at 判定。
- 务必确认主排序字段是否真正反映业务优先级,必要时加入二级排序兜底(如
ORDER BY updated_at DESC, id DESC) - 如果存在并发写入,且依赖“插入顺序”去重,注意 PostgreSQL 的
ctid或xmin不稳定,不能用于生产级去重逻辑 - 临时去重可加
LIMIT测试,但正式 SQL 中不要依赖无ORDER BY的LIMIT——结果不可重现
窗口函数本身不维护状态,“滑动”是伪概念;真正要小心的是分组键是否覆盖全部业务维度、排序字段是否包含足够区分度、以及索引是否真正生效——这三点漏掉任一个,查出来的“去重结果”就只是看起来对。











