数据量一大,两个问题就来了:一是"几万条数据怎么快速入库",二是"查询怎么不卡 UI"。处理不好,应用就会卡顿、appfreeze、甚至 ANR。下面把官方的批量写入建议、TaskPool 异步查询、分批策略整合成一套方案。
一、问题背景:导入万条通讯录
假设一个通讯录应用,用户从旧手机导入通讯录,一次性要写入 10000 条联系人。如果用最朴素的写法------for 循环里一条条 insert,会怎样?
- 每条 insert 都是一次磁盘 IO + 索引更新
- 10000 次 IO,主线程卡死,appfreeze
- 即使不卡死,也要几十秒,用户体验崩塌
这就是批量写入要解决的第一个问题。
二、官方给出的四条建议
官方文档对大数据量场景给了明确建议:
- 单次查询数据量不超过 5000 条
- 在 TaskPool 中查询
- 拼接 SQL 语句尽量简洁
- 合理地分批次查询
这四条是性能优化的总纲,下面逐条展开实战。
三、批量写入优化:用事务包住批量 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 次数 和不阻塞主线程。
- 批量写入用事务:N 次 IO 降为 1 次,效果立竿见影
- 大查询放 TaskPool:主线程不卡,UI 流畅
- 分批查 + 只查需要的列:控制单次查询的数据量
- 索引 + 参数化绑定:让查询走索引、复用执行计划