30 原始 SQL 查询与聚合统计:querySql 实战
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 函数
一、querySql 基础
1.1 方法签名
querySql(sql: string, bindArgs?: Array<number | string | boolean>): Promise<ResultSet>
| 参数 | 类型 | 说明 |
|---|---|---|
sql |
string |
完整的 SQL 查询语句 |
bindArgs |
Array<number|string|boolean>(可选) |
参数化绑定的值 |
1.2 最简单的查询
// SELECT * + 全部返回
const rs = await store.querySql(`SELECT COUNT(*) AS cnt FROM handwriting`)
// 参数化查询(使用 ? 占位符)
const rs = await store.querySql(
'SELECT COUNT(*) AS cnt FROM handwriting WHERE archive_id = ?',
[archiveId]
)
安全提示:始终使用
?参数化绑定,不要拼接字符串。querySql的第二个参数bindArgs会自动处理转义,防止 SQL 注入。
二、COUNT 计数查询
2.1 简单计数
// HandwritingDao.countAll
static async countAll(): Promise<number> {
const store = HandwritingDao.getStore()
const rs = await store.querySql(`SELECT COUNT(*) AS cnt FROM ${TABLE}`)
let cnt = 0
if (rs.goToFirstRow()) {
cnt = rs.getLong(rs.getColumnIndex('cnt'))
}
rs.close()
return cnt
}
2.2 带条件的计数
// HandwritingDao.countByArchive
static async countByArchive(archiveId: number): Promise<number> {
const store = HandwritingDao.getStore()
const rs = await store.querySql(
`SELECT COUNT(*) AS cnt FROM ${TABLE} WHERE archive_id = ?`,
[archiveId]
)
let cnt = 0
if (rs.goToFirstRow()) {
cnt = rs.getLong(rs.getColumnIndex('cnt'))
}
rs.close()
return cnt
}
2.3 去重计数
// HandwritingDao.countDistinctActiveDays
// 统计有记录的不同天数
static async countDistinctActiveDays(): Promise<number> {
const store = HandwritingDao.getStore()
const rs = await store.querySql(
`SELECT COUNT(DISTINCT (created_at / 86400000)) AS cnt FROM ${TABLE}`
)
let cnt = 0
if (rs.goToFirstRow()) {
cnt = rs.getLong(rs.getColumnIndex('cnt'))
}
rs.close()
return cnt
}
去重技巧:created_at 是毫秒时间戳,除以 86400000(一天的毫秒数)后,同一天的所有记录映射为相同的整数,COUNT(DISTINCT ...) 汇总出实际有记录的天数。
三、LIMIT 限制
3.1 带 LIMIT 的查询
// HandwritingDao.listRecent
static async listRecent(limit: number): Promise<HandwritingEntity[]> {
const store = HandwritingDao.getStore()
const rs = await store.querySql(
`SELECT * FROM ${TABLE} ORDER BY created_at DESC LIMIT ?`,
[limit] // limit 作为参数化值
)
const result: HandwritingEntity[] = []
while (rs.goToNextRow()) {
result.push(HandwritingDao.parseRow(rs))
}
rs.close()
return result
}
SQL 语句生成示例:
// 如果 limit = 5
// → SELECT * FROM handwriting ORDER BY created_at DESC LIMIT 5
// 返回最近 5 条笔迹记录
3.2 带条件 + 排序 + LIMIT
// ReportDao.findLatestByArchive
static async findLatestByArchive(archiveId: number): Promise<ReportEntity | null> {
const store = ReportDao.getStore()
const rs = await store.querySql(
`SELECT * FROM ${TABLE} WHERE archive_id = ?
ORDER BY created_at DESC LIMIT 1`,
[archiveId]
)
let result: ReportEntity | null = null
if (rs.goToFirstRow()) {
result = ReportDao.parseRow(rs)
}
rs.close()
return result
}
四、聚合查询汇总
4.1 项目中使用的聚合 SQL
| DAO | SQL | 用途 | 参数 |
|---|---|---|---|
| HandwritingDao | COUNT(*) FROM ${TABLE} |
总笔迹数 | 无 |
| HandwritingDao | COUNT(*) FROM ${TABLE} WHERE archive_id = ? |
某档案笔迹数 | archiveId |
| HandwritingDao | COUNT(DISTINCT (created_at / 86400000)) |
活跃天数 | 无 |
| ReportDao | COUNT(*) FROM ${TABLE} |
总报告数 | 无 |
| ReportDao | SELECT * ... WHERE archive_id = ? ORDER BY created_at DESC LIMIT 1 |
最新报告 | archiveId |
4.2 扩展:更多聚合模式
// 场景 1:AVG 平均能量值(未来可能需要)
const rs = await store.querySql(
'SELECT AVG(energy_score) AS avg_energy FROM handwriting WHERE archive_id = ?',
[archiveId]
)
// 场景 2:GROUP BY 分组统计
const rs = await store.querySql(
'SELECT source, COUNT(*) AS cnt FROM handwriting GROUP BY source'
)
// 场景 3:MAX/MIN 数值范围
const rs = await store.querySql(
'SELECT MAX(word_count) AS max_words, MIN(word_count) AS min_words FROM handwriting'
)
// 场景 4:JOIN 两表(笔迹+报告)
const rs = await store.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 | 类型安全,可读性好 |
| 聚合函数 | querySql | RdbPredicates 不支持 |
| LIMIT 限制 | querySql | RdbPredicates.limit() 可用 |
| JOIN 多表 | querySql | 唯一选择 |
| 简单查询 | RdbPredicates | 代码更简洁 |
5.2 同一个查询的两种写法
// 写法 A:RdbPredicates(推荐)
const predicates = new relationalStore.RdbPredicates(TABLE)
predicates.equalTo('archive_id', archiveId)
predicates.orderByDesc('created_at')
const rs = await store.query(predicates, COLS)
// 写法 B:querySql(等效)
const rs = await store.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 参数化绑定
// ✅ 安全:参数化绑定
await store.querySql(
'SELECT * FROM handwriting WHERE archive_id = ?',
[archiveId]
)
// ❌ 危险:字符串拼接
await store.querySql(
`SELECT * FROM handwriting WHERE archive_id = ${archiveId}` // 注入风险
)
6.2 表名与列名拼接注意事项
// ⚠️ 表名和列名不能参数化,需要直接拼接
const TABLE = 'handwriting'
const sql = `SELECT * FROM ${TABLE} ORDER BY created_at DESC LIMIT ?`
// ⚠️ 但是值必须参数化
// ✅ `${TABLE}`(表名固定,安全)
// ✅ `?`(值可变,参数化安全)
// ❌ `${archiveId}`(用户输入,危险)
七、querySql 返回结果的解析
7.1 聚合结果的解析
// 聚合查询的 ResultSet 解析
const rs = await store.querySql(
'SELECT COUNT(*) AS cnt FROM handwriting'
)
// 聚合结果只有 1 行
let cnt = 0
if (rs.goToFirstRow()) {
cnt = rs.getLong(rs.getColumnIndex('cnt')) // 通过别名获取
}
rs.close()
return cnt
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} 常量(项目做法)
const TABLE = 'handwriting'
const rs = await store.querySql(
`SELECT COUNT(*) AS cnt FROM ${TABLE}`
)
// ⚠️ 注意:${TABLE} 是常量,不是用户输入,所以安全
// 如果表名需要动态传入,应做好白名单校验
总结
本文完整解析了鸿蒙 RDB 中原始 SQL 查询的使用方法:
- querySql(sql, args):支持完整的 SQL 语法,参数化绑定防注入
- COUNT 计数:
COUNT(*)、条件计数、COUNT(DISTINCT ...)去重 - LIMIT 限制:配合
ORDER BY DESC获取最新 N 条 - 聚合扩展:AVG、GROUP BY、MAX/MIN、JOIN 的 SQL 模板
- 安全原则:值用
?参数化,表名/列名用常量 - 与 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 窗口函数:排名统计
-- 按日期对情绪分数排名(使用窗口函数)
SELECT
created_at,
score,
RANK() OVER (ORDER BY score DESC) as score_rank
FROM handwriting
WHERE user_id = ?
const sql = `
SELECT created_at, score,
RANK() OVER (ORDER BY score DESC) as score_rank
FROM handwriting WHERE user_id = ?`
const rs = await store.querySql(sql, [userId])
7.2 CTE:递归层级查询
-- 使用 CTE 简化复杂统计
WITH monthly_stats AS (
SELECT strftime('%Y-%m', created_at) as month,
COUNT(*) as total,
AVG(score) as avg_score
FROM handwriting
WHERE user_id = ?
GROUP BY month
)
SELECT * FROM monthly_stats ORDER BY month DESC
7.3 JSON 字段的 SQL 查询
对于存储为 JSON 的字段,可以使用 SQLite 的 json_extract 函数:
// 查询 keywords JSON 数组中包含特定关键词的记录
const sql = `
SELECT * FROM report
WHERE json_extract(keywords, '$[0]') = ?
OR json_extract(keywords, '$[1]') = ?`
const rs = await store.querySql(sql, [keyword, keyword])
7.4 SQL 注入防护原则
| 操作 | 安全做法 | 危险做法 |
|---|---|---|
| 参数绑定 | querySql(sql, [param]) |
\WHERE id = ${id}`` |
| 动态表名 | 使用白名单校验 | 直接拼接用户输入 |
| LIKE 通配 | LIKE ? + '%' + val + '%' |
LIKE '%${val}%' |
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
更多推荐

所有评论(0)