HarmonyOS 7 / API 26 RDB 分页越翻越慢怎么办:offset、游标条件和组合索引怎么拆

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 越大,扫描成本越高。
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 项目里,只要本地数据会增长,就应该尽早把游标分页设计好。
更多推荐



所有评论(0)