☰
30 原始 SQL 查询与聚合统计:querySql 实战
2026/10/9 6:49:56 网站建设 项目流程

30 原始 SQL 查询与聚合统计:querySql 实战

前言

图:30 原始 SQL 查询与聚合统计:querySql 实战 运行效果截图(HarmonyOS NEXT)

虽然RdbPredicates覆盖了大部分查询场景,但当需要聚合统计(COUNT、SUM、AVG)、GROUP BY 分组、SQL 函数或JOIN 多表关联时,直接使用原始 SQL 更为简单高效。鸿蒙 RDB 提供了store.querySql(sql, bindArgs)方法支持原始 SQL 执行。

本文以"鹿鹿·笔迹心理分析"项目中 HandwritingDao 和 ReportDao 的多个聚合查询方法为例,深入解析 querySql 的使用模式。

鸿蒙官方·RdbStore SQL 执行:developer.huawei.com
项目源码仓库:harmony-app GitHub

图:querySql 的四大使用场景——聚合统计、GROUP BY、JOIN、SQL 函数

store.querySql

聚合统计
COUNT / SUM / AVG

分组查询
GROUP BY + HAVING

多表联接
LEFT JOIN / INNER JOIN

SQL 函数
strftime / substr / COALESCE

参数化绑定
防止 SQL 注入

一、querySql 基础

1.1 方法签名

querySql(sql:string,bindArgs?:Array<number|string|boolean>):Promise<ResultSet>
参数类型说明
sqlstring完整的 SQL 查询语句
bindArgsArray<number|string|boolean>(可选)参数化绑定的值

1.2 最简单的查询

// SELECT * + 全部返回constrs=awaitstore.querySql(`SELECT COUNT(*) AS cnt FROM handwriting`)// 参数化查询(使用 ? 占位符)constrs=awaitstore.querySql('SELECT COUNT(*) AS cnt FROM handwriting WHERE archive_id = ?',[archiveId])

安全提示:始终使用?参数化绑定,不要拼接字符串。querySql的第二个参数bindArgs会自动处理转义,防止 SQL 注入。

二、COUNT 计数查询

2.1 简单计数

// HandwritingDao.countAllstaticasynccountAll():Promise<number>{conststore=HandwritingDao.getStore()constrs=awaitstore.querySql(`SELECT COUNT(*) AS cnt FROM${TABLE}`)letcnt=0if(rs.goToFirstRow()){cnt=rs.getLong(rs.getColumnIndex('cnt'))}rs.close()returncnt}

2.2 带条件的计数

// HandwritingDao.countByArchivestaticasynccountByArchive(archiveId:number):Promise<number>{conststore=HandwritingDao.getStore()constrs=awaitstore.querySql(`SELECT COUNT(*) AS cnt FROM${TABLE}WHERE archive_id = ?`,[archiveId])letcnt=0if(rs.goToFirstRow()){cnt=rs.getLong(rs.getColumnIndex('cnt'))}rs.close()returncnt}

2.3 去重计数

// HandwritingDao.countDistinctActiveDays// 统计有记录的不同天数staticasynccountDistinctActiveDays():Promise<number>{conststore=HandwritingDao.getStore()constrs=awaitstore.querySql(`SELECT COUNT(DISTINCT (created_at / 86400000)) AS cnt FROM${TABLE}`)letcnt=0if(rs.goToFirstRow()){cnt=rs.getLong(rs.getColumnIndex('cnt'))}rs.close()returncnt}

去重技巧:created_at是毫秒时间戳,除以86400000(一天的毫秒数)后,同一天的所有记录映射为相同的整数,COUNT(DISTINCT ...)汇总出实际有记录的天数。

三、LIMIT 限制

3.1 带 LIMIT 的查询

// HandwritingDao.listRecentstaticasynclistRecent(limit:number):Promise<HandwritingEntity[]>{conststore=HandwritingDao.getStore()constrs=awaitstore.querySql(`SELECT * FROM${TABLE}ORDER BY created_at DESC LIMIT ?`,[limit]// limit 作为参数化值)constresult:HandwritingEntity[]=[]while(rs.goToNextRow()){result.push(HandwritingDao.parseRow(rs))}rs.close()returnresult}

