
本文介绍如何通过 sql 子查询与多表连接,从 `messages` 表中高效获取每个用户对(from_id/to_id)的最新消息,并关联 `users` 表展示双方姓名、头像等完整会话摘要。
在构建即时通讯或私信模块时,一个常见需求是:为当前用户列出所有有过聊天记录的联系人,并显示每段对话中时间最新的那条消息(无论该消息是当前用户发出的还是收到的)。但原始代码仅能获取“当前用户作为发送方(from_id)”的最新消息,遗漏了对方主动发起的会话,且未正确处理双向对话关系。
核心问题在于:messages 表中每条记录仅代表单向交互(A→B 或 B→A),而“两人之间的最新消息”应视为一个无向会话单元。因此,需先标准化对话标识(例如按 LEAST(from_id, to_id) 和 GREATEST(from_id, to_id) 分组),再取每组中 created_at 最大的记录。
以下是推荐的 Laravel 实现方案(兼容 MySQL 5.7+ / 8.0+):
✅ 正确 SQL 查询逻辑(支持双向会话)
SELECT
u1.id AS user_id,
u1.name AS user_name,
CONCAT('https://www.interwebs.co.in/puzzle/attach/', u1.avatar) AS user_image,
u2.id AS other_user_id,
u2.name AS other_user_name,
CONCAT('https://www.interwebs.co.in/puzzle/attach/', u2.avatar) AS other_user_image,
m.body AS message,
m.attachment,
m.seen AS seen_count,
m.created_at
FROM messages m
INNER JOIN (
-- 步骤1:为每对用户(无序)找出最新消息时间戳
SELECT
LEAST(from_id, to_id) AS user_a,
GREATEST(from_id, to_id) AS user_b,
MAX(created_at) AS latest_time
FROM messages
WHERE from_id = ? OR to_id = ?
GROUP BY user_a, user_b
) latest ON
LEAST(m.from_id, m.to_id) = latest.user_a
AND GREATEST(m.from_id, m.to_id) = latest.user_b
AND m.created_at = latest.latest_time
-- 步骤2:关联 users 表,动态识别当前用户与对方
INNER JOIN users u1 ON u1.id = CASE
WHEN m.from_id = ? THEN m.from_id ELSE m.to_id
END
INNER JOIN users u2 ON u2.id = CASE
WHEN m.from_id = ? THEN m.to_id ELSE m.from_id
END
ORDER BY m.created_at DESC;✅ Laravel 中安全执行(使用参数绑定防注入)
$userId = $request->user_id;
$sql = "SELECT
u1.id AS user_id,
u1.name AS user_name,
CONCAT('https://www.interwebs.co.in/puzzle/attach/', u1.avatar) AS user_image,
u2.id AS other_user_id,
u2.name AS other_user_name,
CONCAT('https://www.interwebs.co.in/puzzle/attach/', u2.avatar) AS other_user_image,
m.body AS message,
m.attachment,
m.seen AS seen_count,
m.created_at
FROM messages m
INNER JOIN (
SELECT
LEAST(from_id, to_id) AS user_a,
GREATEST(from_id, to_id) AS user_b,
MAX(created_at) AS latest_time
FROM messages
WHERE from_id = ? OR to_id = ?
GROUP BY user_a, user_b
) latest ON
LEAST(m.from_id, m.to_id) = latest.user_a
AND GREATEST(m.from_id, m.to_id) = latest.user_b
AND m.created_at = latest.latest_time
INNER JOIN users u1 ON u1.id = CASE
WHEN m.from_id = ? THEN m.from_id ELSE m.to_id
END
INNER JOIN users u2 ON u2.id = CASE
WHEN m.from_id = ? THEN m.to_id ELSE m.from_id
END
ORDER BY m.created_at DESC";
$chats = DB::select($sql, [$userId, $userId, $userId, $userId]);⚠️ 注意事项
- 索引优化:确保 messages 表上存在复合索引 INDEX(from_id, to_id, created_at) 和 INDEX(to_id, from_id, created_at),大幅提升分组与连接性能;
- 空值处理:若 avatar 字段可能为空,建议在 CONCAT 前用 COALESCE(u1.avatar, 'default.png') 防止拼接出无效 URL;
- 时间精度:若存在毫秒级重复 created_at,可追加 id 作为第二排序键(如 ORDER BY created_at DESC, id DESC)确保唯一性;
- Laravel 9+ 推荐替代方案:可封装为 Eloquent 查询作用域(Scope),或使用 DB::table()->fromSub() 构建子查询,提升可读性与可维护性。
该方案真正实现了「以用户为中心的会话聚合」——无论消息由谁发起、谁接收,只要两人之间存在交互,即归为同一会话并提取最新一条,完美匹配真实业务场景。










