鸿蒙应用开发实战【36】— 数据查询优化:predicates 链式调用

本文是「号码助手全栈开发系列」第 36 篇,持续更新中…
开源社区:https://openharmonycrossplatform.csdn.net


前言

在 DAO 层的基本 CRUD 中,RdbPredicates 承担了所有查询条件的构建工作。但 predicates 的能力远不止 equalToorderByAsc 这么简单——它支持链式调用、复杂条件组合(AND/OR/括号)、原生 SQL 表达式等高级功能。正确使用 predicates 可以写出更高效、更安全的查询代码。

本篇涵盖:predicates 链式调用模式、AND/OR 组合条件、beginWrap/endWrap 括号分组、聚合查询的替代方案、executeSql 原生 SQL、索引优化、查询性能分析与 explain 工具。

在这里插入图片描述


一、predicates 链式调用

每个 predicates 方法都返回自身,支持链式调用:

let predicates = new relationalStore.RdbPredicates('app_binding')
  .equalTo('status', '使用中')
  .orderByDesc('updated_at')
  .limitAs(20);

等价于:

let predicates = new relationalStore.RdbPredicates('app_binding');
predicates.equalTo('status', '使用中');
predicates.orderByDesc('updated_at');
predicates.limitAs(20);

链式调用更紧凑,适合条件固定的场景。条件动态组合时,分布调用更合适。

1.1 predicates 常用方法一览

方法 功能 示例
equalTo(field, value) 等值匹配 equalTo('status', '使用中')
notEqualTo(field, value) 不等值匹配 notEqualTo('status', '已停用')
like(field, pattern) 模糊匹配 like('app_name', '%微信%')
in(field, values) IN 集合匹配 in('id', [1,2,3])
greaterThan(field, value) 大于 greaterThan('sort_order', 0)
lessThan(field, value) 小于 lessThan('created_at', '2026-01-01')
orderByAsc(field) 升序 orderByAsc('sort_order')
orderByDesc(field) 降序 orderByDesc('updated_at')
limitAs(count) 限制条数 limitAs(20)
offsetAs(offset) 偏移量 offsetAs(0)
beginWrap() 开始括号分组 与 endWrap 配合
endWrap() 结束括号分组 与 beginWrap 配合
and() AND 逻辑 默认连接符
or() OR 逻辑 切换为 OR

二、AND / OR 条件组合

2.1 基本 AND 组合

predicates 默认是 AND 关系,多个条件自动 AND:

// WHERE status = '使用中' AND category = '支付'
let predicates = new relationalStore.RdbPredicates('app_binding');
predicates.equalTo('status', '使用中');     // AND
predicates.equalTo('category', '支付');     // AND

2.2 使用 OR

// WHERE status = '使用中' OR status = '待换绑'
let predicates = new relationalStore.RdbPredicates('app_binding');
predicates.equalTo('status', '使用中');
predicates.or();
predicates.equalTo('status', '待换绑');

2.3 AND 和 OR 混用

// WHERE status = '使用中' AND ( category = '支付' OR category = '金融' )
let predicates = new relationalStore.RdbPredicates('app_binding');
predicates.equalTo('status', '使用中');
predicates.and();
predicates.beginWrap();
predicates.equalTo('category', '支付');
predicates.or();
predicates.equalTo('category', '金融');
predicates.endWrap();

2.4 复杂筛选示例

首页筛选面板的组合条件:

/**
 * 构建筛选 predicates
 * 三个维度:分类、卡号、状态,彼此 AND,每个维度内的选项 OR
 */
function buildFilterPredicates(options: {
  categories?: string[];
  cardIds?: number[];
  statuses?: string[];
}): relationalStore.RdbPredicates {
  let predicates = new relationalStore.RdbPredicates('app_binding');
  let hasPrev = false;

  // 分类筛选:OR 关系
  if (options.categories && options.categories.length > 0) {
    if (hasPrev) predicates.and();
    predicates.beginWrap();
    options.categories.forEach((cat, i) => {
      if (i > 0) predicates.or();
      predicates.equalTo('category', cat);
    });
    predicates.endWrap();
    hasPrev = true;
  }

  // 卡号筛选:OR 关系
  if (options.cardIds && options.cardIds.length > 0) {
    if (hasPrev) predicates.and();
    predicates.in('card_id', options.cardIds);
    hasPrev = true;
  }

  // 状态筛选:OR 关系
  if (options.statuses && options.statuses.length > 0) {
    if (hasPrev) predicates.and();
    predicates.beginWrap();
    options.statuses.forEach((status, i) => {
      if (i > 0) predicates.or();
      predicates.equalTo('status', status);
    });
    predicates.endWrap();
    hasPrev = true;
  }

  predicates.orderByDesc('updated_at');
  return predicates;
}

