HarmonyOS批量数据与性能优化实战:万条数据入库与不卡 UI 的查询

数据量一大,两个问题就来了:一是"几万条数据怎么快速入库",二是"查询怎么不卡 UI"。处理不好,应用就会卡顿、appfreeze、甚至 ANR。下面把官方的批量写入建议、TaskPool 异步查询、分批策略整合成一套方案。

一、问题背景:导入万条通讯录

假设一个通讯录应用,用户从旧手机导入通讯录,一次性要写入 10000 条联系人。如果用最朴素的写法------for 循环里一条条 insert,会怎样?

  • 每条 insert 都是一次磁盘 IO + 索引更新
  • 10000 次 IO,主线程卡死,appfreeze
  • 即使不卡死,也要几十秒,用户体验崩塌

这就是批量写入要解决的第一个问题。

二、官方给出的四条建议

官方文档对大数据量场景给了明确建议:

  1. 单次查询数据量不超过 5000 条
  2. 在 TaskPool 中查询
  3. 拼接 SQL 语句尽量简洁
  4. 合理地分批次查询

这四条是性能优化的总纲,下面逐条展开实战。

三、批量写入优化:用事务包住批量 insert

最有效的批量写入优化是用事务把所有 insert 包起来。事务里多次 insert 只在 commit 时做一次磁盘同步,IO 量从 N 次降到 1 次。

typescript 复制代码
import { relationalStore } from '@kit.ArkData';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const DOMAIN = 0x0000;

async function batchInsertContacts(store: relationalStore.RdbStore, contacts: Array<{name: string, phone: string}>) {
  const BATCH_SIZE = 500;  // 每批 500 条
  const totalBatches = Math.ceil(contacts.length / BATCH_SIZE);
  
  for (let batch = 0; batch < totalBatches; batch++) {
    const start = batch * BATCH_SIZE;
    const end = Math.min(start + BATCH_SIZE, contacts.length);
    const batchData = contacts.slice(start, end);
    
    // 每批用一个事务
    const transaction = await store.createTransaction();
    try {
      for (const contact of batchData) {
        await transaction.insert('CONTACT', {
          NAME: contact.name,
          PHONE: contact.phone,
          CREATE_TIME: Date.now()
        });
      }
      await transaction.commit();
      hilog.info(DOMAIN, 'perf', `批次 ${batch + 1}/${totalBatches} 完成,${batchData.length} 条`);
    } catch (error) {
      await transaction.rollback();
      hilog.error(DOMAIN, 'perf', `批次 ${batch + 1} 失败: ${(error as BusinessError).message}`);
      throw error;
    }
  }
}

为什么分批 + 事务

  • 事务减少 IO:每批 500 条只同步一次磁盘
  • 分批控制内存:避免一个超大事务占太多内存
  • 失败可定位:某批失败不影响已成功的批次

四、批量写入优化:用 executeSql 跑批量 SQL

另一种方式是拼一条批量 SQL,用 execute 执行:

typescript 复制代码
async function batchInsertBySql(store: relationalStore.RdbStore, contacts: Array<{name: string, phone: string}>) {
  const transaction = await store.createTransaction();
  try {
    // 用参数化绑定,一条 SQL 插入多行(SQLite 的多值 INSERT)
    // 注意:SQLite 多值 INSERT 有长度限制,仍需分批
    const BATCH = 100;
    for (let i = 0; i < contacts.length; i += BATCH) {
      const slice = contacts.slice(i, i + BATCH);
      const placeholders = slice.map(() => '(?, ?)').join(', ');
      const sql = `INSERT INTO CONTACT (NAME, PHONE) VALUES ${placeholders}`;
      const args: Array<string> = [];
      slice.forEach(c => { args.push(c.name); args.push(c.phone); });
      await transaction.execute(sql, args);
    }
    await transaction.commit();
  } catch (error) {
    await transaction.rollback();
    throw error;
  }
}

