有没有更简单的方法来实现这种用户消息传递方式?

时间:2012-05-14 17:43:05

标签: mysql sql database database-design innodb

我为用户创建了一个消息传递系统,它允许他们向另一个用户发送消息。如果这是他们第一次说话,那么就会启动新的会话,如果不是旧的会话继续。

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

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

当用户点击其中一个会话时,他们会转到一个页面,显示他们在最顶层的最新帖子中进行的整个对话。所以它有点像消息聊天功能。

我有两张桌子:

  • userconversation
  • usermessages

userconversation

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

无论是谁发起第一个对话都将是userId和收件人friendId,然后这个对话将永远不会改变。

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

usermessages

包含特定消息,以及读取标志,时间和conversationId

+----+---------+--------+------+------+----------------+
| 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 ...但这会产生大量冗余数据吗?

13 个答案:

答案 0 :(得分: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

希望这有帮助

答案 1 :(得分:4)

如果您想要一种能够保留所有当前功能和工作流程的方法,并将数据保存在一个表中,我认为您非常接近。

不是让conversationId成为不同表的键,而是让它指向开始对话的消息的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

答案 2 :(得分:3)

如果用户之间只能进行一次对话,我看不到专用对话表的原因。要使此查询快速运行,您需要(user, message_id)上的复合索引,如果这些字段位于不同的表中,则无法进行。将user_idfriend_id移至userconversations。这将使每个记录的表8字节更重(甚至假定为8 - 字节标识符),这对于包含文本消息的表来说几乎不是问题。

如果每个用户的对话很少,每个用户都有很多消息,请使用以下命令:

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_idfriend_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

答案 3 :(得分:3)

如果您想简化查询,则应在表userconversation中添加最后一条消息ID:

ALTER TABLE userconversation ADD lastusermessageid

然后每次添加新消息时都应更新表用户对话:

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

答案 4 :(得分:3)

由于给定的一对用户最多只能有一个会话,因此不需要“发明”单独的密钥来识别会话。此外,你的问题的措辞似乎表明一条消息总是发送给一个用户,所以我可能会这样做:

enter image description here

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

  • 假设无法比SEND_TIME所用类型提供的解决方案更频繁地生成相同两个用户之间的消息。 1
  • 消息的方向不是由USER1_ID和USER2_ID的顺序决定的,而是由单独的标志(DIRECTION)确定。这样,给定用户之间的消息将始终具有相同的USER1_ID和USER2_ID组合(由上面的CHECK强制执行),无论谁发送和谁收到消息。这大大简化了查询。
  • 遗憾的是all InnoDB tables are clustered,因此辅助索引I1相对expensive。有办法解决这个问题,但由此产生的并发症可能不值得。

使用此数据模型,通过最新消息对“对话”(由用户对标识)进行排序变得相当容易。例如(将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较薄,它们会更加纤薄)。

答案 5 :(得分:1)

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

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

      CREATE TABLE pb_topics (
      t_id int(11) NOT NULL AUTO_INCREMENT,
      t_last int(11) NOT NULL DEFAULT '0',
      t_user int(11) NOT NULL DEFAULT '0',
      PRIMARY KEY (t_id),
      KEY last (t_last)
    ) ENGINE=InnoDB AUTO_INCREMENT=137106342 DEFAULT CHARSET=utf8

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

        CREATE TABLE pb_links (
      l_id int(11) NOT NULL AUTO_INCREMENT,
      l_user int(11) NOT NULL DEFAULT '0',
      l_new int(11) NOT NULL DEFAULT '0',
      l_topic int(11) NOT NULL DEFAULT '0',
      l_visible int(11) NOT NULL DEFAULT '1',
      l_bcc int(11) NOT NULL DEFAULT '0',
      PRIMARY KEY (l_id) USING BTREE,
      UNIQUE KEY topic-user (l_topic,l_user),
      KEY user-topicnew (l_user,l_new,l_topic) USING BTREE,
      KEY user-topic (l_user,l_visible,l_topic) USING BTREE
    ) ENGINE=InnoDB AUTO_INCREMENT=64750078 DEFAULT CHARSET=utf8

  3. 制作讯息

        CREATE TABLE pb_messages (
      m_id int(11) NOT NULL AUTO_INCREMENT,
      m_from int(11) NOT NULL,
      m_date datetime NOT NULL DEFAULT '1987-11-13 00:00:00',
      m_title varchar(75) NOT NULL,
      m_content mediumtext NOT NULL,
      m_topic int(11) NOT NULL,
      PRIMARY KEY (m_id),
      KEY date_topic (m_date,m_topic),
      KEY topic_date_from (m_topic,m_date,m_from)
    ) ENGINE=InnoDB 

  4. 对话可以与2个或更多朋友进行对话(BCC已添加,如电子邮件,但您可以跳过它)。

    插入新讯息:  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  创建一个简单的缓存表,并禁止繁忙消息表中选择计数的工作量。

答案 6 :(得分: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

答案 7 :(得分:1)

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

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

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

会话字段是&#39; userid-userid&#39;,因此字符串。要检查用户是否已启动对话,请使用连字符连接user_ids并进行检查。

这种消息传递系统非常复杂。

第二个表非常简单。

+----+--------+----------+-------------+-------+
| id | inboxid | userid | content | datecreated|
+----+--------+----------+-------------+-------+

答案 8 :(得分: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

答案 9 :(得分: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消息VALUES(:conversation_id,0,:sender_id,:message_contents,0,NOW());     UPDATE会话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字段
  2. 划分uid1表格
  3. 根据messages字段
  4. 划分conversation_id表格

    由于您可能被迫进行两次查询,因此消息概述会变得更加复杂;这可以通过缓存(以及在极端情况下的文档数据库)中稍微减轻。

    希望这能为您提供有关未来规划的一些想法:)

答案 10 :(得分: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

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

答案 11 :(得分:0)

扩展Watcher建议的答案。

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

+----+---------+------+------------------+--------+----------+
| 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

答案 12 :(得分: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。
  4. 的顺序显示消息