28

我为用户创建了一个消息系统,它允许他们向另一个用户发送消息。如果这是他们第一次说话,则开始新的对话,如果不是,则继续旧的对话。

用户收件箱列出了用户与所有其他用户的所有对话,然后按其中包含最新帖子的对话排序。

一个用户只能与另一个用户进行一次对话。

当用户单击其中一个对话时,他们会被带到一个页面,该页面显示他们进行过的整个对话,最新帖子位于顶部。所以它有点像消息聊天功能。

我有两张桌子:

  • 用户对话
  • 用户留言

用户对话

包含一个自动增量 id,它是对话 id,以及 userId 和friendId。

发起第一个对话的人将始终是 userId 和接收者friendId,那么该对话将永远不会改变。

+----+--------+----------+
| id | userId | friendId |
+----+--------+----------+

用户留言

包含特定消息,以及读取标志、时间和会话 ID

+----+---------+--------+------+------+----------------+
| id | message | userId | read | time | conversationId |
+----+---------+--------+------+------+----------------+

这个怎么运作

当用户向另一个用户发送消息时,将运行查询以检查两个用户是否在 userconversation 表中具有匹配项,如果使用了匹配项conversationId并继续对话,如果没有为他们创建具有唯一性的新行conversationId

复杂的地方

到目前为止一切都很好,但是在显示所有对话的消息收件箱时,按最新帖子排序,使用一个查询会变得很棘手。

为了能够列出对话,您必须首先找到每个对话的最新帖子,但是由于您不能在组之前排序,因此不可能对两个表进行一次查询,因此我必须使用以下内容:

SELECT  
    c.id,
    c.userId,
    c.friendId,
    m2.message,
    m2.read,
    UNIX_TIMESTAMP(m2.time),      
    user1.username,
    user2.username  
FROM 
    (SELECT MAX(m1.id) AS MessageID 
     FROM usermessages m1 
     GROUP BY m1.conversationId) latest_msg

INNER JOIN usermessages m2 ON latest_msg.MessageID = m2.id 
INNER JOIN userconversation c ON m2.conversationId = c.id
INNER JOIN user user1 ON c.userId = user.id
INNER JOIN user user2 ON c.friendId = user.id

WHERE c.userId = :userId OR c.friendId = :userId
ORDER BY m2.id DESC
LIMIT 10

我只是不认为这是可以做到的最好方法,但想不出其他方法也可以做到吗?

数据库表是 InnoDB 以加快连接并提高数据完整性,因此我不能有两个自动增量行。

有没有另一种方法可以摆脱 userconversation 表并创建一个唯一的 Id 放入 conversationId 列?然后我可以将 userId 和friendId 移到 usermessages ...但这会产生很多冗余数据?

4

13 回答 13

6

嗯,也许我没有正确理解你的问题......但对我来说,解决方案很简单:

SELECT c.*, MAX(m.time) as latest_post 
FROM conversations as c 
INNER JOIN messages as m ON c.id = m.conversation_id
WHERE c.userId = 222 OR c.friendId = 222 
GROUP BY c.id
ORDER BY latest_post DESC

这是我的测试数据:

对话:

id  userId  friendId
1   222     333
2   222     444

留言:

id  message     time (Desc)     conversation_id
14  rty     2012-05-14 19:59:55     2
13  cvb     2012-05-14 19:59:51     1
12  dfg     2012-05-14 19:59:46     2
11  ert     2012-05-14 19:59:42     1
1   foo     2012-05-14 19:22:57     2
2   bar     2012-05-14 19:22:57     2
3   foo     2012-05-14 19:14:13     1
8   wer     2012-05-13 19:59:37     2
9   sdf     2012-05-13 19:59:24     1
10  xcv     2012-05-11 19:59:32     2
4   bar     2012-05-10 19:58:06     1
6   zxc     2012-05-08 19:59:17     2
5   asd     2012-05-08 19:58:56     1
7   qwe     2012-05-04 19:59:20     1

查询结果:

