42-RDB查询拖慢首屏-用索引分页和快照缓存拆开

第42篇|RDB 查询拖慢首屏:用索引分页和快照缓存拆开

摘要:本地 RDB 数据多了以后,首屏卡顿经常不是 UI 写得慢,而是页面一出现就做了全表排序、统计和复杂过滤。更稳的做法是把首屏快照、分页查询和统计重算拆开:首屏先读轻量快照,列表分页加载,统计在写入后维护或后台刷新。

一个记录类应用首屏从 300ms 变成 2 秒,表面看是列表项太多,实际是首页 aboutToAppear 里同时查全部记录、按时间排序、计算月统计、再渲染卡片。数据少时没问题,半年后本地记录多了,首屏就被 RDB 查询拖住。修复时不能只优化组件,得把查询路径拆开。

这篇文章解决四个实际问题:

  1. 区分首屏快照、列表分页、统计重算三类读请求。
  2. 给常用排序和筛选字段补索引,不在页面里全量排序。
  3. 写入后更新轻量摘要,首屏先用摘要展示。
  4. 用耗时日志证明慢在查询还是渲染。

先把首页查询拆成三类

首屏慢时不要只看列表组件。先列出首页到底读了哪些数据,每一类数据是否都必须阻塞首帧。

位置 现象 风险 处理方向
今日摘要 只需要总数和最近一条 全表扫描太重 读 SummaryStore
记录列表 需要分页 一次查全部卡住 limit/offset 或游标
月统计 计算量大 不该阻塞首屏 后台刷新
筛选排序 按时间和类型查 无索引导致慢 迁移补索引

这一步的价值是先把问题放回运行链路。只要能确认问题停在哪个位置,后面就不用靠猜测改页面。

页面不能自己拼复杂查询

页面应该向 Service 要 HomeSnapshot 和 PageResult,而不是直接拿 repository 全量数据后自己排序过滤。

模块 应该负责 不应该负责
HomePage 快照、分页结果、加载态 SQL 条件
HomeService 组合首屏数据 RDB 连接细节
RecordRepository 分页和筛选查询 卡片展示
SummaryStore 轻量摘要缓存 业务按钮
Migration 索引和版本升级 页面生命周期

边界定清后,代码就不会在页面、服务和回调之间来回复制同一段判断。后续新增场景也能先判断应该落在哪一层。

首屏快照只放轻量字段

ts 复制代码
export interface HomeSnapshot {
  todayCount: number
  latestTitle: string
  latestTime: number
  monthTotal: number
  generatedAt: number
}

export interface PageQuery {
  cursorTime: number
  pageSize: number
  typeFilter: string
}

export interface PageResult<T> {
  items: T[]
  nextCursorTime: number
  hasMore: boolean
}

首屏快照是为快速展示设计的,不放完整列表。分页查询用 cursor 表达下一页位置,避免每次都从头读到尾。

这类模型最好放在 modelscommon 中,页面、Service 和仓储都使用同一份类型,避免各层用字符串互相猜。

Repository 只提供分页读

ts 复制代码
export class RecordRepository {
  async queryPage(query: PageQuery): Promise<PageResult<RecordItem>> {
    const rows = await this.queryRows({
      beforeTime: query.cursorTime,
      limit: query.pageSize + 1,
      typeFilter: query.typeFilter
    })
    const items = rows.slice(0, query.pageSize)
    const last = items[items.length - 1]
    return {
      items,
      nextCursorTime: last?.createdAt ?? 0,
      hasMore: rows.length > query.pageSize
    }
  }
}

Repository 不返回全量列表。多查一条用来判断 hasMore,页面只关心下一页 cursor,不需要知道底层查询细节。

Service 的目标不是把所有逻辑都塞进去,而是把跨页面、跨生命周期或需要持久化的判断集中起来。页面只表达用户动作。

首页先显示快照,再加载列表

ts 复制代码
@Component
struct RecordHomePage {
  @State snapshot: HomeSnapshot = createEmptyHomeSnapshot()
  @State records: RecordItem[] = []
  @State loadingMore: boolean = false
  private service: HomeRecordService = createHomeRecordService()

  async aboutToAppear(): Promise<void> {
    this.snapshot = await this.service.loadHomeSnapshot()
    await this.loadFirstPage()
  }

  private async loadFirstPage(): Promise<void> {
    const page = await this.service.loadRecords({ cursorTime: Date.now(), pageSize: 20, typeFilter: '' })
    this.records = page.items
  }
}

首屏不会等月统计重新计算完成才显示。快照先给用户一个稳定首页,列表再分页补齐。

页面层要控制展示和交互节奏,但不应该拥有底层事实。这样页面重建、横竖屏变化或返回前台时,都能重新从 Service 拿到可信结果。

写入后同步更新摘要

ts 复制代码
export class HomeRecordService {
  constructor(
    private readonly repository: RecordRepository,
    private readonly summaryStore: SummaryStore
  ) {}

  async saveRecord(input: SaveRecordInput): Promise<void> {
    const record = normalizeRecord(input)
    await this.repository.save(record)
    await this.summaryStore.updateAfterWrite(record)
  }

  async loadHomeSnapshot(): Promise<HomeSnapshot> {
    return this.summaryStore.loadSnapshot()
  }
}