这种方式比逐条 insert 更快(SQL 解析只一次),但要注意 SQL 语句总长度限制,仍需分批。

五、查询不卡 UI:用 TaskPool 异步执行

官方明确建议:大数据量查询在 TaskPool 中执行。TaskPool 是鸿蒙的轻量级线程池,把耗时查询丢进去,主线程不阻塞。

typescript 复制代码
import { taskpool } from '@kit.ArkTS';

// 在 TaskPool 中执行的查询函数(必须是 @Concurrent 标注)
@Concurrent
async function queryInTask(storeId: string, keyword: string): Promise<Array<any>> {
  // 注意:TaskPool 里不能直接用主线程的 store 实例
  // 需要重新获取或通过可转移对象传递
  // 这里示意,实际要处理 store 的跨线程访问
  const results: Array<any> = [];
  // ... 执行查询,收集结果
  return results;
}

async function searchContacts(keyword: string) {
  // 把查询任务丢到 TaskPool
  const task = new taskpool.Task(queryInTask, 'contact_store', keyword);
  try {
    const results = await taskpool.execute(task) as Array<any>;
    // 查询完成后回到主线程刷新 UI
    AppStorage.setOrCreate('searchResults', results);
  } catch (error) {
    hilog.error(DOMAIN, 'perf', `查询失败: ${(error as BusinessError).message}`);
  }
}

TaskPool 使用的注意点

  • 任务函数要 @Concurrent 标注
  • TaskPool 里不能直接访问主线程对象(如 store 实例),需要重新获取或用可转移对象
  • 查询结果通过 Promise 返回,回到主线程后再刷 UI

六、分批查询:单次不超过 5000 条

官方硬建议:单次查询不超过 5000 条。超过就分批:

typescript 复制代码
async function queryAllContacts(store: relationalStore.RdbStore): Promise<Array<any>> {
  const PAGE_SIZE = 5000;
  let offset = 0;
  let allResults: Array<any> = [];
  let hasMore = true;
  
  while (hasMore) {
    // 用 LIMIT 和 OFFSET 分页查询
    const resultSet = await store.querySql(
      'SELECT ID, NAME, PHONE FROM CONTACT ORDER BY ID LIMIT ? OFFSET ?',
      [PAGE_SIZE, offset]
    );
    
    if (resultSet.rowCount === 0) {
      hasMore = false;
    } else {
      while (resultSet.goToNextRow()) {
        allResults.push({
          id: resultSet.getLong(resultSet.getColumnIndex('ID')),
          name: resultSet.getString(resultSet.getColumnIndex('NAME')),
          phone: resultSet.getString(resultSet.getColumnIndex('PHONE'))
        });
      }
      offset += PAGE_SIZE;
      if (resultSet.rowCount < PAGE_SIZE) {
        hasMore = false;
      }
    }
    resultSet.close();
  }
  
  return allResults;
}

分批查询的好处

  • 单次查询快,不卡 UI
  • 可以边查边显示(流式加载)
  • 内存占用可控

七、SQL 拼接要简洁

官方建议"拼接 SQL 语句尽量简洁",这有几层含义:

7.1 只查需要的列

typescript 复制代码
// 不好:SELECT * 查所有列
await store.querySql('SELECT * FROM CONTACT WHERE ...');

// 好:只查需要的列
await store.querySql('SELECT ID, NAME FROM CONTACT WHERE ...');

SELECT * 会读取所有列数据,包括不需要的 BLOB、长文本,浪费 IO 和内存。

7.2 用参数化绑定而非字符串拼接

typescript 复制代码
// 不好:字符串拼接(慢且有注入风险)
const sql = `SELECT * FROM CONTACT WHERE NAME = '${keyword}'`;

// 好:参数化绑定(预编译,可复用执行计划)
await store.querySql('SELECT * FROM CONTACT WHERE NAME = ?', [keyword]);