id  userId  friendId    latest_post
2   222     444     2012-05-14 19:59:55
1   222     333     2012-05-14 19:59:51

如果不是这样......请忽略我的回答:P

希望这可以帮助

于 2012-05-14T18:16:20.240 回答
4

如果您正在寻求一种能够保留所有当前功能和工作流程的方法,同时将数据保留在一个表中,我认为您已经非常接近了。

与其让conversationIdkey 指向不同的表,不如让它指向开始对话的消息的 ID。这将在开始对话的消息和随后的所有消息之间创建父子关系。为了能够查看所有对话,您只需选择conversationId为空的所有消息。下面是 2 条消息对话的表示:

+----+---------+------+------------------+----------------+--------+----------+
| id | message | read | time             | conversationId | toUser | fromUser |
+----+---------+------+------------------+----------------+--------+----------+
| 1  |  test 1 |  0   | (some timestamp) |  null          |  3     |   4      |
| 2  |  test 2 |  0   | (some timestamp) |   1            |  4     |   3      |
+----+---------+------+------------------+----------------+--------+----------+

对话由用户 3 发起。对话中的所有消息都可以通过 过滤conversationId。这种设计的一个限制是只有 2 个用户可以在对话之外。

更新

您可以通过以下方式获取给定对话 id 的最后一条消息:

SELECT id, message 
FROM userMessages 
WHERE conversationId = {conversationId} 
ORDER BY time DESC 
LIMIT 1
于 2012-05-14T18:03:20.687 回答
3

如果你想简化你的查询,你应该在你的表中添加最后一个消息 id userconversation

ALTER TABLE userconversation ADD lastusermessageid

然后每次添加新消息时,都应该更新表 userconversation :

INSERT INTO userconversation(userId, friendId, lastusermessageid)
VALUES (:userId, :friendId, :lastusermessageid)
ON DUPLICATE KEY UPDATE lastusermessageid = VALUES(lastusermessageid)

最后在所有外键上添加索引:

SELECT  
    c.id,
    c.userId,
    c.friendId,
    m.message,
    m.read,
    UNIX_TIMESTAMP(m.time),      
    user1.username,
    user2.username  
FROM 
    userconversation c
    INNER JOIN usermessages m ON c.lastusermessageid = m.id 
    INNER JOIN user user1 ON c.userId = user.id
    INNER JOIN user user2 ON c.friendId = user.id
WHERE 
    c.userId = :userId OR c.friendId = :userId
ORDER BY
    m.id DESC
LIMIT 10
于 2012-05-17T20:56:34.157 回答
3

由于给定的一对用户最多可以进行一次对话,因此无需“发明”单独的密钥来识别对话。此外,您问题的措辞似乎表明一条消息总是发送给单个用户,所以我可能会采用这样的方式:

在此处输入图像描述

现在,关于这个模型有几点需要注意:

  • 它假定相同的两个用户之间的消息生成的频率不能高于用于 SEND_TIME 的类型提供的分辨率。1
  • 消息的方向不是由 USER1_ID 和 USER2_ID 的顺序决定的,而是有一个单独的标志(DIRECTION)。这样,给定用户之间的消息将始终具有相同的 USER1_ID 和 USER2_ID 组合(由上面的 CHECK 强制执行),无论谁发送和接收消息。这大大简化了查询。
  • 不幸的是,所有的 InnoDB 表都是集群的,所以二级索引I1相对昂贵。有办法解决这个问题,但由此产生的并发症可能不值得。

使用此数据模型,通过最新消息对“对话”(由用户对标识)进行排序变得相当容易。例如(替换1为所需用户的 USER_ID):

SELECT *
FROM (
    SELECT USER1_ID, USER2_ID, MAX(SEND_TIME) NEWEST
    FROM MESSAGE
    WHERE (USER1_ID = 1 OR USER2_ID = 1)
    GROUP BY USER1_ID, USER2_ID
) Q
ORDER BY NEWEST DESC;

OR USER2_ID = 1是二级索引的原因I1。)