摘要不是页面临时算出来的,而是在写入路径维护。这样下一次打开首页时能直接读快照,不用全表重算。

实际项目里,很多问题不是第一次进入页面暴露,而是在冷启动、返回前台、页面重建或回调晚到时暴露。把这段生命周期补上,修复才算闭环。

慢查询要留下耗时证据

情况 页面表现 工程处理
首屏快照慢 记录 SummaryStore 耗时 检查是否退回全表统计
分页慢 记录 cursor 和 pageSize 检查索引和过滤字段
写入后摘要错 对比原始记录重算 修 updateAfterWrite
列表渲染慢 查询快但首帧慢 再看组件和图片

兜底路径不是可有可无。用户遇到异常时,页面至少要给出当前状态、可执行动作和可排查线索。

排查时找全量查询和排序

powershell 复制代码
rg -n "queryAll|findAll|ORDER BY|sort\(|filter\(" entry features common
rg -n "limit|offset|cursor|pageSize|SummaryStore|HomeSnapshot" entry features common
rg -n "aboutToAppear|onShown|loadHome|loadRecords" entry features common

第一组命令找旧路径,第二组命令找新边界,第三组命令看生命周期或回调位置。命中后不要只看字段名,要判断它是否仍在直接改页面状态。

索引和摘要要跟版本迁移一起走

很多首屏慢的问题不是某一次查询写错,而是版本演进时没有同步维护本地结构。新增筛选条件后,页面开始按 typecreatedAtupdatedAt 查,但迁移里没有补索引;新增首页统计后,写入路径没有更新摘要,只能在首页临时全量重算。

变更 需要同步处理 不处理的后果
新增时间排序 给时间字段补索引 数据多后排序拖慢首屏
新增类型筛选 给类型和时间组合查询设计路径 筛选页每次全表过滤
新增首页统计 写入后更新摘要 首页打开时全量重算
新增导入功能 导入结束后重建摘要 导入后统计和列表不一致

所以 RDB 优化不是最后才做的性能补丁。每次本地数据结构变化,都应该顺手问一句:首屏读什么、列表怎么分页、摘要在哪里更新、旧版本升级后这些结构是否存在。

验证清单

验证项 预期结果
千条记录冷启动 先出现快照,再分页出列表
新增记录 首页摘要立即更新
加载更多 每次只追加一页
筛选切换 查询耗时可记录
无记录 空状态不触发全量统计循环

这些验证最好在真机或模拟器里按顺序走一遍。只看源码很难发现冷启动、重启和回调晚到这类问题。

常见问题和处理方式

现象 常见原因 处理方式
首屏 2 秒 全量查加统计阻塞 快照和分页拆开
越用越慢 无索引且全表排序 迁移补索引
统计不准 页面手动改计数 写入后统一更新摘要
列表卡顿 一次渲染全部记录 分页加载

如果现象没有出现在表里,仍然建议按"输入来源 -> 中间状态 -> 持久化或页面展示 -> 失败兜底"的顺序排查。

小结:首屏不要背负全量数据

RDB 首屏慢要先拆读路径。首页读快照,列表分页,统计在写入或后台维护,慢查询有日志。这样数据增长后,首屏仍然是一个可控链路,而不是所有本地数据的总入口。

|

| 列表卡顿 | 一次渲染全部记录 | 分页加载 |

如果现象没有出现在表里,仍然建议按"输入来源 -> 中间状态 -> 持久化或页面展示 -> 失败兜底"的顺序排查。

小结:首屏不要背负全量数据

RDB 首屏慢要先拆读路径。首页读快照,列表分页,统计在写入或后台维护,慢查询有日志。这样数据增长后,首屏仍然是一个可控链路,而不是所有本地数据的总入口。

相关推荐
不羁的木木12 小时前
HarmonyOS技术精讲-Connectivity Kit:蓝牙进阶——GATT服务与音频通道
华为·音视频·harmonyos
Helen_cai12 小时前
HarmonyOS ArkTS 实战:实现一个校园快递代取与跑腿应用
华为·harmonyos
2301_7681034912 小时前
HarmonyOS趣味相机实战第13篇:CameraPicker与CameraKit选型、系统拍照回退及架构边界
harmonyos·arkts·架构设计·camerapicker·camerakit
2301_7681034913 小时前
HarmonyOS趣味相机实战第14篇:装饰素材强类型建模、锚点语义与套装组合校验
harmonyos·arkts·数据建模·arkui·相机贴纸
不羁的木木13 小时前
HarmonyOS技术精讲-Connectivity Kit:Wi-Fi进阶能力——热点、P2P与Aware
asp.net·harmonyos·p2p
Helen_cai14 小时前
HarmonyOS ArkTS 实战:实现一个校园宿舍管理与查寝应用
华为·harmonyos
listening77714 小时前
HarmonyOS ArkTS 实战:实现一个校园打印复印预约应用
华为·harmonyos
绝世番茄14 小时前
鸿蒙原生 ArkTS 布局之道:Grid 替代多层嵌套布局深度解析
华为·harmonyos·鸿蒙
●VON14 小时前
鸿蒙 PC Markdown 编辑器可靠性设计:未保存内容的崩溃恢复闭环
华为·中间件·编辑器·harmonyos·鸿蒙
listening77714 小时前
《从算法到落地:基于HarmonyOS API 23的端侧图像推理实战》
算法·华为·harmonyos