参数化绑定的 SQL 会被预编译,多次执行时复用执行计划,更快也更安全。

7.3 避免复杂子查询

能用 JOIN 就别用子查询,能在应用层做聚合就别在 SQL 里做。SQL 越简单,优化器越好优化。

八、索引优化

查询慢的另一个常见原因是缺索引。给常查的字段建索引:

typescript 复制代码
async function createIndexes(store: relationalStore.RdbStore) {
  // 给 NAME 建索引(经常按名字查)
  await store.execute('CREATE INDEX IF NOT EXISTS idx_contact_name ON CONTACT(NAME)');
  // 给 PHONE 建索引(经常按手机号查)
  await store.execute('CREATE INDEX IF NOT EXISTS idx_contact_phone ON CONTACT(PHONE)');
  // 复合索引(经常按名字+手机号查)
  await store.execute('CREATE INDEX IF NOT EXISTS idx_contact_name_phone ON CONTACT(NAME, PHONE)');
}

索引的取舍

  • 索引加速查询,但拖慢插入/更新(要同步更新索引)
  • 写多读少的表少建索引,读多写少的表多建索引
  • 经常作为查询条件的字段才值得建索引

九、结果集使用的性能细节

typescript 复制代码
// 不好:每行都 getColumnIndex(重复计算)
while (resultSet.goToNextRow()) {
  const name = resultSet.getString(resultSet.getColumnIndex('NAME'));  // 每行都算一次
}

// 好:提前算好列索引
const nameIdx = resultSet.getColumnIndex('NAME');
const phoneIdx = resultSet.getColumnIndex('PHONE');
while (resultSet.goToNextRow()) {
  const name = resultSet.getString(nameIdx);
  const phone = resultSet.getString(phoneIdx);
}
resultSet.close();  // 用完必关

十、性能优化 checklist哦

把上面的内容整理成 checklist:

  • 批量写入用事务包住(每批 500 条)
  • 大查询放 TaskPool 异步执行
  • 单次查询不超过 5000 条,多了分批
  • 只查需要的列,不用 SELECT *
  • 用参数化绑定,不字符串拼接
  • 常查字段建索引
  • 结果集提前算列索引,用完 close
  • 写多读少少建索引,读多写少多建索引

十一、总结一下下

性能优化的核心就两件事:减少 IO 次数不阻塞主线程

  1. 批量写入用事务:N 次 IO 降为 1 次,效果立竿见影
  2. 大查询放 TaskPool:主线程不卡,UI 流畅
  3. 分批查 + 只查需要的列:控制单次查询的数据量
  4. 索引 + 参数化绑定:让查询走索引、复用执行计划
相关推荐
长征coder3 小时前
【无标题】
java·性能优化
FrameNotWork3 小时前
HarmonyOS应用《左右相册》 隐私防窥实战:用 dlpAntiPeep 让相册“感知“正在被偷瞄
华为·harmonyos
昇腾知识体系3 小时前
昇腾 950 RegBase 性能优化:从 msprof 采数到优化手法
人工智能·华为·性能优化·知识图谱
马剑威(威哥爱编程)6 小时前
HarmonyOS 应用上架审核避坑:驳回五类地图与提交前自检清单
华为·harmonyos
威斯软科的老司机7 小时前
通俗讲解 CNN 图像识别、向量 Embedding、Softmax 概率计算这三块的简化原理
前端·人工智能·ui·数字孪生
风哥2号8 小时前
数据库教程FGMT13‑Oracle性能优化之数据仓库与分区表
数据库·oracle·性能优化
爱喝水的鱼丶9 小时前
SAP-ABAP:隐式/显式增强开发规范与避坑指南:10 类典型错误与运维方案
运维·性能优化·sap·abap·增强·经验交流
OH_TPC10 小时前
【鸿蒙优选三方库】@react-native-ohos/react-native-screens:原生屏幕导航管理
华为·harmonyos·鸿蒙