如果您不仅想要最新时间,还想要最新消息,您可以执行以下操作:

SELECT * FROM MESSAGE T1
WHERE
    (USER1_ID = 1 OR USER2_ID = 1)
    AND SEND_TIME = (
        SELECT MAX(SEND_TIME)
        FROM MESSAGE T2
        WHERE
            T1.USER1_ID = T2.USER1_ID
            AND T1.USER2_ID = T2.USER2_ID
    )
ORDER BY SEND_TIME DESC;

您可以在SQL Fiddle中使用它。


1如果不是这种情况,您可以改用单调递增的 INT,但您必须SELECT MAX(...)自己使用,因为自动递增不适用于 PK 子集;或者干脆让它单独PK并在 USER1_ID 和 USER2_ID 上都有二级索引(幸运的是,它们会更苗条,因为 PK 更苗条)。

于 2012-05-23T21:00:34.987 回答
3

如果您只能在用户之间进行一次对话,我认为没有理由使用专用对话表。要使此查询快速运行,您将需要一个复合索引,(user, message_id)如果这些字段位于不同的表中,则无法使用该索引。移动到. user_id_ 这将使每条记录的表字节数变重(即使假设为 -byte 标识符),这对于包含文本消息的表来说几乎不是问题。friend_iduserconversations88

如果每个用户的对话很少,每个用户有很多消息,请使用:

SELECT  um.*
FROM    (
        (
        SELECT  MAX(id) AS messageId
        FROM    usermessages m1
        WHERE   user_id = :me
        GROUP BY
                friend_id
        ORDER BY
                messageId DESC
        LIMIT 10
        )
        UNION ALL
        (
        SELECT  MAX(id) AS messageId
        FROM    usermessages m1
        WHERE   frient_id = :me
        GROUP BY
                user_id
        ORDER BY
                messageId DESC
        LIMIT 10
        )
        ) q
JOIN    usermessages um
ON      um.id = q.messageId
ORDER BY
        id DESC
LIMIT 10

user_id在和上创建单独的索引friend_id

如果您有很多对话,每个对话中的消息很少,请使用以下查询:

(
SELECT  *
FROM    usermessages um
WHERE   user_id = :me
        AND id = 
        (
        SELECT  MAX(id)
        FROM    usermessages umi
        WHERE   umi.user_id = um.user_id
                AND umi.friend_id = um.friend_id
        )
ORDER BY
        id DESC
LIMIT 10
)
UNION ALL
(
SELECT  *
FROM    usermessages um
WHERE   frient_id = :me
        AND id = 
        (
        SELECT  MAX(id)
        FROM    usermessages umi
        WHERE   umi.user_id = um.user_id
                AND umi.friend_id = um.friend_id
        )
ORDER BY
        id DESC
LIMIT 10
)
ORDER BY
        id DESC
LIMIT 10

这个查询背后的想法是,它只是对给定用户的所有消息进行下降,检查每条消息是否是其对话中的最后一条。这可能比为所有对话(如果你有很多)排序所有最后的消息要快得多。

为了快速工作,请在

friend_id
user_id, friend_id
于 2012-05-14T18:09:41.340 回答
1

我没有测试过这种方法,因为我现在无法访问 mysqldb。但是,我认为您应该能够通过使用排名功能来完成这项工作。由于 mysql 没有相当于 Oracle 的 row_number 函数,我认为你可以这样做:

Select * from (
Select 
    uc.id, 
    uc.user_id, 
    uc.friend_id 
    um.message
    um.read, 
    um.time,
    @rownum := IF(@prev_val = um.conversation_id, @rownum + 1, 1) AS rank,
    @prev_val := um.conversation_id
From
    userconversation uc,
    usermessages um,
    (select @row_num:=1) rows,
    (select @prev_val:='') partitions
Where 
    uc.id=um.conversation_id        
    and c.userId = 222 OR c.friendId = 222 

Order By 
    um.conversation_id,um.id desc
)t where t.rank=1
于 2012-05-22T12:50:30.373 回答
1

