ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

分页查询的基石:ArkTS 为鸿蒙商品列表设计 LIMIT/OFFSET 的表

分页查询的基石:ArkTS 为鸿蒙商品列表设计 LIMIT/OFFSET 的表 实例商品分页列表Product技术商品字段、批量种子生成、ProductDao一、业务需求分析当数据量变大列表需要分页前面的实例里列表都是「一次加载全部」——几十条数据毫秒级加载全量没问题。但电商商品列表的数据量是千、万、十万级一次把 10 万件商品全部查出来塞进内存App 直接卡死。分页Pagination就是解决这个问题的标准方案每次只加载一页如 10 条滚动到底部再加载下一页让无限数据流在有限内存里流畅呈现。实例 8 的核心需求分页查询LIMIT pageSize OFFSET offset——每页 10 条按销量排序翻页加载懒加载滚动到底部自动加载下一页onReachEnd触发总条数统计头部显示「共 N 件商品」让用户知道总量COUNT(*)分类筛选按分类过滤后再分页组合条件 分页双列瀑布流两列网格展示商品卡片List 的 lanes 多列布局。这五个需求组合就是电商列表的完整形态。数据层核心是LIMIT/OFFSET 分页——本实例的技术主线。二、字段设计电商商品表商品表 goods字段名类型约束说明idINTEGERPRIMARY KEY AUTOINCREMENT自增主键nameTEXTNOT NULL商品名称priceREALNOT NULL现价元original_priceREALNOT NULL DEFAULT 0划线价原价salesINTEGERNOT NULL DEFAULT 0销量imageTEXTDEFAULT ‘’占位图 emojitagTEXTDEFAULT ‘’标签热卖/新品/清仓categoryTEXTDEFAULT ‘其他’分类created_timeINTEGERNOT NULL上架时间戳设计要点1. original_price 划线价。电商标配——现价 划线原价制造「折扣感」¥99.00¥138.60。原价通常 现价UI 上原价加删除线。2. image 用 emoji 占位。真实电商存图片 URLDemo 用 emoji做占位图——零资源依赖页面也直观。生产环境替换为image_url TEXT即可。3. sales 销量字段。排序依据——电商列表通常按销量/热度排序ORDER BY sales DESC让爆款在前。销量是「运营指标」每次下单 1本实例由种子数据指定。4. tag 标签。热卖/新品/清仓页面用红色小徽标展示。可空DEFAULT 无标签的商品不显示徽标。5. created_time 上架时间。排序备选字段按新品排序本实例未用但保留——字段设计为「可能用到的查询」预留。三、建表 SQL 与索引CREATETABLEIFNOTEXISTSgoods(idINTEGERPRIMARYKEYAUTOINCREMENT,nameTEXTNOTNULL,priceREALNOTNULL,original_priceREALNOTNULLDEFAULT0,salesINTEGERNOTNULLDEFAULT0,imageTEXTDEFAULT,tagTEXTDEFAULT,categoryTEXTDEFAULT其他,created_timeINTEGERNOTNULL);CREATEINDEXIFNOTEXISTSidx_goods_salesONgoods(sales);idx_goods_sales索引的意义分页查询的核心排序是ORDER BY sales DESC——没有索引SQLite 每次都要全表排序有索引直接走索引倒序遍历配合LIMIT只取前 N 条性能提升显著。分页 排序的组合必须建索引这是分页查询的第一性能法则。四、ProductDao 封装分页查询的两种形态数据层核心ProductDao。分页查询有「纯分页」和「分类过滤分页」两种形态形态一纯分页按销量倒序staticasyncqueryPage(context:common.Context,limit:number,offset:number):PromiseGoods[]{conststoreawaitProductDao.getStore(context);constpredicatesnewrelationalStore.RdbPredicates(ProductDao.TABLE);predicates.orderByDesc(sales).limitAs(limit).offsetAs(offset);constresultawaitstore.query(predicates);returnProductDao.collect(result);}limitAs(limit).offsetAs(offset)是 RdbPredicates 的分页 API等价于 SQL 的LIMIT ${limit} OFFSET ${offset}——取「从第 offset 条开始的 limit 条」。形态二分类过滤分页staticasyncqueryPageByCategory(context:common.Context,limit:number,offset:number,category:string):PromiseGoods[]{conststoreawaitProductDao.getStore(context);constpredicatesnewrelationalStore.RdbPredicates(ProductDao.TABLE);if(category!全部){predicates.equalTo(category,category);}predicates.orderByDesc(sales).limitAs(limit).offsetAs(offset);constresultawaitstore.query(predicates);returnProductDao.collect(result);}关键细节category ! 全部时才加过滤条件——「全部」是页面的特殊筛选值语义是「不过滤」。把「全部」的语义判断放在 DAO 层而非页面拼接 SQL让 DAO 接口保持「分类参数直达」的干净。五、OFFSET 的语义与翻页计算理解 OFFSET 是分页的关键。假设 pageSize10页码从 0 开始页码 pageOFFSET 计算取出的记录00 × 10 0第 1~10 条11 × 10 10第 11~20 条22 × 10 20第 21~30 条nn × 10第 10n1 ~ 10n10 条页面层的翻页状态管理ProductPageStatepageSize:number10;Statepage:number0;Stategoods:Goods[][];Stateloading:booleanfalse;Statefinished:booleanfalse;asyncloadMore():Promisevoid{if(this.loading||this.finished){return;// 加载中或已加载完忽略}this.loadingtrue;constoffsetthis.page*this.pageSize;constlistawaitProductDao.queryPageByCategory(this.context,this.pageSize,offset,this.category);if(list.length0){this.goodsthis.goods.concat(list);// 追加到已有列表this.page;}if(list.lengththis.pageSize){this.finishedtrue;// 返回不足一页 → 没有更多了}this.loadingfalse;}翻页三要素page 自增每次成功加载this.pageOFFSET page × pageSize 递进concat 追加this.goods.concat(list)把新页追加到已有列表而不是替换——列表持续增长形成「瀑布流」finished 判定返回条数 pageSize 说明最后一页了或数据不足一页置 finished 停止加载——这是分页终止的经典判断。防重入if (this.loading || this.finished) return——防止快速滚动时 onReachEnd 连续触发导致重复加载同一页。loading 标志位是懒加载的防抖闸门。六、总条数统计与分类计数总条数头部显示staticasynccount(context:common.Context):Promisenumber{conststoreawaitProductDao.getStore(context);constresultawaitstore.querySql(SELECT COUNT(*) AS c FROM${ProductDao.TABLE});lettotal0;if(result.goToNextRow()){totalresult.getLong(result.getColumnIndex(c));}result.close();returntotal;}分类计数筛选条徽标staticasynccategoryCounts(context:common.Context):PromiseRecordstring,number{conststoreawaitProductDao.getStore(context);constresultawaitstore.querySql(SELECT category, COUNT(*) AS cnt FROM${ProductDao.TABLE}GROUP BY category);constmap:Recordstring,number{};while(result.goToNextRow()){map[result.getString(result.getColumnIndex(category))]result.getLong(result.getColumnIndex(cnt));}result.close();returnmap;}GROUP BY category一次统计出每个分类的商品数——页面筛选条用Object.keys拿到分类列表同时知道每类数量。分类筛选条的数据源就是这条 GROUP BY。七、技术要点对照表技术点实现方式生产价值分页limitAs offsetAs每次只取一页排序orderByDesc(‘sales’)爆款在前分页索引idx_goods_sales排序走索引翻页状态page 自增 concat瀑布流增长终止判断返回 pageSize最后一页检测防重入loading 标志懒加载防抖总条数COUNT(*)头部总量展示八、文章小结商品分页的数据层核心是LIMIT/OFFSET 销量索引 翻页状态机limitAs/offsetAs 取页、idx_goods_sales 保排序性能、page/concat/finished 三要素驱动翻页、loading 防重入。这是所有「大数据量列表」应用电商、信息流、通讯录分页的地基。分类过滤分页展示了「条件 分页」的组合写法是分页的高级形态。下一篇8-2展示双列瀑布流 UI——触底加载动画 总数卡 分类筛选条。九、字段设计详解价格 REAL、库存 INTEGER 与销量排序的取舍上一节的字段表一笔带过这一节把「每个字段为什么选这个类型」讲透——类型选择不是随意背后是数据库存储与查询的权衡。9.1 price 用 REAL小数价格直存不做分转换商品价格是小数¥9.9、¥138.60SQLite 的 REAL 是 8 字节浮点数直接存小数、直接读出代码零转换成本。另一种方案是「以分为单位存 INTEGER」¥9.90 存 990精度绝对可靠但每次读写都要乘除 100且容易忘记转换出 bug。Demo 用 REAL 图直观生产环境金额建议 INTEGER 分存储——金融数据对浮点误差零容忍。方案存储精度代码成本适用REAL 直存9.9有浮点误差零转换Demo、展示类INTEGER 分990绝对精确乘除 100生产、支付场景9.2 stock 库存用 INTEGER整数语义 原子扣减库存是「件」——离散整数不存在 3.5 件。INTEGER 4 字节-21 亿~21 亿存库存绰绰有余且整数语义让「扣库存」可以写成原子 SQLUPDATEgoodsSETstockstock-1WHEREid?ANDstock0;AND stock 0保证库存不为负——并发下单时这个条件就是防超卖的第一道闸门。如果用 REAL 存库存stock - 1的语义和比较都会变得含混。9.3 sales 销量冗余聚合字段为排序而生销量本质是「订单数的聚合结果」理论上可以COUNT(*)现算。但每次列表展示都要 JOIN 统计成本高所以电商表普遍冗余一个 sales 字段下单时 1查询时直接ORDER BY sales DESC——用写入时的微小代价换查询时的高性能这是「读多写少」场景的经典折中也是分页排序的直接数据来源。9.4 字段类型对照总表字段类型设计理由price / original_priceREAL小数价格直存stockINTEGER库存整数 原子扣减salesINTEGER销量聚合冗余排序依据name / tag / category / imageTEXT变长字符串created_timeINTEGER毫秒时间戳十、LIMIT/OFFSET 原理数据库怎么「翻页」10.1 SQL 语义先排序、再跳过、后截断SELECT*FROMgoodsORDERBYsalesDESCLIMIT10OFFSET20;执行分三步① 按 sales 排序全表②跳过前 20 条OFFSET③ 取接下来的 10 条LIMIT返回。注意 OFFSET 的语义是「跳过多少条」不是「第几页」——第 2 页 跳过第一页的 10 条。10.2 RdbPredicates 的 limit/offset 写法RdbPredicates 把 LIMIT/OFFSET 封装成链式方法新旧两套 API 并存// 旧写法limit() / offset()predicates.orderByDesc(sales).limit(10).offset(20);// 当前推荐limitAs() / offsetAs()API 9 起predicates.orderByDesc(sales).limitAs(10).offsetAs(20);两者语义完全一致limitAs/offsetAs是官方演进后的推荐写法本实例全程用后者。链式调用时书写顺序无关——predicates 是命令构建器最终拼出的 SQL 恒为LIMIT n OFFSET m。十一、分页参数设计页大小与页码公式11.1 页大小 pageSize 怎么定pageSize首屏加载翻页频率适用10最快高商品瀑布流20快中通用列表50较慢低表格、后台电商列表取10~20首屏秒开 触底加载节奏合适。pageSize 是页面层常量DAO 层只接收参数不写死——翻页接口保持可复用。11.2 页码 → OFFSET 公式SQL 行号从 0 起所以页码也从 0 起公式offset page × pageSizepagepageSize10取出的行0offset0第 1~10 条1offset10第 11~20 条2offset20第 21~30 条若 UI 页码从 1 显示公式变为offset (page - 1) × pageSize——页面显示层与 SQL 偏移层差 1是分页最常见的 off-by-one 坑。本实例页面层直接管理 0 起的 page与 OFFSET 天然对齐绕开了这个坑。11.3 最后一页的边界不足 pageSize 即终止「返回条数 pageSize」意味着要么是最后一页、要么总数据不足一页——两种情况都该停止翻页置 finished。这一判定已写进loadMore是 OFFSET 分页的通用收尾逻辑。十二、索引对排序的作用idx_goods_sales 为什么关键12.1 无索引全表排序 O(n log n)没有索引时ORDER BY sales DESC迫使 SQLite 把全表假设 6 万行读进内存建临时排序树复杂度 O(n log n)且每次翻页都重复这一过程——数据量上十万后开始卡顿。12.2 有索引倒序遍历 O(pageSize)B-tree 索引的叶子节点天然有序。ORDER BY sales DESC直接从索引最右端向左遍历配合 LIMIT 只读前 10 个叶子节点就返回——开销与数据总量无关只与 pageSize 相关。这也是「分页 排序必须建索引」的原理依据。12.3 索引失效与代价函数包裹会失效ORDER BY LOWER(sales)之类对列做运算索引无法命中写放大每条 INSERT/UPDATE 都要同步维护索引树所以只给高频排序字段sales建不为每个字段都建深分页仍慢OFFSET 到第 590 条时索引也得先「跳过」590 个节点——数据百万级时应换游标分页8-3 已对比三种方案。十三、FAQ问题解答LIMIT 和 OFFSET 能互换吗SQL 语法固定LIMIT n OFFSET mRdbPredicates 链式调用顺序无关最终拼出固定顺序页码从 0 还是 1SQL OFFSET 从 0UI 从 1 时offset (page - 1) × pageSize注意 off-by-onelimitAs 和 limit 有什么区别同一能力的两代 APIlimitAs/offsetAs 为当前推荐写法为什么 LIMIT 10 还是很慢通常慢在 ORDER BY 未走索引或 OFFSET 过大深分页索引建了为什么没生效检查是否对列做函数运算、WHERE 与 ORDER BY 字段是否一致、是否走了其他索引分页结果会重复或漏数据吗OFFSET 分页在翻页期间若数据变动会偏移本实例静态种子数据无此问题动态数据需游标分页
返回列表