获取 multi-participant 个对话以及每个对话的最后一条消息

Fetch multi-participant conversations with last message for each

我正在尝试创建一个简单的聊天应用程序数据库模式,并查询对话。我当前的 table 设置如下:

CREATE TABLE chat_user (
    id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    display_name VARCHAR(140),
    ... other user stuff ...
);

CREATE TABLE conversation (
    id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    title VARCHAR(140),
    created timestamp with time zone NOT NULL
);

CREATE TABLE conversation_message (
    id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    conversation_id bigint NOT NULL,
    sender_id bigint NOT NULL,
    body TEXT NOT NULL,
    created timestamp with time zone NOT NULL
);

CREATE TABLE conversation_participant (
    id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    conversation_id bigint NOT NULL,
    user_id bigint NOT NULL
);

所以基本上每个对话都有自己的标题和多个参与者。我想获取按对话中最新消息的日期排序的对话(因此首先显示包含最新消息的对话)。结果集应该包含对话的id,标题和参与者列表+最新消息的id,sender_id和body。

还需要获取根据对话的创建日期分页的对话(每页 20 个)

我的 table 设置是否足够有效以满足上述限制条件?在我看来,这可能会导致具有多个子查询的相当大的查询?

这回答了问题的原始版本。

您似乎想要 join 和聚合:

select cm.conversation_id, max(created)
from conversation_message cm join
     conversation_participant cp
     on cm.conversation_id = cp.conversation_id
where cp.user_id = ?
group by cm.conversation_id
order by max(created) desc;

您可以尝试使用 lateral join

所以您的查询看起来像这样。 您可以获取所有需要的数据,应用限制和偏移量,并检索每个对话的最后一条消息。希望对你有帮助。

select * from conversation c
left join lateral (
    select * from conversation_message cm 
       where cm.conversation_id=c.id
       order by created desc 
       limit 1
 ) cm on true
 left join conversation_participant cp on cp.id = cm.sender_id;

此处的左连接适用于没有任何消息的聊天室。

要获取对话的最新消息,有多种方法可以实现,例如自连接或 window 函数(row_number()、rank() 等)。使用 window 函数,您可以将查询写为

with cm as (
  select *,
  rank() over (partition by conversation_id order by created desc) as r
  from conversation_message
)

select  c.id, 
        c.title, 
        cm.body,
        cm.created,
        cm.r,
        cu.display_name
from conversation as c
left join cm on c.id = cm.conversation_id and cm.r  <= 1
left join chat_user cu on cu.id = cm.sender_id

DEMO

在上面的查询中,我使用左联接来包含没有消息的转换,如果您只需要有消息的对话,则使用内部联接。如果每次对话更改需要 1 条以上的最新消息 cm.r <= @no

要获取每个对话的参与者列表,您可以添加新的 CTE,例如

with cm as (
  select *,
    rank() over (partition by conversation_id order by created desc) as r
  from conversation_message
),
 message_participants as (
  select
    m.conversation_id,
    array_agg(u.display_name order by m.created desc) as participants
  from chat_user as u
  join conversation_message as m on u.id = m.sender_id
  group by m.conversation_id
)

select  c.id, 
        c.title, 
        cm.body,
        cm.created,
        cm.r,
        cu.display_name,
        cmp.participants
from conversation c
left join cm on c.id = cm.conversation_id and cm.r  <= 1
left join chat_user cu on cu.id = cm.sender_id
left join message_participants cmp on c.id = cmp.conversation_id

DEMO

改进

  • conversation中添加user_id table标识谁创建了 这段对话。

  • Table conversation_participant 是多余的,你可以提取 来自 conversation_message

    的参与者名单

简而言之: 我认为您对 normalized (3NF) OLTP 数据库的设计是合理的。这就是您应该瞄准的目标,而不是特定用例的 JOIN 数量。您拥有的设计将满足您定义的用例和许多其他用例,我确信这些用例涉及您的这个应用程序。

详情: 您正在设计一个 OLTP 系统,其中数据保持规范化以确保数据一致性并提高 OLTP 事务的效率。

然而,这意味着您必须做的 JOIN 比 de-normalized 数据库多得多(这对 OLAP、报告、分析系统来说更重要 table)。这就是 OLTP 的本质 relational databases.

尝试减少规范化数据库中 JOIN 的数量(即 3NF - Third normal form)意味着您将把来自不同粒度的数据合并到相同的 table 并导致重复,从而使更新变得更加困难和缓慢,最终导致数据不一致。

所以,你真的不应该以减少 JOIN 的数量为目标进行设计。相反,请确保您拥有标准化设计并避免 over-normalizing。在某些情况下,您可能希望避免编写长查询,您可以添加 VIEWS 并使用视图来编写查询以简化您的查询(但这有时会导致 sub-optimal 查询性能,因为它会带来不必要的连接)。