
本文介绍如何通过原生 sql 子查询与多表 join,准确获取当前用户与所有联系人之间的最新消息记录,并关联用户信息(如头像、姓名),解决 `from_id/to_id` 双向场景下“每对用户仅取最新一条消息”的核心需求。
在构建即时通讯或私信功能时,一个常见且关键的需求是:为当前登录用户展示其所有对话对象的最新消息摘要(含对方用户名、头像、消息内容、时间等)。难点在于:消息表 messages 中的 from_id 和 to_id 是双向的——用户 A 发给 B 的消息与 B 发给 A 的消息属于同一会话,但数据库中是两条独立记录;而我们需要的是「A 与 B 这对组合中时间最新的那条」,而非单纯按 from_id 或 to_id 分组。
直接使用 GROUP BY + MAX(created_at) 无法直接获取完整消息行(因 MySQL 严格模式下非聚合字段不可选),因此推荐采用「子查询定位最新时间 → 主表 JOIN 回填完整数据」的经典方案。
以下是优化后的完整 SQL 查询(兼容 Laravel DB::select()):
SELECT
COALESCE(from_user.id, to_user.id) AS user_id,
COALESCE(from_user.name, to_user.name) AS name,
CONCAT('https://www.interwebs.co.in/puzzle/attach/', COALESCE(from_user.avatar, to_user.avatar)) AS image,
COALESCE(from_user.mobile, to_user.mobile) AS mobile,
msgs.body AS message,
msgs.attachment,
msgs.seen AS seen_count,
msgs.created_at
FROM messages AS msgs
INNER JOIN (
-- 步骤1:为每一对 (from_id, to_id) 找出最新 created_at
SELECT
from_id,
to_id,
MAX(created_at) AS last_created_at
FROM messages
WHERE from_id = ? OR to_id = ?
GROUP BY from_id, to_id
) AS latest ON
msgs.from_id = latest.from_id
AND msgs.to_id = latest.to_id
AND msgs.created_at = latest.last_created_at
-- 步骤2:关联 users 表,动态识别「对方用户」
LEFT JOIN users AS from_user ON msgs.from_id = from_user.id
LEFT JOIN users AS to_user ON msgs.to_id = to_user.id
-- 步骤3:确保只返回当前用户参与的对话(无论作为发送方 or 接收方)
WHERE (msgs.from_id = ? OR msgs.to_id = ?)
ORDER BY msgs.created_at DESC;? 关键说明:
- 使用 COALESCE(from_user.id, to_user.id) 确保取到「非当前用户」的 ID(即对话对象),需配合 WHERE 条件动态判断角色;
- ? 占位符对应 $request->user_id,Laravel 会自动绑定参数,防止 SQL 注入;
- WHERE from_id = ? OR to_id = ? 保证只拉取当前用户参与的全部对话(含自己发的和收到的);
- GROUP BY from_id, to_id 按「对话对」分组,而非单侧 ID,这是实现「每两人仅一条最新消息」的核心逻辑;
- 若需区分消息方向(如标记“你发送”/“对方发送”),可在 SELECT 中添加 CASE WHEN msgs.from_id = ? THEN 'outgoing' ELSE 'incoming' END AS direction。
在 Laravel 控制器中调用如下:
$user_id = $request->user_id; $sql = "SELECT ... "; // 上述完整 SQL(注意替换 ? 为 :user_id 以支持命名绑定) $results = DB::select($sql, [$user_id, $user_id, $user_id, $user_id]);
✅ 优势总结:
- ✅ 精确匹配双向会话,避免重复或遗漏;
- ✅ 利用数据库索引(建议为 (from_id, to_id, created_at) 建复合索引)提升性能;
- ✅ 兼容高并发场景,无需 PHP 层循环处理;
- ✅ 易扩展:可轻松增加未读数统计、最后在线状态等字段。
切勿使用多次子查询或 IN (SELECT MAX()) 等低效写法——它们在大数据量下极易引发全表扫描。始终让数据库做它最擅长的事:基于索引的聚合与关联。