生成的 SQL:

WHERE category IN ('支付', '金融')
  AND card_id IN (1, 2)
  AND (status = '使用中' OR status = '待换绑')
ORDER BY updated_at DESC

三、查询优化策略

3.1 列投影(Columns)

只查询需要的字段,减少数据传输:

// ❌ 差:查询所有列
let rs = await store.query(predicates);

// ✅ 好:只查询需要的列
let rs = await store.query(predicates, [
  'id', 'app_name', 'status', 'card_id'
]);

在需要查询 COUNT(*) 等聚合时,列投影尤其重要:

// 只查询 count
let rs = await store.query(predicates, ['COUNT(*) AS count']);
rs.goToFirstRow();
const total = rs.getLong(0);

3.2 分页查询

对于可能返回大量数据的查询,使用 limit + offset:

let predicates = new relationalStore.RdbPredicates('card');
predicates.orderByAsc('sort_order');
predicates.limitAs(20);    // 每页 20 条
predicates.offsetAs(0);    // 第 1 页

3.3 排序优化

多字段排序时,优先级从左到右:

// 先按状态排序(使用中排前面),再按更新时间倒序
predicates.orderByAsc('status');
predicates.orderByDesc('updated_at');

📌 排序与索引:如果 statusupdated_at 上有复合索引 (status, updated_at),排序可以直接使用索引,避免文件排序。


四、索引设计

4.1 索引的原理

索引是数据库查询优化的核心武器。没有索引时,SQLite 执行全表扫描——逐行检查每条记录是否匹配条件。有索引时,通过 B-Tree 结构快速定位符合条件的数据行。

4.2 哪些字段需要索引

// 在 DatabaseService 中创建索引
const CREATE_INDEXES = `
  -- app_binding 表
  CREATE INDEX IF NOT EXISTS idx_binding_card_id ON app_binding(card_id);
  CREATE INDEX IF NOT EXISTS idx_binding_status ON app_binding(status);
  CREATE INDEX IF NOT EXISTS idx_binding_category ON app_binding(category);

  -- card 表
  CREATE INDEX IF NOT EXISTS idx_card_operator ON card(operator);

  -- sms_candidate 表
  CREATE INDEX IF NOT EXISTS idx_candidate_imported ON sms_candidate(imported);
  CREATE INDEX IF NOT EXISTS idx_candidate_sms_body ON sms_candidate(sms_body);
`;

索引策略:

索引字段 查询场景
app_binding card_id WHERE card_id = ?(查某卡号的绑定)
app_binding status WHERE status = ‘使用中’(筛选状态)
app_binding category WHERE category = ‘支付’(分类筛选)
card operator WHERE operator = ‘中国移动’
sms_candidate imported WHERE imported = 0(查询未导入)
sms_candidate sms_body 去重查询优化

4.3 复合索引

对于同时按多个字段筛选的场景,复合索引更高效:

-- 按状态 + 更新时间排序
CREATE INDEX IF NOT EXISTS idx_binding_status_updated
  ON app_binding(status, updated_at DESC);

复合索引 (status, updated_at) 可以同时服务于:

  • WHERE status = '使用中' ORDER BY updated_at DESC
  • WHERE status = '使用中'
  • 但不能服务于仅查询 updated_at 的条件

📌 最左前缀原则:复合索引 (A, B, C) 可以服务 A(A, B)(A, B, C) 的查询,但不能单独服务 BC


五、executeSql 原生查询

当 predicates 无法表达某些查询时(如 JOIN、子查询、复杂聚合),使用 executeSql 执行原生 SQL。

5.1 原生 SQL vs predicates 对比

场景 predicates executeSql
等值查询 ✅ equalTo ✅ WHERE =
模糊搜索 ✅ like ✅ LIKE
IN 查询 ✅ in ✅ IN
JOIN 查询 ❌ 不支持 ✅ JOIN
GROUP BY ❌ 不支持 ✅ GROUP BY
子查询 ❌ 不支持 ✅ 子查询
窗口函数 ❌ 不支持 ✅ 窗口函数
SQL 注入风险 ✅ 无 ⚠️ 需谨慎

