我为用户创建了一个消息传递系统,它允许他们向另一个用户发送消息。如果这是他们第一次说话,那么就会启动新的会话,如果不是旧的会话继续。
用户收件箱会列出用户与所有其他用户进行的所有对话,然后按照包含最新帖子的对话排序。
用户只能与另一位用户进行一次对话。
当用户点击其中一个会话时,他们会转到一个页面,显示他们在最顶层的最新帖子中进行的整个对话。所以它有点像消息聊天功能。
我有两张桌子:
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 ...但这会产生大量冗余数据吗?
答案 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_id
和friend_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_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
答案 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)
由于给定的一对用户最多只能有一个会话,因此不需要“发明”单独的密钥来识别会话。此外,你的问题的措辞似乎表明一条消息总是发送给一个用户,所以我可能会这样做:
现在,关于这个模型有几点需要注意:
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(希伯来语)。
创建“主题”(对话)表:
CREATE TABLEpb_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
), KEYlast
(t_last
) ) ENGINE=InnoDB AUTO_INCREMENT=137106342 DEFAULT CHARSET=utf8
在用户和对话之间创建链接:
CREATE TABLEpb_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 KEYtopic-user
(l_topic
,l_user
), KEYuser-topicnew
(l_user
,l_new
,l_topic
) USING BTREE, KEYuser-topic
(l_user
,l_visible
,l_topic
) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=64750078 DEFAULT CHARSET=utf8
制作讯息
CREATE TABLEpb_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
), KEYdate_topic
(m_date
,m_topic
), KEYtopic_date_from
(m_topic
,m_date
,m_from
) ) ENGINE=InnoDB
对话可以与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
免责声明:我没有对此进行过测试,但这篇文章应该对您有所帮助。
拥有双向表的好处的另一个原因是它为分片做好准备,这是一种将相关数据推送到另一个数据库(在另一台机器上)的方法;根据某些规则,您将确定从何处获取信息。
您可以通过以下方式移动数据:
participation
字段uid1
表格
messages
字段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。
所以实际上这个过程正在做什么: