FEATURED · 精选文章

找回昨天的自己:ArkTS 时间分组与关键词搜索的查询实现

发布时间 / 2026/8/16 3:47:22
来源 / 创域科博编辑部
栏目 / 资讯中心
找回昨天的自己:ArkTS 时间分组与关键词搜索的查询实现 实例电子日记本Diary技术时间分组查询、关键词 LIKE 搜索一、日记查询的两大需求日记应用的查询需求与记账本完全不同记账本要「算」SUM/GROUP BY日记本要「找」——在个人编年史里快速定位某段时间、某篇日记。具体拆解为两个核心查询按时间找时间轴浏览。用户想知道「6 月写了什么」——按月份/日期分组展示按内容找关键词搜索。用户记得「那篇提到爬山的日记」——标题/正文模糊匹配。本篇文章逐一讲解这两种查询在 ArkTS RDB 中的实现以及一个必须避开的经典坑。二、时间分组全量加载 前端分组先看需求本质时间轴页面需要把日记按「日期」分组展示。最直观的 SQL 方案是按日期 GROUP BY但日记的时间粒度是「天」——每天可能 0 篇或 1 篇GROUP BY 出来的分组跟原始列表差异不大还得额外处理空日期。落地版采用了更务实的方案数据层全量按时间倒序返回页面在渲染时用方法动态分组。数据层查询已在 4-1 展示staticasyncqueryAll(context:common.Context):PromiseDiary[]{conststoreawaitDiaryDao.getStore(context);constpredicatesnewrelationalStore.RdbPredicates(DiaryDao.TABLE);predicates.orderByDesc(created_time);constresultawaitstore.query(predicates);returnDiaryDao.collect(result);}页面拿到按时间倒序的完整数组后时间轴节点直接由fmtDate(d.createdTime)生成——日期本身就是分组的自然呈现每一篇日记的左侧节点显示它的日期视觉上自动形成「同一日期相邻」的分组效果privatefmtDate(ts:number):string{constdnewDate(ts);return${d.getFullYear()}-${String(d.getMonth()1).padStart(2,0)}-${String(d.getDate()).padStart(2,0)};}privatefmtMonth(ts:number):string{constdnewDate(ts);return${d.getFullYear()}年${d.getMonth()1}月;}为什么全量加载是合理选择三个前提条件都满足数据量小个人日记一年撑死一两百篇全量加载内存占用不到 1MB查询简单没有分页需求时间轴一屏展示全部滚动浏览分组逻辑简单按日期展示SQL 分组反而多此一举。如果数据量到上万篇就应改为「按月查询」queryByMonth(context, 2025-06)用between(月初毫秒, 月末毫秒)拉取单月数据再前端按日分组——这是数据量增长后的自然演进路径。DAO 里已经备好了queryByMonth方法4-1 文章讲过它的 LIKE 写法读者可以自行改造成 between 版本。三、关键词搜索双字段 OR LIKE「找回昨天」的另一个入口是搜索。用户记不清日期只记得内容片段这时标题/正文的模糊匹配就派上用场staticasyncsearch(context:common.Context,keyword:string):PromiseDiary[]{conststoreawaitDiaryDao.getStore(context);constpredicatesnewrelationalStore.RdbPredicates(DiaryDao.TABLE);predicates.like(title,%${keyword}%).or().like(content,%${keyword}%).orderByDesc(created_time);constresultawaitstore.query(predicates);returnDiaryDao.collect(result);}SQL 语义拆解。RdbPredicates 链式调用最终生成SELECT*FROMdiaryWHEREtitleLIKE%关键词%ORcontentLIKE%关键词%ORDERBYcreated_timeDESCLIKE %关键词%两侧都有%通配符表示「任意位置包含」——这是模糊搜索的标配.or()把前后两个条件连接为 OR。RdbPredicates 的.or()与.and()是条件连接器默认 AND 语义需要 OR 时必须显式调用orderByDesc(created_time)搜索结果仍按时间倒序与时间轴视图一致用户检索后看到的还是时间线。执行流程页面onSearch拿到输入 →DiaryDao.search查询 → 返回结果直接替换this.diaries→ List 重新渲染。输入清空时refresh()恢复全量。整个过程数据层只做一件事LIKE 匹配 时间排序。四、LIKE 的性能边界为什么这个坑值得讲LIKE 模糊搜索有一个众所周知的性能特性前导%会让索引失效。LIKE 关键词%前缀匹配可以走 B 树索引——SQLite 能利用索引快速定位到以「关键词」开头的行而LIKE %关键词%包含匹配无法走索引——因为目标字符串可能在字段的任意位置索引的排序优势完全失效只能全表扫描。这对日记本意味着什么看数据规模数据量全表扫描耗时结论几百条个人日记微秒级无感随便用几万条团队笔记毫秒级可接受百万条全文检索秒级必须换方案个人日记场景几百条数据LIKE %关键词%全表扫描微秒级完成性能完全不是问题。在数据量小的场景优先考虑实现简洁性不必过度优化——这是本实例与「生产大数据量」场景的关键区别。真到了十万级解决方案是 SQLite 的FTS5 全文索引CREATE VIRTUAL TABLE diary_fts USING fts5(title, content)MATCH查询那是另一个量级的技术本书后续不展开。同样受 LIKE 坑影响的还有 4-1 文章的queryByMonthlike(created_time, 2025-06%)在 INTEGER 时间戳上既不匹配也不走索引——这是文章版代码的遗留设计落地版页面已绕开全量加载 前端分组读者在使用 queryByMonth 前务必理解这一点。五、组合条件AND 与 OR 的链式写法如果搜索需求升级为「标题含关键词且天气是晴」条件组合怎么写RdbPredicates 支持混合链式// 标题含关键词 且 天气晴predicates.like(title,%关键词%).and().equalTo(weather,晴);// (标题含关键词 或 正文含关键词) 且 天气晴predicates.like(title,%关键词%).or().like(content,%关键词%).and().equalTo(weather,晴);注意 OR 与 AND 的优先级RdbPredicates 的链式条件按书写顺序从左到右组合没有 SQL 里的括号优先级概念AND优先于OR。如果你的逻辑需要「(A OR B) AND C」这样的括号语义当前写法恰好符合先 OR 后 AND但如果是「A AND (B OR C)」链式写法会生成错误语义——这种情况应改用querySql手写 SQL 并显式加括号constresultawaitstore.querySql(SELECT * FROM${DiaryDao.TABLE}WHERE title LIKE %${kw}% AND (weather 晴 OR location 家) ORDER BY created_time DESC);安全警示手写 SQL 时kw来自用户输入直接拼接有 SQL 注入风险。生产环境应对用户输入做转义把替换为或改用参数化查询。RdbPredicates 的 like 方法自带参数化无此问题——所以能用 RdbPredicates 就用 RdbPredicates只有复杂括号逻辑才退回到手写 SQL且必须消毒输入。六、双时间戳在查询中的协作4-1 文章介绍了 created_time 与 updated_time 双字段。在查询层面它们各司其职字段查询用途语义created_time排序、时间轴分组「什么时候写的」updated_time编辑后刷新「什么时候改的」当前页面排序用created_time时间轴按写作时间组织。如果产品想支持「最近编辑优先」视图只需把 queryAll 的排序字段换成updated_timepredicates.orderByDesc(updated_time);// 切换为编辑时间排序一行改动即可切换两种时间语义——这就是双时间戳设计带来的灵活性。6.1 从「按月查询」到「按天分组」的完整演进如果我们进一步思考会发现时间查询还有更细的粒度需求某一天写了什么日历视图点击某天、某个星期周报视图、某个季度季度回顾。这些都可以用同一套 between 模式扩展// 某天区间00:00:00.000 ~ 23:59:59.999staticasyncqueryByDay(context:common.Context,day:number):PromiseDiary[]{conststartnewDate(day).setHours(0,0,0,0);constendstart86399999;conststoreawaitDiaryDao.getStore(context);constpredicatesnewrelationalStore.RdbPredicates(DiaryDao.TABLE);predicates.between(created_time,start,end).orderByDesc(created_time);constresultawaitstore.query(predicates);returnDiaryDao.collect(result);}毫秒区间的边界技巧86399999是一天毫秒数86400000减 1——因为时间戳是连续整数[start, start86400000)是半开区间用86399999得到闭区间[start, end]。这个「毫秒区间端点计算」在报表类查询里反复出现记住dayMs - 1这个细节可以避免「少查一条/多查一条」的边界 bug。6.2 时间分组的前端渲染细节前面提到页面用fmtDate动态分组。真正的渲染代码里还有一个细节月份标签只在「月份变化的第一条」显示避免每张卡片都重复显示「2025年6月」造成视觉噪音// 伪代码示意遍历时记录上一个月份变化时才渲染月份标题letlastMonth;for(constdofdiaries){constmonthfmtMonth(d.createdTime);if(month!lastMonth){renderMonthHeader(month);// 渲染月份分组标题lastMonthmonth;}renderDiaryCard(d);}时间轴页面4-2 文章用更简单的方式——每张卡片底部都显示月份靠视觉重复形成分组感如果要做「月份吸顶标题」的精美效果上述「变化时渲染」模式就是标准实现。两种方案按产品要求取舍。七、技术要点对照表技术点实现方式适用场景时间分组全量加载 前端按日期渲染个人日记小数据量月查询between(月初, 月末)改造 queryByMonth大数据量演进方案天查询between(dayStart, dayStart86399999)日历视图点击某天关键词搜索title/content OR LIKE标题正文双字段命中条件组合RdbPredicates 链式 and/or简单组合复杂括号手写 querySql 输入消毒AND 与 OR 嵌套LIKE 前导%全表扫描小数据量可接受大数据量换 FTS5八、常见问题 FAQQ1为什么 search 的结果顺序和时间轴一致Asearch 方法里显式orderByDesc(created_time)保证搜索结果仍是时间线视图——用户搜索后看到的还是按时间排的故事线而不是随机的命中列表。这是产品层面的有意设计。Q2中文分词搜索怎么办ALIKE ‘%关键词%’ 不做分词——用户输入「爬山」只能命中包含这两个连续字的日记。中文场景如果需要「输入 山 也命中 爬山」需要分词库如 jieba或 FTS5 的 tokenizer 配置。个人日记场景「整词命中」已够用生产级搜索另开文章讲解。Q3搜索太慢怎么办A先确认数据量。几百条慢是心理作用几万条才是真慢。真慢了三个方案给 title 建索引只对前缀 LIKE 有效、限制搜索范围只搜标题、上 FTS5 全文索引。Q4String(d.getMonth() 1).padStart(2, 0)为什么必须补零A不补零会得到2025-6-1这种变长字符串字典序排序错乱2025-10-1会排在2025-6-1前面因为 ‘1’ ‘6’。所有日期格式化工具必须统一YYYY-MM-DD定长格式这是时间字段字符串化方案的铁律。Q5queryByMonth 到底能不能用A能用但要注意——落地版里它的 LIKE 写法对 INTEGER 时间戳不生效需要按本文章节 6.1 改成 between 区间。如果你已经按文章版代码实现务必修正。Q6搜索框输入很快会不会频繁查询数据库A当前实现每次 onChange 都查询。个人日记数据量小无感若担心性能可加 300ms 防抖——setTimeout延迟执行查询用户停顿后才真正查询。实例 2 通讯录已经演示过防抖写法可以对照参考。Q7OR LIKE 的查询能命中「标题有、正文也有」的记录吗A会命中一次而不是两次——OR 是逻辑合并一行记录只要标题或正文任一匹配就返回该行一次。COUNT不会重复计数这是 SQL 集合语义的天然保证。Q8FTS5 和 LIKE 可以共存吗A可以。FTS5 是虚拟表CREATE VIRTUAL TABLE需要额外的同步逻辑触发器或手动同步。小数据量用 LIKE 足够大数据量再迁移 FTS5两者可以在不同阶段并存过渡。九、文章小结本篇文章讲解了日记本的两大查询时间分组全量 前端分组与关键词搜索双字段 OR LIKE并顺带澄清了两个关键坑INTEGER 时间戳不能 LIKE 匹配月份queryByMonth 的正确用法是 between 区间、LIKE 前导 % 导致索引失效小数据量无感大数据量换 FTS5。查询逻辑的核心心法根据数据量选方案简单场景用简单做法复杂场景再升级。从天查询到季度查询between 毫秒区间模式可以覆盖所有时间粒度。下一篇4-4将展示 12 篇跨月日记种子数据如何让时间轴「饱满」起来验证本篇文章的查询在真实数据下的效果。补充时间范围查询细节补充 1按日期字符串比较的范围查询写法前面章节的范围查询全部基于 INTEGER 时间戳的 between。如果建表时把时间存成YYYY-MM-DD HH:mm:ss定长字符串范围查询可以直接用字符串比较——定长格式的字典序就是时间序这是字符串方案能成立的前提// 日期字符串方案查 2025-06-01 到 2025-06-30 的日记predicates.greaterThanOrEqualTo(created_time,2025-06-01 00:00:00).and().lessThanOrEqualTo(created_time,2025-06-30 23:59:59);注意字符串比较要求格式严格定长年-月-日-时分秒位数字数固定2025-06-30 2025-06-01的字典序判断才等价于时间先后一旦出现2025-6-1这种变长写法Q4 的坑整个字符串方案立即失效。补充 2跨月查询的边界处理跨月查询最容易漏掉首尾两天。以「6 月 28 日到 7 月 3 日」为例两种方案的端点计算不同方案起始端点结束端点易错点between 毫秒首日 00:00:00.000末日 23:59:59.999忘记-1会多查一天日期字符串YYYY-MM-DD 00:00:00YYYY-MM-DD 23:59:59漏写时间部分会截断首尾两天字符串方案还有个更稳的写法用半开区间[start, nextStart)即结束条件写成lessThan(created_time, 2025-07-04 00:00:00)——这样完全不用关心 6 月有 30 天还是 28 天跨月/跨年一律用「下一个月初」作右端点彻底规避月末天数计算// 跨月半开区间6 月 28 日到 7 月 3 日predicates.greaterThanOrEqualTo(created_time,2025-06-28 00:00:00).and().lessThan(created_time,2025-07-04 00:00:00);补充 3时间戳与日期字符串的选择对比维度INTEGER 时间戳日期字符串排序/比较数字比较天然正确依赖定长格式保证字典序LIKE 匹配月份完全无效见 6.12025-06%直接命中存储/索引4 字节索引紧凑19 字符索引略大可读性需 fmtDate 格式化肉眼可读调试方便个人日记「排序为主、按月浏览为辅」选时间戳更优若业务大量按月份前缀查询报表按月汇总日期字符串反而省心——但必须守住定长格式铁律且范围查询优先用半开区间。FAQQ1字符串范围查询和 between 毫秒哪个更快A都是范围扫描量级相同。INTEGER 时间戳索引更紧凑、扫描 I/O 略少但格式统一的字符串比较同样走索引。个人日记数据量下性能完全无感按查询习惯选即可。Q2跨月查询有没有现成 APIA没有按月专用 API但可封装queryByMonthRange(year, month)用「本月月初」和「下月月初」构造半开区间天然规避月末 31/30/29 天的计算这是最稳的跨月写法。Q3日期字符串能 LIKE 查月份时间戳不行是不是字符串更好A看 LIKE 是否核心需求。日记时间轴是「按范围浏览」而非「按前缀搜索」LIKE 月份只是文章版妥协设计。排序、范围、索引三项时间戳均占优不建议为了一个可绕开的需求换存储格式。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