私信表需为sender_id和receiver_id分别建单列索引或(receiver_id, created_at)联合索引;已读标记须在查收件箱时批量UPDATE并加read_at IS NULL条件;删除逻辑应拆分为deleted_by_sender/deleted_by_receiver两字段。

私信表必须带双向索引,否则查收件箱会慢得想删库
用户查自己收到的私信,本质是 WHERE receiver_id = ?;查自己发的,则是 WHERE sender_id = ?。如果只在 receiver_id 上建索引,后者就全表扫描。
实操建议:
- 给
sender_id和receiver_id分别建单列索引,或建联合索引(receiver_id, created_at)(收件箱按时间倒序最常用) - 别用
INDEX(sender_id, receiver_id)这种“看起来覆盖全面”的联合索引——它对纯WHERE sender_id = ?有效,但对WHERE receiver_id = ?几乎无效 - 如果还要支持“两人之间全部往来消息”,加一个
UNIQUE KEY (sender_id, receiver_id, created_at)不现实(时间精度问题),改用CHECK((sender_id receiver_id))配合生成列做归一化更稳
已读状态不能只靠 update time 字段判断
read_at 为 NULL 表示未读,这是常见设计。但线上常出现“用户明明点开了消息列表,却没触发已读标记”,结果新消息红点一直不掉。
原因和做法:
- 前端列表页通常只查摘要(
id, sender_id, title, created_at),根本没取read_at字段,自然没法触发更新 - 正确做法:查收件箱时,用
UPDATE ... SET read_at = NOW() WHERE id IN (...) AND read_at IS NULL批量标记(注意加AND read_at IS NULL防重复更新) - 别在应用层先
SELECT再UPDATE—— 并发下会漏标;也别依赖 WebSocket 推送后才更新,网络抖动就丢状态
软删除字段要拆开:逻辑删除 + 归档标记
用户点了“删除私信”,不是真删,但也不能让它永远躺在主表里拖慢查询。直接用 is_deleted 一刀切,会导致“我删了,对方还能看见”这种体验 bug。
真实场景需要区分:
-
deleted_by_sendertinyint(1) DEFAULT 0 —— 发件人是否已删除该条 -
deleted_by_receivertinyint(1) DEFAULT 0 —— 收件人是否已删除该条 - 查发件箱时加
WHERE deleted_by_sender = 0;查收件箱加WHERE deleted_by_receiver = 0 - 归档需求另加
archived_at字段,别和删除逻辑混在一起;清理历史数据时,只清deleted_by_sender = 1 AND deleted_by_receiver = 1 AND created_at
MySQL 8.0+ 可用生成列简化“会话 ID”计算
想快速查 A 和 B 之间的全部消息(不分方向),传统做法是写 WHERE (sender_id = ? AND receiver_id = ?) OR (sender_id = ? AND receiver_id = ?),无法走索引。
更稳的解法:
- 加生成列:
CONCAT(LEAST(sender_id, receiver_id), '-', GREATEST(sender_id, receiver_id)) STORED,命名为chat_session_id - 在该列上建索引:
INDEX(chat_session_id, created_at) - 查会话时直接
WHERE chat_session_id = '123-456' ORDER BY created_at DESC,索引生效,无歧义 - 注意:生成列必须
STORED(非VIRTUAL),否则无法索引;且LEAST/GREATEST对 NULL 敏感,确保sender_id和receiver_id是NOT NULL
最易被忽略的是时间精度和事务边界:发信成功但通知失败、已读标记和消息拉取不在同一事务里,都会让状态错乱。宁可多一次 SELECT ... FOR UPDATE,也别信“前端点一下就完了”。


