SQL 语句生成示例:

// 如果 limit = 5// → SELECT * FROM handwriting ORDER BY created_at DESC LIMIT 5// 返回最近 5 条笔迹记录

3.2 带条件 + 排序 + LIMIT

// ReportDao.findLatestByArchivestaticasyncfindLatestByArchive(archiveId:number):Promise<ReportEntity|null>{conststore=ReportDao.getStore()constrs=awaitstore.querySql(`SELECT * FROM${TABLE}WHERE archive_id = ? ORDER BY created_at DESC LIMIT 1`,[archiveId])letresult:ReportEntity|null=nullif(rs.goToFirstRow()){result=ReportDao.parseRow(rs)}rs.close()returnresult}

四、聚合查询汇总

4.1 项目中使用的聚合 SQL

DAOSQL用途参数
HandwritingDaoCOUNT(*) FROM ${TABLE}总笔迹数无
HandwritingDaoCOUNT(*) FROM ${TABLE} WHERE archive_id = ?某档案笔迹数archiveId
HandwritingDaoCOUNT(DISTINCT (created_at / 86400000))活跃天数无
ReportDaoCOUNT(*) FROM ${TABLE}总报告数无
ReportDaoSELECT * ... WHERE archive_id = ? ORDER BY created_at DESC LIMIT 1最新报告archiveId

4.2 扩展:更多聚合模式

// 场景 1:AVG 平均能量值(未来可能需要)constrs=awaitstore.querySql('SELECT AVG(energy_score) AS avg_energy FROM handwriting WHERE archive_id = ?',[archiveId])// 场景 2:GROUP BY 分组统计constrs=awaitstore.querySql('SELECT source, COUNT(*) AS cnt FROM handwriting GROUP BY source')// 场景 3:MAX/MIN 数值范围constrs=awaitstore.querySql('SELECT MAX(word_count) AS max_words, MIN(word_count) AS min_words FROM handwriting')// 场景 4:JOIN 两表(笔迹+报告)constrs=awaitstore.querySql(`SELECT h.id, h.ocr_text, r.personality_type, r.radar_json FROM handwriting h JOIN report r ON h.id = r.handwriting_id WHERE h.archive_id = ? ORDER BY h.created_at DESC`,[archiveId])

五、querySql vs query(Predicates) 对比

5.1 选择策略

场景使用理由
等值查询 + 排序RdbPredicates类型安全,可读性好
聚合函数querySqlRdbPredicates 不支持
LIMIT 限制querySqlRdbPredicates.limit() 可用
JOIN 多表querySql唯一选择
简单查询RdbPredicates代码更简洁

5.2 同一个查询的两种写法

// 写法 A:RdbPredicates(推荐)constpredicates=newrelationalStore.RdbPredicates(TABLE)predicates.equalTo('archive_id',archiveId)predicates.orderByDesc('created_at')constrs=awaitstore.query(predicates,COLS)// 写法 B:querySql(等效)constrs=awaitstore.querySql(`SELECT * FROM${TABLE}WHERE archive_id = ? ORDER BY created_at DESC`,[archiveId])

5.3 代码量对比

查询类型RdbPredicates(行数)querySql(行数)
简单等值3 行1 行(但 SQL 字符串长)
排序+条件4 行1 行(SQL 字符串更长)
COUNT❌ 不支持✅ 1 行
JOIN❌ 不支持✅ 1 行

六、SQL 安全性

6.1 参数化绑定

// ✅ 安全:参数化绑定awaitstore.querySql('SELECT * FROM handwriting WHERE archive_id = ?',[archiveId])// ❌ 危险:字符串拼接awaitstore.querySql(`SELECT * FROM handwriting WHERE archive_id =${archiveId}`// 注入风险)

6.2 表名与列名拼接注意事项

// ⚠️ 表名和列名不能参数化,需要直接拼接constTABLE='handwriting'constsql=`SELECT * FROM${TABLE}ORDER BY created_at DESC LIMIT ?`// ⚠️ 但是值必须参数化// ✅ `${TABLE}`(表名固定,安全)// ✅ `?`(值可变,参数化安全)// ❌ `${archiveId}`(用户输入,危险)

