在这里插入图片描述
在这里插入图片描述

实例:文件资料库(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 抛异常——所以数据库层硬约束兜底必不可少。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