鸿蒙应用开发实战【37】--- 数据统计:countByStatus 聚合查询
本文是「号码助手全栈开发系列」第 37 篇,持续更新中...
前言
号码助手首页顶部有四个统计卡片------全部、使用中、待处理、已停用 。这些数字不是硬编码的占位符,而是从 app_binding 表中实时查询出来的。本篇将实现 countByStatus 聚合查询,涵盖 RDB 中聚合函数的应用、状态分布统计、以及首页统计卡片的数据驱动。
本篇涵盖:COUNT 聚合函数应用、基于 predicates 的 count 方法、状态分布统计 countByStatus、跨表统计(绑定数/卡号数)、首页统计卡片的数据联动、缓存与刷新策略。

一、COUNT 基础
1.1 基本计数
typescript
async count(): Promise<number> {
let predicates = new relationalStore.RdbPredicates('app_binding');
let rs = await this.store.query(predicates, ['COUNT(*) AS count']);
try {
rs.goToFirstRow();
return rs.getLong(0);
} finally {
rs.close();
}
}
等价 SQL:SELECT COUNT(*) AS count FROM app_binding
1.2 带条件的计数
typescript
async countByStatus(status: string): Promise<number> {
let predicates = new relationalStore.RdbPredicates('app_binding');
predicates.equalTo('status', status);
let rs = await this.store.query(predicates, ['COUNT(*) AS count']);
try {
rs.goToFirstRow();
return rs.getLong(0);
} finally {
rs.close();
}
}
等价 SQL:SELECT COUNT(*) AS count FROM app_binding WHERE status = '使用中'
二、多个状态同时统计
每次查询一个计数,需要 4 次数据库查询。优化后一次查出所有状态分布:
2.1 方法一:多次查询(简单易懂)
typescript
async getStatusStats(): Promise<{
total: number;
active: number;
pending: number;
disabled: number;
}> {
const total = await this.count();
const active = await this.countByStatus('使用中');
const pendingSwitch = await this.countByStatus('待换绑');
const pendingCancel = await this.countByStatus('待注销');
const disabled = await this.countByStatus('已停用');
return {
total,
active,
pending: pendingSwitch + pendingCancel,
disabled,
};
}
优点: 代码直观,每个 DAO 方法职责单一
缺点: 4 次数据库查询,性能开销大
2.2 方式二:一次查询 + 内存统计
typescript
async getStatusStats(): Promise<{
total: number;
active: number;
pending: number;
disabled: number;
}> {
// 一次查出所有记录
const all = await this.listAll();
// 内存中统计
let active = 0;
let pending = 0;
let disabled = 0;
for (const item of all) {
switch (item.status) {
case '使用中':
active++;
break;
case '待换绑':
case '待注销':
pending++;
break;
case '已停用':
disabled++;
break;
}
}
return {
total: all.length,
active,
pending,
disabled,
};
}
优点: 只需 1 次查询
缺点: 如果只需要计数,查询所有列浪费带宽
适用: 数据量不大(<1000)的场景
2.3 方式三:按状态多次 COUNT(推荐)
typescript
async countByStatusGroup(): Promise<Record<string, number>> {
const statuses = ['使用中', '待换绑', '待注销', '已停用'];
const result: Record<string, number> = {};
for (const status of statuses) {
result[status] = await this.countByStatus(status);
}
return result;
}
优点: 每次只查一列 COUNT 值,数据传输最少
缺点: 多次查询,但有索引后每次是 O(log n)
三、跨表统计
首页统计卡片不仅显示绑定数,还可能显示卡号总数、候选记录数:
typescript
export class DashboardStats {
private cardDao: CardDao;
private bindingDao: AppBindingDao;
private candidateDao: SmsCandidateDao;
constructor(store: relationalStore.RdbStore) {
this.cardDao = new CardDao(store);
this.bindingDao = new AppBindingDao(store);
this.candidateDao = new SmsCandidateDao(store);
}
/**
* 获取首页仪表盘的所有统计数据
*/
async getAllStats(): Promise<{
// 卡号统计
totalCards: number;
// 绑定统计
totalBindings: number;
activeBindings: number;
pendingBindings: number;
disabledBindings: number;
// 候选统计
pendingCandidates: number;
}> {
// 并行执行所有查询
const [
totalCards,
totalBindings,
activeBindings,
pendingBindings,
disabledBindings,
pendingCandidates,
] = await Promise.all([
this.cardDao.count(),
this.bindingDao.count(),
this.bindingDao.countByStatus('使用中'),
(async () => {
const s = await this.bindingDao.countByStatus('待换绑');
const c = await this.bindingDao.countByStatus('待注销');
return s + c;
})(),
this.bindingDao.countByStatus('已停用'),
this.candidateDao.countUnimported(),
]);
return {
totalCards,
totalBindings,
activeBindings,
pendingBindings,
disabledBindings,
pendingCandidates,
};
}
}
⚡ Promise.all 并行化 :所有 COUNT 查询互不依赖,可以并行执行。
Promise.all比串行快 3-4 倍。
四、首页统计卡片的数据驱动
4.1 状态定义
typescript
@Entry
@Component
struct HomePage {
@State stats: {
totalCards: number;
totalBindings: number;
activeBindings: number;
pendingBindings: number;
disabledBindings: number;
pendingCandidates: number;
} = {
totalCards: 0,
totalBindings: 0,
activeBindings: 0,
pendingBindings: 0,
disabledBindings: 0,
pendingCandidates: 0,
};
private statsService: DashboardStats = new DashboardStats(store);
async aboutToAppear(): Promise<void> {
await this.loadStats();
}
async loadStats(): Promise<void> {
try {
this.stats = await this.statsService.getAllStats();
} catch (error) {
console.error('Failed to load stats', error);
}
}
}
4.2 刷新策略
typescript
// 数据变更后刷新统计
async onDataChanged(): Promise<void> {
// 更新统计
await this.loadStats();
// 如果有待处理候选,显示红点提示
if (this.stats.pendingCandidates > 0) {
// 显示导入引导
}
}
4.3 统计卡片组件
typescript
@Component
struct StatCard {
@Prop label: string = '';
@Prop count: number = 0;
@Prop color: ResourceColor = '#4F7CFF';
build() {
Column() {
Text(this.count.toString())
.fontSize(28)
.fontWeight(700)
.fontColor(this.color)
Text(this.label)
.fontSize(13)
.fontColor('#68708A')
.margin({ top: 4 })
}
.padding(12)
.backgroundColor(Color.White)
.borderRadius(12)
.layoutWeight(1)
}
}
五、数据一致性保障
5.1 计数缓存
频繁切换页面时,每次都重新 COUNT 会影响性能。简单的内存缓存策略:
typescript
export class StatsCache {
private cache: {
data: DashboardStats | null;
timestamp: number;
} = { data: null, timestamp: 0 };
private readonly TTL = 5000; // 5 秒缓存
async getStats(service: DashboardStats): Promise<DashboardStats> {
const now = Date.now();
if (this.cache.data && (now - this.cache.timestamp) < this.TTL) {
return this.cache.data;
}
this.cache.data = await service.getAllStats();
this.cache.timestamp = now;
return this.cache.data;
}
invalidate(): void {
this.cache.data = null;
this.cache.timestamp = 0;
}
}
5.2 写操作后失效
typescript
// 在需要的地方注入缓存的失效逻辑
async onAddBinding(): Promise<void> {
await this.bindingDao.insert(newBinding);
statsCache.invalidate(); // 使缓存失效
await this.loadStats(); // 重新加载
}
六、聚合查询的进阶
6.1 最大值/最小值
typescript
// 查询最新的绑定更新时间
async getLatestUpdateTime(): Promise<string | null> {
let predicates = new relationalStore.RdbPredicates('app_binding');
predicates.orderByDesc('updated_at');
predicates.limitAs(1);
let rs = await this.store.query(predicates, ['updated_at']);
try {
if (rs.rowCount === 0) return null;
rs.goToFirstRow();
return rs.getString(0);
} finally {
rs.close();
}
}
6.2 使用列投影模拟更多聚合
typescript
// 统计每张卡的绑定数量
async countByCard(): Promise<Map<number, number>> {
const all = await this.listAll();
const map = new Map<number, number>();
for (const item of all) {
map.set(item.card_id, (map.get(item.card_id) || 0) + 1);
}
return map;
}
七、性能对比
| 方法 | 查询次数 | 数据传输 | 适用场景 |
|---|---|---|---|
| 逐状态 COUNT | 4 次 | 4 个整数 | 数据量大,只需计数 |
| 全量查 + 内存统计 | 1 次 | 所有行数据 | 数据量小 (<1000) |
| Promise.all 并行 COUNT | 4 次(并行) | 4 个整数 | 推荐方案,兼顾性能 |
7.1 统计指标说明
| 指标 | 含义 | 数据来源 | 对应 UI |
|---|---|---|---|
| totalCards | 卡号总数 | card 表 | 首页卡号卡 |
| totalBindings | 绑定总数 | app_binding 表 | 首页全部卡 |
| activeBindings | 使用中 | app_binding.status='使用中' | 使用中卡 |
| pendingBindings | 待处理 | status='待换绑' + '待注销' | 待处理卡 |
| disabledBindings | 已停用 | app_binding.status='已停用' | 已停用卡 |
| pendingCandidates | 待导入候选 | sms_candidate.imported=0 | 导入提示 |
小结
本篇完成了数据统计的完整实现:
| 技能 | 方法 | 关键点 |
|---|---|---|
| 基础 COUNT | count() | 列投影 COUNT(*) |
| 条件 COUNT | countByStatus | equalTo + COUNT |
| 状态分布 | countByStatusGroup | 循环查询各状态 |
| 跨表统计 | DashboardStats | 组合多个 DAO |
| 并行查询 | Promise.all | 4 个查询并行 |
| 内存缓存 | StatsCache | 5 秒 TTL 避免重复查询 |
| 缓存失效 | invalidate() | 写操作后刷新 |
| 聚合查询 | MAX/MIN 查询 | 列投影 + 排序 + limit |
7.2 状态统计维度
| 状态 | 含义 | 统计方法 | 首页显示 |
|---|---|---|---|
| 使用中 | 正常绑定的应用 | countByStatus('使用中') | 绿色标记 |
| 待换绑 | 标记为待换绑 | countByStatus('待换绑') | 黄色标记 |
| 待注销 | 标记为待注销 | countByStatus('待注销') | 橙色标记 |
| 已停用 | 已注销/停用 | countByStatus('已停用') | 灰色标记 |
下一篇将专题讲解「数据导出与备份 JSON 序列化」,实现数据库全量数据的 JSON 导出功能。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- openHarmony 跨平台社区:https://openharmonycrossplatform.csdn.net
- HarmonyOS 官方文档:https://developer.huawei.com/consumer/cn/doc/
- HarmonyOS RDB开发指南:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/data-persistence-by-rdb
- HarmonyOS hilog日志:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/hilog