本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:暂存20260913/docs/插件性能与索引审计报告 (2).md。
FlintHub 插件性能与索引审计报告
审计日期:2026-08-13
范围:15 个插件目录(14 启用 + invite 停用)全部 PHP 文件
方法:① 静态扫描 JOIN 语句 → ② 核对实际 SQLite 库索引(PRAGMA index_list)→ ③ 对每处联表查询实测
EXPLAIN QUERY PLAN结论:未发现会显著拖慢页面的缺索引联表查询;3 处查询存在可优化点(详见第四节),建议但非必须补索引
一、静态扫描结果:全部联表查询清单
遍历 15 个插件目录,共发现 7 处 JOIN 语句(分布在 4 个插件):
| # | 插件 | 位置 | SQL 摘要 | 查询库 |
|---|---|---|---|---|
| 1 | medal | Plugin.php:184 getUserMedals() | user_medals um JOIN medals m ON um.medal_id = m.id WHERE um.user_id = :uid ORDER BY um.created_at DESC | medal 独立库 |
| 2 | medal | Plugin.php:201 getWearingMedals() | user_medal_wears w JOIN medals m ON w.medal_id = m.id WHERE w.user_id = :uid AND m.status = 1 ORDER BY w.wear_sort ASC | medal 独立库 |
| 3 | medal | hook/controller_view_before.php:38 | user_medal_wears w JOIN medals m ON w.medal_id = m.id WHERE w.user_id IN (...) AND m.status = 1 ORDER BY w.user_id, w.wear_sort | medal 独立库 |
| 4 | dice | Plugin.php:43 getLeaderboard() | points_log pl JOIN users u ON pl.user_id = u.id WHERE pl.reason = '摇骰子投注' OR pl.reason = '摇骰子中奖' GROUP BY pl.user_id ORDER BY total_profit DESC LIMIT 10 | 核心库 |
| 5 | seo | hook/layout_head_end.php:48 | threads t LEFT JOIN users u ON t.user_id = u.id WHERE t.id = :id AND t.deleted_at IS NULL | 核心库 ⚠️ |
| 6 | seo | hook/layout_head_end.php:68 | blogs b LEFT JOIN users u ON b.user_id = u.id WHERE b.id = :id | 核心库 |
| 7 | user_profile | Plugin.php:112 getBlogs() | blogs b LEFT JOIN blog_categories bc ON b.category_id = bc.id WHERE b.user_id = :uid ORDER BY b.created_at DESC LIMIT 10 | 核心库 |
其余 11 个插件(announcements / content_review / daily_checkin / edit_info / friend_links / icon_generator / invite / notifications / post_favorite / quiz_duel / red_packet)零 JOIN——迁移后均已改为「独立库查询 + 核心库批量补字段」,无跨库联表。
二、已建索引核对(PRAGMA index_list 实测)
medal 独立库 plugins/medal/data/medal.sqlite
| 表 | 现有索引 |
|---|---|
medals | 无索引(仅 id INTEGER PRIMARY KEY,JOIN 走主键 ✓) |
user_medals | sqlite_autoindex_user_medals_1 UNIQUE(user_id, medal_id) |
user_medal_wears | sqlite_autoindex_user_medal_wears_1 UNIQUE(user_id, wear_sort);sqlite_autoindex_user_medal_wears_2 UNIQUE(user_id, medal_id) |
核心库 data/meta/business.sqlite(dice/seo/user_profile 联查所在库)
| 表 | 现有索引 |
|---|---|
points_log | idx_points_log_user (user_id) |
users | 主键 + idx_users_email(email)、idx_users_username(username) |
threads | 主键 + idx_threads_created(created_at)、idx_threads_category(category_id) |
blogs | idx_blogs_user (user_id) |
blog_categories | 无索引(仅主键,JOIN 走主键 ✓) |
三、EXPLAIN QUERY PLAN 实测执行计划
✅ 执行计划健康(无 SCAN / 无临时B树,无需处理)
| 查询 | 执行计划 | 判定 |
|---|---|---|
| #2 medal getWearingMedals | SEARCH w USING INDEX sqlite_autoindex_user_medal_wears_1 (user_id=?) + SEARCH m USING INTEGER PRIMARY KEY | ✓ 全走索引 |
| #3 medal hook IN 查询 | SEARCH w USING INDEX sqlite_autoindex_user_medal_wears_1 (user_id=?) + SEARCH m USING INTEGER PRIMARY KEY | ✓ 全走索引 |
| #5 seo 帖子详情 | SEARCH t USING INTEGER PRIMARY KEY + SEARCH u USING INTEGER PRIMARY KEY LEFT-JOIN | ✓ 全走索引 |
| #6 seo 博客详情 | SEARCH b USING INTEGER PRIMARY KEY + SEARCH u USING INTEGER PRIMARY KEY LEFT-JOIN | ✓ 全走索引 |
⚠️ 存在优化空间(USE TEMP B-TREE / 索引扫描)
| 查询 | 执行计划 | 问题 |
|---|---|---|
| #1 medal getUserMedals | SEARCH um USING INDEX ..._user_medals_1 (user_id=?) ✓ + SEARCH m USING INTEGER PRIMARY KEY ✓ + USE TEMP B-TREE FOR ORDER BY ★ | WHERE 已走索引,但 ORDER BY um.created_at 无索引 → 需要临时B树排序(数据量小,开销可忽略) |
| #4 dice getLeaderboard | SCAN pl USING INDEX idx_points_log_user ★ + SEARCH u USING INTEGER PRIMARY KEY ✓ + USE TEMP B-TREE FOR ORDER BY ★ | 该查询按 reason 过滤,而 idx_points_log_user 是按 user_id 建的,无法用于 reason 过滤 → 全索引扫描 + 聚合排序 |
| #7 user_profile getBlogs | SEARCH b USING INDEX idx_blogs_user (user_id=?) ✓ + SEARCH bc USING INTEGER PRIMARY KEY LEFT-JOIN ✓ + USE TEMP B-TREE FOR ORDER BY ★ | WHERE 已走索引,但 ORDER BY b.created_at 无索引 → 临时B树排序 |
四、缺失索引汇总与建议修复语句
| 优先级 | 库 | 表 | 缺失字段 | 影响查询 | 建议 CREATE INDEX(幂等) |
|---|---|---|---|---|---|
| P2(建议) | medal 独立库 | user_medals | (user_id, created_at) | #1 勋章列表排序 | CREATE INDEX IF NOT EXISTS idx_user_medals_user_created ON user_medals (user_id, created_at DESC); |
| P1(建议) | 核心库 | points_log | reason | #4 骰子排行榜过滤 | CREATE INDEX IF NOT EXISTS idx_points_log_reason ON points_log (reason); |
| P2(建议) | 核心库 | blogs | (user_id, created_at) | #7 个人主页博客排序 | CREATE INDEX IF NOT EXISTS idx_blogs_user_created ON blogs (user_id, created_at DESC); |
说明:三处均为「WHERE 已走索引、仅排序/过滤字段缺索引」的轻量问题,当前数据量下每次排序几十行内,不影响 0.02s 级别的页面性能。若追求极致可补,但不补也无风险。
五、附注(非性能问题,顺带发现)
- seo#5 查询已退役表:
layout_head_end.php帖子详情分支查的是 business.sqlite 的threads表,而 SplitDB 迁移后帖子已迁至 main_index(threads表 0 行)→ 该分支实际查不到数据,SEO 帖子描述长期为空。建议后续改为经main_index.topic_index读取(属功能修复,不在本次索引审计范围)。
六、结论
- 无致命缺索引联表查询:7 处 JOIN 中 4 处执行计划完全健康,3 处仅有「排序/过滤字段缺索引」的轻微优化空间
- 页面 0.02s 的开销主因不是 JOIN 索引,而是 14 个启用插件的 hook 各自产生的固定查询开销(每页多 10+ 条简单查询)——如需进一步压时,应从「减少 hook 次数 / 合并批量查询 / 结果缓存」入手,而非补索引