你为什么要把数据分解成对话?

如果是我,我会使用一个名为“usermessages”的表格,格式如下:

+----+--------+----------+-------------+------------+--------+
| id | userto | userfrom | timecreated | timeviewed | message|
+----+--------+----------+-------------+------------+--------+

对话由“userto”和“userfrom”列的组合来标识。因此,当您要选择所有对话时:

SELECT * FROM usermessages 
WHERE (userto = :userto OR userto = :userfrom) 
AND (userfrom = :userfrom OR userfrom = :userto) 
ORDER BY timecreated DESC 
LIMIT 10
于 2012-05-14T18:22:20.713 回答
1

它在 Fiverr.com 和www.infinitbin.com上使用。我自己开发了infinitbin。它也有两个像你一样的数据库。收件箱表:-

+----+--------+----------+-------------+------------+--------------------------------+
| id | useridto | useridfrom | conversation | last_content | lastviewed | datecreated|
+----+--------+----------+-------------+------------+--------------------------------+

此表非常重要,用于列出对话/收件箱。last_content 字段是对话之间最后一条消息的 140 个字符。lastviewed 是一个整数字段,如果会话中的其他用户阅读了该消息,则最后发送消息的用户是最后查看的用户。它被更新为 NULL。因此,要获得通知,您的 lastviewed 不为空,也不是登录用户的 id。

对话字段是'userid-userid',因此是字符串。要检查用户是否已经开始对话,您可以将 user_ids 与连字符连接起来并检查它。

这种消息传递系统是一个非常复杂的系统。

第二张表很简单。

+----+--------+----------+-------------+-------+
| id | inboxid | userid | content | datecreated|
+----+--------+----------+-------------+-------+
于 2012-05-20T10:14:38.230 回答
1

我认为您不需要创建用户对话表。

如果只有用户只能与某人进行一次对话,则此线程的唯一 id 是 userId 和friendId 之间的连接。所以我在 usersmessage 表中移动了friendId 列。顺序问题(friendId-userId是userId-friendId的同一个线程)可以这样解决:

SELECT CONCAT(GREATEST(userId,FriendId),"_",LEAST(userId,FriendId)) AS threadId

现在有一个在 GROUP BY threadId 之后获取最后一条消息的问题。

我认为是一个很好的解决方案,在 DATE 和消息之间以及在该字段上的 MAX 之后进行连接。

为简单起见,我假设列日期是 DATETIME 字段('YYYY-mm-dd H:i:s'),但它不需要,因为有 FROM_UNIXTIME 函数。

所以最后的查询是

SELECT 
        CONCAT(GREATEST(userId,FriendId),"_",LEAST(userId,FriendId)) AS threadId,
        friendId, MAX(date) AS last_date, 
        MAX(CONCAT(date,"|",message)) AS last_date_and_message 

FROM usermessages
WHERE userId = :userId OR friendId = :userId
GROUP BY threadId ORDER BY last_date DESC

字段 last_date_and_message 的结果是这样的:

2012-05-18 00:18:54|Hi my friend this is my last message

它可以简单地从您的服务器端代码中解析出来。

于 2012-05-24T17:34:06.020 回答
1

