LIKE 搜索与类型统计:ArkTS 通配符查询在鸿蒙文件库的实战


实例:文件资料库(FileLib)|技术:LIKE 通配符、GROUP BY 统计、批量去重导入
一、文件库查询的三个技术点
文件资料库的查询体系引入了前面实例没有的两类技术:LIKE 通配符搜索(模糊匹配)与 GROUP BY 类型统计(分组聚合)。本篇文章逐一深入,重点讲 LIKE 搜索 与 批量导入去重。
二、LIKE 通配符搜索:名称/标签命中
搜索「需求」要命中「项目需求文档.docx」(名称)和 tags 含「需求」的文件(标签):
static async search(context: common.Context, keyword: string): Promise<FileRecord[]> {
const store = await FileLibDao.getStore(context);
const predicates = new relationalStore.RdbPredicates(FileLibDao.TABLE);
predicates.like('name', `%${keyword}%`)
.or()
.like('tags', `%${keyword}%`)
.orderByDesc('created_time');
const result = await store.query(predicates);
return FileLibDao.collect(result);
}
SELECT * FROM file_lib
WHERE name LIKE '%需求%' OR tags LIKE '%需求%'
ORDER BY created_time DESC;
LIKE 的两个通配符:
%:匹配任意长度字符(含 0 个)——%需求%匹配「需求」在任意位置(开头/中间/结尾);_:匹配单个字符——需求_匹配「需求文」等单字符后缀。
%关键字% 的语义:包含匹配(子串)——「项目需求文档.docx」含「需求」即命中。三种模式:
关键字%:前缀匹配(「项目」开头);%关键字:后缀匹配(「.docx」结尾);%关键字%:包含匹配(任意位置)——搜索框最常用。
LIKE 的 OR 组合:.like('name', ...).or().like('tags', ...)——名称或标签任一命中。为什么用 OR 不用 AND? 搜索语义是「找包含关键字的文件」——名称命中或标签命中都算(不是两个都包含)。多字段搜索的 OR 语义是搜索框的标准。
LIKE 转义注意:关键字本身含 % 或 _ 时需转义(\%)——用户搜「100%」会当通配符。Demo 不做转义(教学简化);生产环境要 replace 转义关键字。「用户输入即通配符」的风险:搜「%」命中所有文件——数据量小时无碍,真实搜索要转义。
LIKE 的性能:%关键字% 无法用普通索引(前导通配符无法走 B+ 树)——全表扫描。数据量小无所谓;大库用全文索引(FTS5)或分词方案。**「LIKE %kw% 一定全表扫」**是索引知识的常识——知道即可,Demo 无需优化。
三、GROUP BY 类型统计:COUNT + SUM 分组
类型筛选条的选项与统计卡需要「每类型的文件数与总大小」:
static async typeStats(context: common.Context): Promise<TypeStat[]> {
const store = await FileLibDao.getStore(context);
const result = await store.querySql(
`SELECT file_type, COUNT(*) AS cnt, SUM(size) AS total_size
FROM ${FileLibDao.TABLE}
GROUP BY file_type
ORDER BY cnt DESC`
);
const list: TypeStat[] = [];
while (result.goToNextRow()) {
const item: TypeStat = {
fileType: result.getString(result.getColumnIndex('file_type')),
count: result.getLong(result.getColumnIndex('cnt')),
totalSize: result.getLong(result.getColumnIndex('total_size')),
};
list.push(item);
}
result.close();
return list;
}
SELECT file_type, COUNT(*) AS cnt, SUM(size) AS total_size
FROM file_lib
GROUP BY file_type
ORDER BY cnt DESC;
-- 文档 | 4 | 15872000
-- 图片 | 3 | 33554432
-- 视频 | 3 | 1300234240
-- 压缩包 | 4 | 389231360
-- 音频 | 2 | 24117248
-- 其他 | 2 | 18874368
GROUP BY 的执行逻辑:按 file_type 分组(同类型的行归一组)→ 每组 COUNT(组内行数)+ SUM(组内 size 和)→ 一行结果。**「分组 → 组内聚合」**是 GROUP BY 的心智模型。
COUNT(*) 与 COUNT(column):COUNT(*) 计行数(含 NULL 行);COUNT(size) 只计非 NULL——本表无 NULL,两者等价。COUNT(*) 是「行数」的通用写法。
ORDER BY cnt DESC:组结果按文件数降序——文档(4 个)排第一。分组结果的排序:ORDER BY 聚合列(cnt)允许——SQLite 支持按别名排序。
与影音 AVG 的对比:AVG 是「全表聚合」(一行结果),GROUP BY 是「分组聚合」(每类一行)——实例 13 的全表 AVG 升级为实例 16 的分组统计,聚合能力的递进。
总统计(无分组):
static async totalStats(context: common.Context): Promise<TotalStat> {
const result = await store.querySql(
`SELECT COUNT(*) AS cnt, SUM(size) AS total_size FROM ${FileLibDao.TABLE}`
);
// 一行:cnt + total_size
}
标题栏「18 个文件 · 共 1.7GB」——COUNT + SUM 无 GROUP BY = 全表聚合(一行)。有无 GROUP BY 的区别:有分组按组分行,无分组全表一行——「每类统计」vs「总统计」的 SQL 差异只在 GROUP BY 一句。
四、批量导入去重:路径先查后插
批量导入 18 个文件,路径已存在则跳过(UNIQUE 去重):
static async batchImport(context: common.Context, files: FileRecord[]): Promise<number> {
const store = await FileLibDao.getStore(context);
let added = 0;
for (const f of files) {
const predicates = new relationalStore.RdbPredicates(FileLibDao.TABLE);
predicates.equalTo('path', f.path);
const result = await store.query(predicates);
const exists = result.goToNextRow();
result.close();
if (exists) {
continue; // UNIQUE 去重:路径已存在则跳过
}
const values: relationalStore.ValuesBucket = {
name: f.name, file_type: f.fileType, size: f.size, path: f.path,
tags: f.tags, note: f.note, created_time: f.createdTime,
};
await store.insert(FileLibDao.TABLE, values);
added++;
}
return added;
}
逐文件「先查后插」:每个文件先查 path 是否存在 → 存在跳过、不存在插入——「查重-插入」循环。返回 added:实际新增数(页面可提示「导入 16 个,跳过 2 个重复」)。
为什么不用 INSERT OR IGNORE? SQLite 有 INSERT OR IGNORE(冲突忽略)——一条语句搞定去重。但 RdbStore 的 insert API 没有 OR IGNORE 选项(需要 executeSql 手写),且「先查后插」能拿到 exists 布尔值做更细控制。「应用层先查后插」是 RdbStore 生态的常用去重方案。
UNIQUE 约束的双层作用:应用层 batchImport 先查后插(友好跳过),数据库 UNIQUE 兜底(即使并发绕过也会拒绝)——**「软去重 + 硬约束」**与健康数据同日 UNIQUE 同模式。
批量的性能:18 个文件 18 次查询 + 插入——数据量小没问题。真实大量文件可用事务(beginTransaction/commit)包裹提速。「循环小批量」教学够用。
五、技术要点对照表
| 技术点 | 实现方式 | 生产价值 |
|---|---|---|
| 包含匹配 | LIKE ‘%kw%’ | 子串搜索 |
| 多字段 OR | name LIKE OR tags LIKE | 多字段命中 |
| 分组统计 | GROUP BY + COUNT + SUM | 每类统计 |
| 总统计 | COUNT + SUM 无分组 | 全库汇总 |
| 批量去重 | 路径先查后插 | 防重复导入 |
| 字节存储 | size INTEGER + fmtSize | 可聚合可展示 |
六、常见问题 FAQ
Q1:LIKE 和 equalTo 的区别?
A:equalTo 是精确匹配(file_type = '文档');LIKE 是模式匹配(name LIKE '%需求%' 包含即中)。精确值筛选(类型、状态)用 equalTo;用户输入的模糊搜索用 LIKE——搜索框是 LIKE 的典型场景。
Q2:%关键字% 和 关键字% 什么区别?
A:%关键字% 匹配任意位置包含(「项目需求文档」中「需求」在中间也中);关键字% 只匹配开头(「需求」开头的名称)。搜索框一般用 %关键字%(最宽松的包含语义);「按前缀找文件」用 关键字%。通配符位置决定匹配语义。
Q3:搜索「%」会怎样?
A:LIKE '%%%' 匹配所有行(任意字符任意次数)——整个列表全显示。关键字含通配符未转义的风险。真实项目对关键字做转义:keyword.replace(/%/g, '\\%').replace(/_/g, '\\_')。Demo 教学简化未转义,读者生产环境务必转义。
Q4:GROUP BY 的 ORDER BY cnt 为什么能排?
A:SQLite 允许 ORDER BY 引用聚合别名(cnt)——按分组统计值排序。先分组聚合,再对组结果排序——「先聚合后排序」是 SQL 的求值顺序(WHERE → GROUP BY → HAVING → ORDER BY)。
Q5:typeStats 和 totalStats 能合并吗?
A:能——SELECT file_type, COUNT(*) ... GROUP BY file_type 加上 ROLLUP(小计行)可一次返回「每类 + 总计」。但 RdbStore 的 querySql 返回结果需额外解析小计行,代码变复杂。分离两个方法更清晰(页面两次调用,数据量小无性能顾虑)。
Q6:为什么 size 不存字符串「120MB」?
A:字符串无法聚合(SUM 报错)也无法排序(字典序 ‘800MB’ < ‘120MB’ 错误)。存最小单位字节(整数),展示时换算——「存储最小化、展示最大化」的字段设计原则。fmtSize 在页面层换算。
Q7:批量导入为什么不用事务?
A:事务(beginTransaction/commit)能把 18 次插入合成一次提交(更快 + 原子性)。Demo 数据量小(18 个),无事务也能秒级完成。「小批量循环插入」教学简化;真实导入大文件列表用事务包裹。
七、文章小结
本篇文章深入讲解了文件库的数据层核心:LIKE 通配符搜索(%包含匹配 + 名称/标签 OR 组合 + 转义风险)、GROUP BY 分组统计(COUNT + SUM 每类一行,从实例 13 的全表 AVG 升级为分组聚合)、批量导入去重(路径先查后插 + UNIQUE 硬约束兜底)、字节整数存储(可聚合可换算)。核心心法是「LIKE 的包含匹配语义」与「分组聚合 = 按维度分行统计」——搜索与统计是文件管理器的两大核心功能。
下一篇(16-4)展示 18 个文件六类种子数据,让文件库一开屏就内容丰富、统计饱满。
八、进阶专题:通配符语义、FTS5 取舍与导入策略
本专题把全文四个技术点再往深处挖一层:% 与 _ 的精确语义、LIKE 与 FTS5 的性能边界、GROUP BY 的执行步骤、先查后插与 INSERT OR IGNORE 的取舍——供读者在真实项目中做决策。
8.1 LIKE 通配符:% 与 _ 的语义详解
% 匹配任意长度的字符序列(含空串),_ 匹配恰好一个字符:
-- % 匹配任意长度(含 0 个)
SELECT * FROM file_lib WHERE name LIKE '%需求%'; -- 含「需求」任意位置
SELECT * FROM file_lib WHERE name LIKE '项目%'; -- 以「项目」开头
SELECT * FROM file_lib WHERE name LIKE '%.docx'; -- 以 .docx 结尾
-- _ 匹配恰好一个字符
SELECT * FROM file_lib WHERE name LIKE '需求_'; -- 「需求文」「需求书」(1 个字符后缀)
SELECT * FROM file_lib WHERE name LIKE '____.docx'; -- 4 字符文件名 + .docx
| 模式 | 语义 | 示例命中 |
|---|---|---|
需求% |
前缀匹配 | 需求文档.docx、需求评审.pdf |
%需求 |
后缀匹配 | 项目需求、用户需求 |
%需求% |
包含匹配(任意位置) | 项目需求文档.docx、需求.docx |
需求_ |
单字符后缀 | 需求文、需求书(不匹配「需求」本身) |
_需求 |
单字符前缀 | 新需求、老需求 |
关键语义边界:_ 是「恰好一个」,% 是「任意个(含 0)」——需求% 能匹配「需求」本身,需求_ 不能(必须有第 3 个字符)。搜索框常用 %kw%(最宽松),文件名补全用 kw%(前缀),两者场景不同。
8.2 LIKE 性能与全文索引(FTS5)对比
%关键字% 的前导通配符导致 B+ 树索引无法利用(不知道从哪个 key 开始扫),只能全表扫描:
-- 走全表扫描:前导 % 无法用索引
SELECT * FROM file_lib WHERE name LIKE '%需求%';
-- 可能走索引:前缀匹配可以范围扫描
SELECT * FROM file_lib WHERE name LIKE '需求%';
| 维度 | LIKE ‘%kw%’ | FTS5 全文索引 |
|---|---|---|
| 索引利用 | 前导 % 无法走索引,全表扫描 | 倒排索引,词条定位 |
| 十万行耗时 | 数百毫秒级(全表扫) | 毫秒级(词条命中) |
| 匹配粒度 | 子串(任意位置) | 分词后的词条(中文需分词器) |
| 排序能力 | 无相关性排序 | ORDER BY rank 按相关度 |
| 实现成本 | 一个 like 调用 | 建 FTS5 虚拟表 + 同步维护 |
-- FTS5 方案示意(中文需 icu 分词器,生产场景才值得引入)
CREATE VIRTUAL TABLE file_lib_fts USING fts5(name, tags, content=file_lib);
SELECT * FROM file_lib_fts WHERE file_lib_fts MATCH '需求';
决策依据:万行以内 LIKE 足够(Demo 18 行秒回);十万行以上且搜索是核心功能,才引入 FTS5——「LIKE 是零成本方案,FTS5 是重量级方案」。
8.3 GROUP BY + COUNT + SUM 的执行逻辑
GROUP BY 三步走:分组 → 组内聚合 → 输出一行:
-- 原始行(示意 3 行文档)
-- 项目需求文档.docx | 文档 | 2411725
-- 接口设计说明.pdf | 文档 | 5242880
-- 用户手册终稿.pdf | 文档 | 8388608
SELECT file_type, COUNT(*) AS cnt, SUM(size) AS total_size
FROM file_lib
GROUP BY file_type;
-- 第 1 步:按 file_type 分桶(文档/图片/视频/...)
-- 第 2 步:每桶 COUNT 行数、SUM(size)
-- 第 3 步:每桶输出一行 → 文档 | 3 | 16043213
| 步骤 | 动作 | 结果 |
|---|---|---|
| 1 分组 | 按 file_type 分桶 | 6 个桶(六类) |
| 2 聚合 | 每桶 COUNT(*) 计行、SUM(size) 求和 | 每桶两个聚合值 |
| 3 输出 | 每桶一行(file_type + cnt + total_size) | 6 行结果 |
细节:COUNT(*) 计行数(含 NULL),SUM 忽略 NULL;分组后 ORDER BY cnt DESC 对聚合结果排序。无 GROUP BY 的 COUNT/SUM 是「全表一个桶」——typeStats 与 totalStats 只是「分组/不分组的同一套聚合」。
8.4 批量导入:先查后插 vs INSERT OR IGNORE
| 维度 | 先查后插(Demo 采用) | INSERT OR IGNORE |
|---|---|---|
| 语句数 | 每文件 1 查 + 1 插 | 每文件 1 条 |
| 冲突感知 | 能拿到 exists 布尔值 | 只看受影响行数 |
| RdbStore 支持 | 原生 API(query + insert) | 需 executeSql 手写 SQL |
| 应用逻辑 | 可做「跳过提示」「更新旧记录」 | 只能忽略 |
| 并发安全 | 靠 UNIQUE 兜底 | UNIQUE 直接生效 |
-- INSERT OR IGNORE:冲突时静默忽略(UNIQUE 冲突不报错)
INSERT OR IGNORE INTO file_lib(name, file_type, size, path)
VALUES ('项目需求文档.docx', '文档', 2411725, '/docs/需求/项目需求文档.docx');
-- 若 path 已存在:插入被忽略,返回 0 行受影响(不报错)
结论:Demo 选「先查后插」是因为 RdbStore 的 insert API 无 OR IGNORE 选项,且要 exists 布尔值做友好跳过;生产大量导入用事务 + INSERT OR IGNORE(一条 SQL 原子完成去重)更快。「应用层友好提示 + 数据库 UNIQUE 硬约束」双层保险不变。
8.5 进阶 FAQ
Q1:% 和 _ 能混用吗?
A:能——需求_%.docx 表示「需求 + 至少一个字符 + 任意 + .docx」,_ 占一位、% 占任意位,可自由组合成复杂模式。
Q2:LIKE 对中文有效吗?
A:有效——SQLite 按字符比较,%需求% 对中文子串同样成立(UTF-8 下按字符语义匹配)。
Q3:FTS5 什么时候才值得上?
A:十万行以上 + 搜索是核心路径 + 需要相关度排序时才值得——引入分词器与同步维护成本。Demo 18 行数据用 LIKE 即可。
Q4:GROUP BY 能按多个字段分组吗?
A:能——GROUP BY file_type, tags 按「类型+标签」组合分组,每组一行,统计粒度更细。
Q5:先查后插有并发漏洞吗?
A:有——两个线程同时查同一 path 都不存在时会双插;UNIQUE 约束会让第二个 insert 抛异常——所以数据库层硬约束兜底必不可少。
更多推荐




所有评论(0)