按重复属性分组
基本上我有一个表messages
, user_id
字段标识创建消息的用户。
当我在两个用户之间显示一个对话(一组消息)时,我希望能够通过user_id
对消息进行分组,但这是一种棘手的方式:
假设有一些消息(按created_at desc
排序):
id: 1, user_id: 1 id: 2, user_id: 1 id: 3, user_id: 2 id: 4, user_id: 2 id: 5, user_id: 1
我希望按以下顺序获得3个消息组: [1,2], [3,4], [5]
它应该按* user_id *分组,直到它看到另一个,然后按那个分组。
我正在使用PostgreSQL,并乐于使用特定的东西,无论什么能带来最佳性能。
适当的SQL
@Igor提供了一个很好的带有窗口函数的纯SQL技术。
然而:
我希望按以下顺序获得3个消息组:[1,2],[3,4],[5]
要获取请求的订单,请添加ORDER BY min(id)
:
SELECT array_agg(id) AS ids FROM ( SELECT id ,user_id ,row_number() OVER (ORDER BY id) - row_number() OVER (PARTITION BY user_id ORDER BY id) AS grp FROM messages ORDER BY id) t -- for ordered arrays in result GROUP BY grp, user_id ORDER BY min(id);
SQL小提琴。
这一补充几乎无法得到另一个答案。 更重要的问题是:
使用PL / pgSQL更快
我正在使用PostgreSQL,并乐于使用特定的东西,无论什么能带来最佳性能 。
纯SQL既漂亮又shiny,但程序服务器端function对于此任务来说要快得多。 虽然在程序上处理行通常较慢 ,但plpgsql赢得此竞争的重要时间,因为它可以使用单个表扫描和单个 ORDER BY
操作:
CREATE OR REPLACE FUNCTION f_msg_groups() RETURNS TABLE (ids int[]) AS $func$ DECLARE _id int; _uid int; _id0 int; -- id of last row _uid0 int; -- user_id of last row BEGIN FOR _id, _uid IN SELECT id, user_id FROM messages ORDER BY id LOOP IF _uid <> _uid0 THEN RETURN QUERY VALUES (ids); -- output row (never happens after 1 row) ids := ARRAY[_id]; -- start new array ELSE ids := ids || _id; -- add to array END IF; _id0 := _id; _uid0 := _uid; -- remember last row END LOOP; RETURN QUERY VALUES (ids); -- output last iteration END $func$ LANGUAGE plpgsql;
呼叫:
SELECT * FROM f_msg_groups();
基准和链接
我在一个类似60k行的真实生活表上运行了EXPLAIN ANALYZE
的快速测试(执行几次,选择最快结果以排除兑现效果):
SQL:
总运行时间:1009.549毫秒
PL / pgSQL的:
总运行时间: 336.971 ms
还要考虑这些密切相关的问题:
- GROUP BY和聚合顺序数值
- GROUP BY由间隙分隔的连续日期
- 连续重复/重复的有序计数
尝试这样的事情:
SELECT user_id, array_agg(id) FROM ( SELECT id, user_id, row_number() OVER (ORDER BY created_at)- row_number() OVER (PARTITION BY user_id ORDER BY created_at) conv_id FROM table1 ) t GROUP BY user_id, conv_id;
表达方式:
row_number() OVER (ORDER BY created_at)- row_number() OVER (PARTITION BY user_id ORDER BY created_at) conv_id
将为每个消息组提供一个特殊的id(对于其他user_id
,可以重复此conv_id
,但user_id, conv_id
将为您提供所有不同的消息组)
我的SQLFiddle与示例。
详细信息: row_number()
, OVER (PARTITION BY ... ORDER BY ...)
GROUP BY
子句将折叠2条记录中的响应 – 一条带有user_id
1,另一条带有user_id
2,无论ORDER BY
子句如何,所以我建议你只发送ORDER BY created_at
prev_id = -1 messages.each do |m| if ! m.user_id == prev_id do prev_id = m.user_id #do whatever you want with a new message group end end
你可以使用chunk :
Message = Struct.new :id, :user_id messages = [] messages << Message.new(1, 1) messages << Message.new(2, 1) messages << Message.new(3, 2) messages << Message.new(4, 2) messages << Message.new(5, 1) messages.chunk(&:user_id).each do |user_id, records| p "#{user_id} - #{records.inspect}" end
输出:
"1 - [#, # ]" "2 - [# , # ]" "1 - [# ]"
- 在Hartl的ruby-on-rails教程的第11章中,“没有路由匹配”错误删除了Micropost – 完全难倒
- rails ActiveAdmin嵌套表单has_one accepts_attributes_for formtastic issue