查询优化指南
PocketBase 使用 SQLite 作为底层存储,了解查询优化技巧可以显著提升应用性能。
查询执行流程
Section titled “查询执行流程”客户端请求 → API 规则检查 → 数据库查询 → 结果返回优化点:
- 减少不必要的请求
- 优化数据库查询
- 减少返回数据量
- 合理使用缓存
索引能显著提升查询性能,但会增加写入开销。
何时需要索引:
- 高频过滤字段
- 排序字段
- 关联查询的外键
- 唯一性约束字段
索引类型:
// 单列索引{ "name": "status_idx", "type": "index", "options": { "fields": ["status"] }}
// 复合索引{ "name": "user_resource_idx", "type": "index", "options": { "fields": ["userId", "resourceId"], "unique": true }}
// 唯一索引{ "name": "slug_unique", "type": "index", "options": { "fields": ["slug"], "unique": true }}复合索引顺序
Section titled “复合索引顺序”复合索引的字段顺序很重要:
// 好的设计// 查询: filter=status='published' && sort=-publishedAt{ "name": "posts_list_idx", "type": "index", "options": { "fields": ["status", "publishedAt"] }}
// 不好的设计// 反转顺序无法优化查询{ "name": "bad_idx", "type": "index", "options": { "fields": ["publishedAt", "status"] }}常见索引模式
Section titled “常见索引模式”// 用户登录优化{ "name": "user_email_idx", "type": "index", "options": { "fields": ["email"], "unique": true }}
// 文章列表查询优化{ "name": "posts_status_created_idx", "type": "index", "options": { "fields": ["status", "created"] }}
// 关联查询优化{ "name": "posts_author_idx", "type": "index", "options": { "fields": ["authorId", "created"] }}减少返回数据
Section titled “减少返回数据”使用 fields 参数只获取需要的字段:
// 不好的做法:获取所有字段const records = await pb.collection("posts").getList(1, 20);
// 好的做法:只获取需要的字段const records = await pb.collection("posts").getList(1, 20, { fields: "id,title,slug,created",});跳过总数计算
Section titled “跳过总数计算”对于无需总数的场景,使用 skipTotal:
// 不好的做法:每次都计算总数const records = await pb.collection("posts").getList(1, 50);
// 好的做法:跳过总数计算const records = await pb.collection("posts").getList(1, 50, { skipTotal: true,});// 方案 1: 基于偏移的分页let page = 1;const result = await pb.collection("posts").getList(page, 20);
// 方案 2: 基于游标的分页(更适合大数据量)const result = await pb.collection("posts").getList(1, 20, { filter: `created < '${lastTimestamp}'`, sort: "-created",});// 不好的做法:OR 条件await pb.collection("posts").getList(1, 20, { filter: "status = 'published' || status = 'featured'",});
// 好的做法:使用 INawait pb.collection("posts").getList(1, 20, { filter: "status IN ('published', 'featured')",});// 不好的做法:循环查询for (const id of ids) { const record = await pb.collection("posts").getOne(id);}
// 好的做法:使用过滤批量获取const records = await pb.collection("posts").getList(1, 50, { filter: ids.map((id) => `id = '${id}'`).join(" || "),});关联查询优化
Section titled “关联查询优化”控制 Expand 深度
Section titled “控制 Expand 深度”// 不好的做法:过深的嵌套await pb.collection("posts").getList(1, 20, { expand: "author.profile.avatar,category.parent,comments.author.profile",});
// 好的做法:限制嵌套深度await pb.collection("posts").getList(1, 20, { expand: "author,category",});分步查询替代深度 Expand
Section titled “分步查询替代深度 Expand”// 不好的做法:一次查询获取所有关联const posts = await pb.collection("posts").getList(1, 20, { expand: "author.profile.comments.post",});
// 好的做法:分步查询const posts = await pb.collection("posts").getList(1, 20, { expand: "author",});const authorIds = posts.items.map((p) => p.author);const authors = await pb.collection("users").getList(1, 50, { filter: authorIds.map((id) => `id = '${id}'`).join(" || "),});// 简单的内存缓存const cache = new Map();const CACHE_TTL = 60000; // 1 分钟
async function getCachedPosts() { const cached = cache.get("posts"); if (cached && Date.now() - cached.time < CACHE_TTL) { return cached.data; }
const posts = await pb.collection("posts").getList(1, 20, { filter: "status = 'published'", }); cache.set("posts", { data: posts, time: Date.now() }); return posts;}// 使用 ETag 或 Last-Modifiedconst posts = await pb.collection("posts").getList(1, 20, { headers: { "If-None-Match": etag, },});缓存不常变化的数据
Section titled “缓存不常变化的数据”// 配置、分类等不常变化的数据let categoriesCache = null;
async function getCategories() { if (!categoriesCache) { categoriesCache = await pb.collection("categories").getList(1, 50); } return categoriesCache;}实时订阅优化
Section titled “实时订阅优化”// 不好的做法:订阅所有事件pb.collection("posts").subscribe("*", callback);
// 好的做法:只订阅需要的事件pb.collection("posts").subscribe("update", callback);pb.collection("posts").subscribe("delete", callback);使用过滤订阅
Section titled “使用过滤订阅”// 只订阅特定记录的更新pb.collection("posts").subscribe(`id = '${postId}'`, callback);
// 只订阅特定状态的记录pb.collection("posts").subscribe("status = 'published'", callback);// 组件卸载时取消订阅let subscription = null;
onMounted(() => { subscription = pb.collection("posts").subscribe("*", callback);});
onUnmounted(() => { subscription?.unsubscribe();});Vacuum 数据库
Section titled “Vacuum 数据库”# 定期执行 vacuum 优化数据库./pocketbase db vacuum分析查询性能
Section titled “分析查询性能”# 启用查询日志./pocketbase serve --logLevel=debug数据库连接池
Section titled “数据库连接池”PocketBase 使用单文件 SQLite,无需配置连接池。
// 在 Hooks 中记录慢查询onRecordListRequest((e) => { const start = Date.now(); e.next()?.then(() => { const duration = Date.now() - start; if (duration > 1000) { console.warn(`Slow query: ${e.http.Request().URL} (${duration}ms)`); } });});监控查询数量
Section titled “监控查询数量”// 记录 API 请求数量let requestCount = 0;
pb.beforeSend = function (url) { requestCount++; console.log(`Request ${requestCount}:`, url);};# 使用 wrk 进行压力测试wrk -t4 -c100 -d30s http://localhost:8090/api/collections/posts/records// 测试查询性能async function benchmark() { const iterations = 100; const start = Date.now();
for (let i = 0; i < iterations; i++) { await pb.collection("posts").getList(1, 20); }
const duration = Date.now() - start; console.log(`Average: ${duration / iterations}ms per query`);}1. 合理设计数据结构
Section titled “1. 合理设计数据结构”// 好的设计:扁平结构{ "title": "Post Title", "authorId": "user_id", "status": "published"}
// 不好的设计:深层嵌套{ "title": "Post Title", "author": { "id": "user_id", "name": "Author Name", "profile": { "avatar": "...", "bio": "..." } }}2. 使用批量操作
Section titled “2. 使用批量操作”// 批量创建const promises = data.map((item) => pb.collection("posts").create(item));await Promise.all(promises);3. 避免过度查询
Section titled “3. 避免过度查询”// 使用防抖减少请求const debouncedSearch = debounce(async (query) => { return await pb.collection("posts").getList(1, 20, { filter: `title ~ '${query}'`, });}, 300);4. 合理使用分页
Section titled “4. 合理使用分页”// 不要一次性加载所有数据// 好的做法:使用分页或无限滚动const result = await pb.collection("posts").getList(page, 20);| 指标 | 目标值 |
|---|---|
| API 响应时间 | < 200ms |
| 列表查询 | < 500ms |
| 单条记录查询 | < 100ms |
| 实时订阅延迟 | < 100ms |
| 数据库写入 | < 50ms |
性能瓶颈识别
Section titled “性能瓶颈识别”- API 规则复杂:简化规则逻辑
- 缺少索引:为过滤字段添加索引
- 过深 Expand:减少关联深度
- 大量数据返回:使用 fields 限制
- 频繁查询:增加缓存层
Q: 如何优化 LIKE 查询?
Section titled “Q: 如何优化 LIKE 查询?”SQLite 的 LIKE 查询较慢,考虑:
// 不好的做法await pb.collection("posts").getList(1, 20, { filter: "content ~ '%keyword%'",});
// 好的做法:使用全文搜索扩展或外部搜索引擎Q: 如何处理百万级数据?
Section titled “Q: 如何处理百万级数据?”考虑:
- 分表/分库
- 使用专业的 PostgreSQL/MySQL
- 实现数据归档策略
Q: 为什么查询突然变慢?
Section titled “Q: 为什么查询突然变慢?”检查:
- 是否添加了新的复杂规则
- 数据量是否增长过快
- 索引是否正常工作
- 是否有其他资源竞争
Q: 如何减少实时订阅的服务器负载?
Section titled “Q: 如何减少实时订阅的服务器负载?”- 使用过滤订阅
- 降低订阅频率
- 使用轮询替代(对于非关键数据)
// 轮询实现setInterval(async () => { const result = await pb.collection("posts").getList(1, 1, { filter: `created > '${lastCheck}'`, }); if (result.items.length > 0) { // 处理新数据 lastCheck = new Date().toISOString(); }}, 5000);