如何创建一个快速的类似 Facebook 的消息系统。经 Arutz Sheva 用户测试并广泛使用 - http://www.inn.co.il(希伯来语)。

  1. 创建一个“主题”(对话)表:

      CREATE TABLE pb_topics(
       t_idint(11) NOT NULL AUTO_INCREMENT,
       t_lastint(11) NOT NULL DEFAULT '0',
       t_userint(11) NOT NULL DEFAULT '0',
      主键 ( t_id),
      键last( t_last)
    ) 引擎=InnoDB AUTO_INCREMENT=137106342 默认字符集=utf8

  2. 在用户和对话之间创建链接:

        CREATE TABLE pb_links(
       l_idint(11) NOT NULL AUTO_INCREMENT,
       l_userint(11) NOT NULL DEFAULT '0',
       l_newint(11) NOT NULL DEFAULT '0',
       l_topicint(11) NOT NULL DEFAULT '0',
       l_visibleint(11) NOT NULL默认“1”,
       l_bccint(11) 非空默认“0”,l_id使用 BTREE 的
      主键 ( ),
      唯一键topic-user( l_topic, l_user),使用 BTREE 的
      键user-topicnew( l_user, l_new, ),l_topic使用 BTREE的
      键user-topic( l_user, l_visible, )l_topic
    ) 引擎=InnoDB AUTO_INCREMENT=64750078 默认字符集=utf8

  3. 创建消息

        CREATE TABLE pb_messages(
       m_idint(11) NOT NULL AUTO_INCREMENT,
       m_fromint(11) NOT NULL,
       m_datedatetime NOT NULL DEFAULT '1987-11-13 00:00:00',
       m_titlevarchar(75) NOT NULL,
       m_contentmediumtext NOT NULL,
       m_topicint(11)非空,
      主键 ( m_id),
      键date_topic( m_date, m_topic),
      键topic_date_from( m_topic, m_date, m_from)
    ) 引擎=InnoDB

可以与 2 个或更多朋友进行对话(像电子邮件一样添加密件抄送,但您可以跳过它)。

插入新消息: 1. 创建新主题 2. 为用户创建链接(从/到) 3. 添加消息(4. 更新用户缓存表 - 用户有消息)

向主题添加消息:添加消息

选择文件夹:

select 
     z.*, group_concat(u_name) as users_name from
         (select max(m_id) as m_id, m_topic as t_id,  m_From, m_title,m_date, l_new 
              from pb_links as l1, pb_messages 
              where l1.l_user=<user>  and m_from < If(inbox, "<>", "=") > and m_topic=l_topic and l1.l_visible=1 
               group by m_topic order by m_id desc limit " & iPage * 35 & ",35) z
           left join  pb_links l2  on (l2.l_topic=t_id)
           left join  users  on (l_user=u_id and l_bcc=0 and l_user<user>)  
            group by l_topic order by m_date desc;

详细说明:

第一个是内部选择 - 这是最快的方式(我检查了大约 7 个其他选项,也在 Percona/MariaDB 版本中检查)获取所有消息,并获取最后一条显示在列表中的消息。还要查看内部 IF - 在收件箱中,最后一条消息是任何人,但不是我,而在发件箱中 - 相反。LIMIT 用于分页。

外部用于添加用户列表(只是名称逗号名称字符串)和更多信息,每个主题只有一条消息,并且在分页之后(我需要将用户列表添加到每页 35 条消息,而不是我的所有历史悠久)。

另外,我在这里用希伯来语写了:http: //blogs.microsoft.co.il/blogs/moshel/archive/2010/08/12/quot-x-quot.aspx 创建一个简单的缓存表,并禁止工作量从忙碌消息表中选择计数。

于 2012-05-14T18:05:44.200 回答
1

我会这样设置

表详细信息

