本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:7月5日复制的资料/数据库查询优化计划.md。
SoSite 数据库查询优化计划
目标:将典型页面的 DB 查询次数从 16~18 次降到 6~8 次
原则:不改业务逻辑、不影响数据实时性、改动量小
当前每次请求的固定开销(用户已登录)
| 查询 | 次数 | 用途 |
|---|---|---|
| Session 读 | 1 | MySQL Session |
SELECT * FROM users | 1 | 查当前用户 |
SELECT theme FROM users | 1 | 查主题设置 |
SELECT COUNT FROM messages | 1 | 查未读消息数 |
SELECT + INSERT online_users | 2 | 在线状态 |
| Session 写 | 1 | 请求结束写入 |
| 固定小计 | 7 |
优化方案
🏆 第一刀:用户数据存 Session(省 2 次查询)
改动文件:app/Helpers/Auth.php、app/Controllers/AuthController.php
当前:每次请求都 SELECT * FROM users WHERE id = :id,即使同一个人 1 秒前刚查过。
改后:登录成功时,把用户信息写进 $_SESSION:
// AuthController::login() 登录成功后
$_SESSION['user_id'] = (int)$user['id'];
$_SESSION['user_data'] = $user; // 新增:用户信息直接存 Session// Auth::getCurrentUser()
// 优先从 Session 读,不再每次都查 DB
if (isset($_SESSION['user_data'])) {
return $_SESSION['user_data'];
}
// Session 中没有才查 DB(第一次或缓存被清除)```php
// 当用户修改资料时需要刷新 Session
Auth::clearUserCache() 改为同时清除 $_SESSION['user_data']
```
效果:每次请求省 1 次 SELECT * FROM users
🏆 第二刀:主题设置并入用户数据(省 1 次查询)
改动文件:app/Controllers/ThemeController.php、app/Helpers/Theme.php
当前:Theme::getCurrent() 对已登录用户执行 SELECT theme FROM users。
改后:$currentUser['theme'] 已经在用户数据里了(SELECT * FROM users 本来就包含 theme 字段),直接从用户数据读,不再单独查。
// Theme::getCurrent() 中
if (\app\Helpers\Auth::isLoggedIn()) {
$user = \app\Helpers\Auth::getCurrentUser(); // 已从 Session 读取,0 次查询
if ($user && !empty($user['theme'])) return $user['theme'];
}效果:省 1 次 SELECT theme FROM users
🏆 第三刀:未读消息数不进布局(省 1 次查询)
改动文件:app/Core/Controller.php
当前:每个页面加载都 SELECT COUNT(*) FROM messages WHERE receiver_id = :uid AND is_read = 0,仅为了在导航栏显示一个红点。
改后:布局中去掉未读消息数的自动查询,改为:
方案 A(推荐):用户点开「消息」时才查
方案 B:在 Session 中存一个每分钟过期的时间戳,1 分钟内不重复查
// Controller::view() 中去掉这一行
'unreadCount' => $isLoggedIn ? Settings::getUnreadMessageCount() : 0,
// 改为固定值,或只对有消息的用户显示
'unreadCount' => 0,效果:省 1 次 SELECT COUNT(*),代价是导航栏不显示未读红点(或者在消息页单独查)
🏆 第四刀:在线状态去掉 SELECT 检查(省 1 次查询)
改动文件:app/Helpers/Settings.php
当前:每次请求先 SELECT last_activity 判断是否 60 秒内已更新,没更新才写入。
改后:去掉 SELECT 预检,改成用 Session 记时间,60 秒内不执行任何在线状态操作:
public static function updateOnlineStatus(): void
{
if (!Auth::isLoggedIn()) return;
// 用 Session 时间戳判断,不查 DB
$lastUpdate = $_SESSION['_online_last_update'] ?? 0;
if (time() - $lastUpdate < 60) return;
$_SESSION['_online_last_update'] = time();
$now = date('Y-m-d H:i:s');
$db = \app\Core\Database::getInstance();
$db->query(
"INSERT INTO online_users (user_id, last_activity) VALUES (:uid, :now)
ON DUPLICATE KEY UPDATE last_activity = :now2",
[':uid' => $_SESSION['user_id'], ':now' => $now, ':now2' => $now]
);
}效果:省 1 次 SELECT,每 60 秒才写 1 次 DB
🏆 第五刀:搜索页去掉重复查询(省 4 次查询)
改动文件:app/Controllers/SearchController.php
当前:搜索时 Search::search() 被调用了两次——一次拿计数(perPage=1),一次拿数据(perPage=10)。每次内部都是 4 条 SQL(帖子 COUNT + SELECT + 博客 COUNT + SELECT)。
改后:一次调用同时返回计数和数据:
// 一次查询同时拿到计数和当前页数据
$searchResults = Search::search($keyword, $type === 'all' ? 'all' : $searchType, $page, $perPage, $catFilter);
$threadTotal = $searchResults['thread_count'] ?? 0;
$blogTotal = $searchResults['blog_count'] ?? 0;
$threadResults = $searchResults['threads'] ?? [];
$blogResults = $searchResults['blogs'] ?? [];效果:搜索时有结果时省 4 次(从 8 次降到 4 次)
效果汇总
| 优化 | 省查询 | 改动量 | 风险 |
|---|---|---|---|
| 用户数据存 Session | -1 | ~10 行 | 低(clearUserCache 已存在) |
| 主题从用户数据读 | -1 | ~3 行 | 低(theme 已在 users 表中) |
| 未读消息不从布局查 | -1 | ~1 行 | 低(仅导航红点消失) |
| 在线状态去 SELECT | -1 | ~5 行 | 低(Session 同样可靠) |
| 搜索去重 | -4 | ~5 行 | 低(逻辑不变) |
| 合计 | -8 | ~25 行 |
改完后各页面的预期查询数:
| 页面 | 当前 | 优化后 |
|---|---|---|
| 首页(已登录) | ~15 | ~7 |
| 论坛(已登录) | ~16 | ~8 |
| 搜索有结果 | ~18 | ~10 |
| 搜索无关键词 | ~7 | ~3 |
| 主题设置(已登录) | ~7 | ~4 |
| 主题设置(未登录) | ~2 | ~2 |
不改的部分
| 查询 | 原因 |
|---|---|
| Session 读/写 | MySQL Session 不可避免 |
| 帖子/博客内容查询 | 业务必须 |
| 权限检查 | 安全必须 |
| 插件钩子 | 插件系统必须 |
实施顺序
第一刀(用户存 Session) → 效果最明显,改完立即生效
第二刀(主题合并) → 顺势就改了
第三刀(未读消息) → 看你能不能接受导航栏不显示红点
第四刀(在线状态) → 独立改动,不影响其他
第五刀(搜索去重) → 搜索页专用要不要开始做?可以先做前两刀,十分钟搞定,效果立竿见影。