Rails 8+PostgreSQL中如何用DISTINCT ON实现线程按最新消息降序排序?
问题背景
我们有一个MarketplaceMessage模型,每条消息的initial_record_id字段规则是:首条消息的该字段等于自身ID,回复消息则等于线程首条消息的ID。需要实现:展示每个线程的最新消息,且按这些最新消息的ID降序排列。
当前用DISTINCT ON的scope能拿到每个线程的最新消息,但结果是按initial_record_id升序排列的,直接修改排序会触发PostgreSQL的错误:PG::InvalidColumnReference: ERROR: SELECT DISTINCT ON expressions must match initial ORDER BY expressions,这是因为PostgreSQL要求DISTINCT ON指定的字段必须是ORDER BY的第一个字段。
解决方案1:子查询+外层排序(贴近Rails风格)
把原有的DISTINCT ON查询作为子查询,在外层对结果按最新消息ID降序排列:
scope :distinct_threads, -> { from( select("DISTINCT ON (initial_record_id) *") .order("initial_record_id, id desc"), :latest_thread_messages ).order(id: :desc) }
原理:先通过子查询按initial_record_id分组,拿到每个组内ID最大的消息(即最新消息),然后外层对这些结果按id降序排序,既满足PostgreSQL的语法要求,又实现了最终的排序需求。
解决方案2:窗口函数实现(更灵活)
用PostgreSQL的窗口函数ROW_NUMBER()给每个线程内的消息排序,筛选出每个线程的第一条(最新)消息,再按ID降序排列:
scope :distinct_threads, -> { select("*") .from( select("*, ROW_NUMBER() OVER (PARTITION BY initial_record_id ORDER BY id desc) as rn") .from(:marketplace_messages), :ranked_messages ) .where(rn: 1) .order(id: :desc) }
原理:PARTITION BY initial_record_id按线程分组,ORDER BY id desc让每个线程内最新的消息排在前面并标记rn=1,筛选出rn=1的记录就是每个线程的最新消息,最后按id降序排列即可。
这两种方式都能得到你想要的结果:
- Message id: 12, initial_record_id: 3
- Message id: 11, initial_record_id: 4
- Message id: 10, initial_record_id: 10
内容的提问来源于stack exchange,提问作者sethherr