5.2 使用 executeSql 进行 JOIN

async queryWithJoin(): Promise<AppRow[]> {
  const sql = `
    SELECT
      b.id, b.app_name, b.category, b.card_id,
      b.status, b.remark, b.icon_key,
      b.created_at, b.updated_at,
      c.label AS card_label,
      c.color AS card_color,
      c.phone
    FROM app_binding b
    LEFT JOIN card c ON b.card_id = c.id
    WHERE b.status IN ('使用中', '待换绑')
    ORDER BY b.updated_at DESC
    LIMIT 50
  `;

  // executeSql 不返回 ResultSet
  // 需要使用 RdbStore 的 query 方法配合 predicates 无法实现
  // 或者直接操作 SQLite 原生接口
  await this.store.executeSql(sql);
  // 返回值为 void,此处仅为概念展示

  // 实际项目中可采用批量查询 + 内存组合方案
  return await this.listWithCard();
}

5.3 使用 executeSql 进行复杂聚合

async countByCategory(): Promise<{ category: string; count: number }[]> {
  // 使用 predicates + 列投影模拟 GROUP BY
  // 由于 predicates 不支持 GROUP BY,在应用层聚合
  const all = await this.listAll();
  const map = new Map<string, number>();

  for (const item of all) {
    const category = item.category || '未分类';
    map.set(category, (map.get(category) || 0) + 1);
  }

  return Array.from(map.entries())
    .map(([category, count]) => ({ category, count }))
    .sort((a, b) => b.count - a.count);
}

六、查询性能分析

6.1 使用执行计划分析查询

// 通过 EXPLAIN 分析查询计划
// 在 executeSql 中执行
const explainSql = 'EXPLAIN QUERY PLAN SELECT * FROM app_binding WHERE status = ?';
// 分析输出可以帮助判断是否使用了索引

EXPLAIN 输出的典型解读:

输出 含义 操作
SCAN TABLE 全表扫描 需要加索引
SEARCH TABLE USING INDEX 使用了索引 查询高效
USE TEMP B-TREE FOR ORDER BY 排序未使用索引 需加复合索引
SEARCH USING COVERING INDEX 覆盖索引 最高效

6.2 慢查询定位

// 使用 hilog 记录查询耗时
async queryWithTiming(predicates: relationalStore.RdbPredicates): Promise<CardEntity[]> {
  const start = performance.now();

  let rs = await this.store.query(predicates);
  try {
    let list: CardEntity[] = [];
    while (rs.goToNextRow()) {
      list.push(parseCard(rs));
    }

    const elapsed = performance.now() - start;
    if (elapsed > 100) {
      console.warn(`Slow query: ${elapsed}ms, rows=${list.length}`);
    }

    return list;
  } finally {
    rs.close();
  }
}

七、最佳实践总结

7.1 查询优化清单

  1. ✅ 常用查询字段建索引
  2. ✅ WHERE 条件用索引字段
  3. ✅ 只查需要的列(列投影)
  4. ✅ 查询大量数据加分页
  5. ✅ ORDER BY 字段靠近 WHERE 字段组成复合索引
  6. ✅ predicates 实现了 AND/OR/括号,无需回退到原生 SQL
  7. ✅ LIKE 尽量少用前缀通配(少用 %keyword,多用 keyword%

7.2 避免的反模式

// ❌ SELECT * — 查了不需要的列
let rs = await store.query(predicates);

// ✅ SELECT id, app_name, status
let rs = await store.query(predicates, ['id', 'app_name', 'status']);

// ❌ 在循环中查数据库(N+1 问题)
for (const id of ids) {
  const item = await dao.getById(id);
}

// ✅ 用 IN 一次查完
const items = await dao.listByIds(ids);

小结

本篇全面讲解了 RDB 查询优化的核心技能:

技能 方法 关键点
链式调用 方法返回自身 固定条件用链式,动态组合用分布
条件组合 beginWrap/endWrap AND/OR + 括号分组
列投影 columns 参数 只查需要的字段
分页 limitAs + offsetAs 避免大数据量 OOM
索引 CREATE INDEX 常用查询字段建索引
复合索引 (A, B, C) 最左前缀原则
执行计划 EXPLAIN QUERY PLAN 验证索引是否生效
慢查询 性能计时 >100ms 告警

下一篇将专题讲解「数据统计 countByStatus 聚合查询」,实现首页统计卡片的真实数据驱动。


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


相关资源:

Logo

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

更多推荐