conversations (#id, last_message_id)

participation (#uid1, #uid2, conversation_id)

messages (#conversation_id, #id, uid, contents, read, *time)

对话

该表将主要用于为每个对话生成一个新的标识符,以及最后一次更新的计算字段(用于优化)。这两个用户已与此表断开连接并移至participation

参与

该表记录了两个用户之间的双向对话;要解释原因,请查看以下键:

ALTER TABLE `table` ADD PRIMARY(uid1, uid2);

虽然这对强制唯一性和简单查找都有好处,但您应该注意以下行为:

  • SELECT * FROM table WHERE uid1=1 AND uid2=2
  • SELECT * FROM table WHERE uid1=1
  • SELECT * FROM table WHERE uid1=1 AND uid2>5
  • SELECT * FROM table WHERE uid2=2

前两个查询执行得非常好,MySQL 还优化了密钥第一部分的身份查找。第三个也产生了相当不错的性能,因为您的密钥的第二部分可用于范围查询。最后一个查询表现不佳,因为索引是“左偏”的,因此它执行全表扫描。

消息

该表存储实际发送的消息,包括会话标识符、发送者 ID、内容、读取标志和发送时间。

手术

发送消息

要确定两个用户之间的对话是否已经建立,您可以简单地查询participation表:

SELECT conversation_id FROM participation WHERE uid1=:sender_id AND uid2=:receiver_id

如果它尚不存在,则创建两条记录:

INSERT INTO conversations (last_message_id) VALUES (NULL);
# fetch last insert id here
INSERT INTO participation VALUES (:sender_id, :receiver_id, :conversation_id), (:receiver_id, :sender_id, :conversation_id);
INSERT INTO messages VALUES (:conversation_id, 0, :sender_id, :message_contents, 0, NOW());
UPDATE conversations SET last_message_id=LAST_INSERT_ID() WHERE id = :conversation_id

如果对话已经建立: INSERT INTO messages VALUES (:conversation_id, 0, :sender_id, :message_contents, 0, NOW()); 更新对话 SET last_message_id=LAST_INSERT_ID() WHERE id = :conversation_id

注意:可以将 UPDATE 语句安排为 LOW_PRIORITY,因为您不必总是 100% 正确。

对话概述

这已成为一个更简单的查询:

SELECT other_user.name, m.contents, m.read, c.id
FROM participation AS p
INNER JOIN user AS other_user ON other_user.id = p.uid2
INNER JOIN conversation AS c ON c.id = p.conversation_id
INNER JOIN messages AS m ON m.id = c.last_message_id
WHERE p.uid1 = :user_id
ORDER BY m.time DESC
LIMIT 50

免责声明:我没有对此进行测试,但这篇文章对你来说应该是有意义的。

优化

拥有双向表的另一个好处是它可以为分片做好准备,这是一种将相关数据推送到另一个数据库(在不同的机器上)的方法;根据某些规则,您将确定从何处获取信息。

您可以通过以下方式移动数据:

  1. participation根据uid1字段划分表格
  2. messages根据conversation_id字段划分表格

消息概述将变得更加复杂,因为您可能被迫进行两个查询;不过,这可以通过缓存(在极端情况下是文档数据库)来缓解。

希望这能给你一些关于未来规划的想法:)

于 2012-05-24T15:05:50.553 回答
0

我认为最简单的方法是:

表:

conversation(cid | userId | friendId | last_message_id)
messages(mid | message | userId | read | time | cid)

然后在用户在特定对话中插入每条消息后更新 last_message_id。

然后运行这个简单的查询。它会给你你想要的。

SELECT * FROM conversation c, messages m 
WHERE (c.userId='$uid' OR c.friendId='$uid')
AND c.last_msg_id=m.message_id
ORDER BY created_time DESC

$uid 是登录用户的 id。

所以实际上这个过程在做什么:

  1. 显示登录用户的所有对话。
  2. 引用最后一条消息(因此您不需要分组依据)
  3. 最后按 desc 顺序显示消息。
于 2012-06-09T04:59:26.627 回答
-1

扩展观察者建议的答案。

您应该考虑放弃“对话”概念以进一步简化。

+----+---------+------+------------------+--------+----------+
| id | message | read | time             | toUser | fromUser |
+----+---------+------+------------------+--------+----------+
| 1  |  test 1 |  0   | (some timestamp) |  3     |   4      |
| 2  |  test 2 |  0   | (some timestamp) |  4     |   3      |
+----+---------+------+------------------+--------+----------+

用户 123 的所有对话列表:

SELECT * FROM (
    SELECT id, message, toUser, fromUser   
    FROM userMessages 
    WHERE toUser = 123 OR fromUser = 123 
    ORDER BY id DESC
) AS internalTable 
GROUP BY toUser, fromUser 

列出用户 123 和用户 456 之间的整个对话:

SELECT * 
FROM userMessages
WHERE (toUser = 123 OR fromUser = 123) 
AND (toUser = 456 OR fromUser = 456)
ORDER BY time DESC
于 2012-05-25T00:30:18.450 回答