七、querySql 返回结果的解析

7.1 聚合结果的解析

// 聚合查询的 ResultSet 解析constrs=awaitstore.querySql('SELECT COUNT(*) AS cnt FROM handwriting')// 聚合结果只有 1 行letcnt=0if(rs.goToFirstRow()){cnt=rs.getLong(rs.getColumnIndex('cnt'))// 通过别名获取}rs.close()returncnt

7.2 聚合字段的命名

// 聚合字段推荐使用 AS 别名// 便于在代码中通过 getColumnIndex('alias') 读取'SELECT COUNT(*) AS cnt ...'→getColumnIndex('cnt')'SELECT AVG(energy_score) AS avg ...'→getColumnIndex('avg')'SELECT MAX(word_count) AS max ...'→getColumnIndex('max')

八、模板字符串的合理使用

// ✅ 在 DAO 中使用 ${TABLE} 常量(项目做法)constTABLE='handwriting'constrs=awaitstore.querySql(`SELECT COUNT(*) AS cnt FROM${TABLE}`)// ⚠️ 注意:${TABLE} 是常量,不是用户输入,所以安全// 如果表名需要动态传入,应做好白名单校验

总结

本文完整解析了鸿蒙 RDB 中原始 SQL 查询的使用方法:

  1. querySql(sql, args):支持完整的 SQL 语法,参数化绑定防注入
  2. COUNT 计数:COUNT(*)、条件计数、COUNT(DISTINCT ...)去重
  3. LIMIT 限制:配合ORDER BY DESC获取最新 N 条
  4. 聚合扩展:AVG、GROUP BY、MAX/MIN、JOIN 的 SQL 模板
  5. 安全原则:值用?参数化,表名/列名用常量
  6. 与 RdbPredicates 互补:简单查询用 Predicates,复杂统计用 querySql

下一篇文章将深入数据更新与级联操作——ArchiveDao 中 delete 时的级联清理。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


参考资源:

  • RdbStore.querySql 方法
  • [HandwritingDao 项目源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/HandwritingDao.ets)
  • [ReportDao 项目源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/features/data/ReportDao.ets)
  • SQLite 聚合函数
  • 参数化绑定防注入
  • 鸿蒙数据持久化指南
  • HarmonyOS 开发文档

七、高级 SQL 技巧:窗口函数与 CTE

鸿蒙 RDB 基于 SQLite 3.35+,支持窗口函数和公共表表达式(CTE),适用于更复杂的统计场景。

7.1 窗口函数:排名统计

-- 按日期对情绪分数排名(使用窗口函数)SELECTcreated_at,score,RANK()OVER(ORDERBYscoreDESC)asscore_rankFROMhandwritingWHEREuser_id=?
constsql=`SELECT created_at, score, RANK() OVER (ORDER BY score DESC) as score_rank FROM handwriting WHERE user_id = ?`constrs=awaitstore.querySql(sql,[userId])

7.2 CTE:递归层级查询

-- 使用 CTE 简化复杂统计WITHmonthly_statsAS(SELECTstrftime('%Y-%m',created_at)asmonth,COUNT(*)astotal,AVG(score)asavg_scoreFROMhandwritingWHEREuser_id=?GROUPBYmonth)SELECT*FROMmonthly_statsORDERBYmonthDESC

7.3 JSON 字段的 SQL 查询

对于存储为 JSON 的字段,可以使用 SQLite 的json_extract函数:

// 查询 keywords JSON 数组中包含特定关键词的记录constsql=`SELECT * FROM report WHERE json_extract(keywords, '$[0]') = ? OR json_extract(keywords, '$[1]') = ?`constrs=awaitstore.querySql(sql,[keyword,keyword])

7.4 SQL 注入防护原则

操作安全做法危险做法
参数绑定querySql(sql, [param])\WHERE id = ${id}``
动态表名使用白名单校验直接拼接用户输入
LIKE 通配LIKE ?+'%' + val + '%'LIKE '%${val}%'

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询