HarmonyOS 7 API 26 RDB 游标分页优化示意图

先说问题:列表分页慢,很多时候不是页面渲染慢

HarmonyOS 7 / API 26 项目里,只要有历史记录、搜索结果、消息列表、收藏列表或离线数据,就绕不开分页查询。页面看起来是列表卡顿,但根因可能在 RDB 查询:offset 越翻越大、排序字段没索引、下一页边界没记住。

这篇只拆一个具体问题:RDB 分页查询怎么从 offset 改成游标条件,避免越翻越慢,同时保证删除、插入数据后列表不乱。

环境和版本

项目 取值
系统方向 HarmonyOS 7.0.0 / API 26
开发语言 ArkTS
数据方向 RDB 分页查询、索引、游标条件、边界验证
验证方式 offset 慢查询案例 + cursor 稳定分页案例

问题是怎么发生的

很多分页一开始会这样写:

function buildOffsetSql(page: number, pageSize: number) {
  const offset = (page - 1) * pageSize
  return "SELECT * FROM food_history ORDER BY updated_at DESC LIMIT " + pageSize + " OFFSET " + offset
}

前几页没问题,翻到后面就慢。因为 offset 不是直接跳到目标位置,它仍然需要数据库跳过前面的数据。数据越多,跳过成本越高。

案例一:offset 越翻越慢怎么复现

先用一个简单估算函数模拟成本。真实数据库不一定是这个数,但趋势是一样的:offset 越大,扫描成本越高。

type PageQuery = { page: number; pageSize: number }

function estimateOffsetCost(query: PageQuery) {
  const offset = (query.page - 1) * query.pageSize
  return offset + query.pageSize
}

console.log(estimateOffsetCost({ page: 2, pageSize: 20 }))
console.log(estimateOffsetCost({ page: 200, pageSize: 20 }))

输出:

40
4000

这说明第 200 页不是只取 20 条,而是要处理更大的跳过成本。用户感觉是“列表越翻越卡”,实际是查询方式不适合深分页。

案例二:用游标条件稳定翻页

游标分页的思路是:第一页按 updated_at + id 排序;下一页带上上一页最后一条的 updatedAt 和 id,查询更早的数据。

type PageCursor = { updatedAt: number; id: string }

function buildCursorSql(cursor: PageCursor | null, pageSize: number) {
  if (!cursor) {
    return "SELECT * FROM food_history ORDER BY updated_at DESC, id DESC LIMIT " + pageSize
  }
  return "SELECT * FROM food_history WHERE updated_at < " + cursor.updatedAt +
    " OR (updated_at = " + cursor.updatedAt + " AND id < "" + cursor.id + "")" +
    " ORDER BY updated_at DESC, id DESC LIMIT " + pageSize
}

第一页:

console.log(buildCursorSql(null, 20))

下一页:

console.log(buildCursorSql({ updatedAt: 1795600000000, id: "food_100" }, 20))

输出能看到第二页不再依赖 page=200 这种数字,而是依赖上一页最后一条记录。

索引怎么配合

游标分页必须配合排序字段索引,否则只是换了 SQL 字符串。这个方向建议建立组合索引:

CREATE INDEX IF NOT EXISTS idx_food_history_time_id
ON food_history(updated_at DESC, id DESC);

这个索引对应 ORDER BY updated_at DESC, id DESC,也对应下一页的游标条件。查询条件和排序字段要一起设计,不能只给单个字段随便加索引。

删除或插入数据后怎么避免乱页

offset 分页在数据变化后容易错页。例如第一页加载后,有新数据插进来,第二页 offset 还是 20,就可能重复或漏掉。游标分页用的是上一页最后一条记录,抗变化能力更好。

可以加一个去重集合兜底:

function appendUnique<T extends { id: string }>(oldList: T[], nextList: T[]) {
  const exists = new Set(oldList.map(item => item.id))
  const merged = [...oldList]
  for (const item of nextList) {
    if (!exists.has(item.id)) merged.push(item)
  }
  return merged
}

这个兜底不能替代正确 SQL,但能防止边界场景下 UI 重复展示。

几种方案怎么选

方案 优点 问题 适合场景
offset 分页 写法简单 深分页越来越慢 数据少、后台工具
游标分页 深分页稳定 不能随便跳页 App 列表、时间流
游标 + 组合索引 查询更稳 要设计字段 正式项目优先
游标 + 去重集合 UI 更稳 仍要控制内存 动态列表

我的选择是游标 + 组合索引。只要列表会长期增长,就不要把 offset 当默认方案。

可以封装成什么

可以把 SQL 构建封成 CursorPageQuery:

export class CursorPageQuery {
  constructor(private pageSize: number) {}

  next(cursor: PageCursor | null) {
    return buildCursorSql(cursor, this.pageSize)
  }

  merge<T extends { id: string }>(oldList: T[], nextList: T[]) {
    return appendUnique(oldList, nextList)
  }
}

页面只保存最后一条 cursor,不需要知道 SQL 细节。后面换表、换字段、换排序规则,也只改这个类。

最后怎么验收

我会看五个结果:深分页不再依赖巨大 offset;SQL 排序字段和索引字段一致;下一页由上一页最后一条记录决定;插入新数据后不会重复展示;删除数据后不会直接跳空。

RDB 分页优化不要只看 UI 列表。真正要查的是 SQL、索引和分页边界。HarmonyOS 7.0.0 / API 26 项目里,只要本地数据会增长,就应该尽早把游标分页设计好。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