使用node.js向Sqlite数据库里的插入许多行数据,使用不同的库差别巨大。
本文使用的测试数据是勾股数三元组,下面的代码能在1秒之内生成100万条勾股数。
function* pythagoreanTriplets(count) {
let countTriples = 0;
for (let m = 2; ; m++) {
for (let n = 1; n < m; n++) {
if (++countTriples > count) return;
yield [m * m - n * n, 2 * m * n, m * m + n * n];
}
}
}
const triplesGenerator = pythagoreanTriplets(100);
for (const triplet of triplesGenerator) {
console.log(triplet);
}
最简单的几行代码,将数据插入到DB中
使用node-sqlite3模块,可以非常方便地调用SQL语句将数据插入到数据库中。注意serialize()的用法,因为javascript的异步执行机制,使用它可保证SQL语句的顺序执行,否则可能出现CREATE TABLE还没有完成,就插入数据的错误。
const sqlite3 = require('sqlite3').verbose();
// 将勾股数保存到数据库
function gougu2db(limit) {
const db = new sqlite3.Database('gougu.db');
db.serialize(() => {
db.run('DROP TABLE IF EXISTS gougu');
db.run('CREATE TABLE IF NOT EXISTS gougu (a INTEGER, b INTEGER, c INTEGER)');
const stmt = db.prepare('INSERT INTO gougu (a, b, c) VALUES (?, ?, ?)');
for (const triplet of pythagoreanTriplets(limit)) {
const [a, b, c] = triplet;
stmt.run(a, b, c);
}
stmt.finalize();
db.close();
});
}
gougu2db(100000);
上面的程序可以正常运行,可惜插入10万数据竟然耗时有76秒。
我的机器是i7-8700 3.2GHz 12核CPU,内存32GB,输出的db文件保存在固态硬盘。nodeJS版本为22.14.0。node里的sqlite3版本为5.1.7。
问题在于:使用stmt.run()运行SQL没有问题,但如果插入大量数据,则要启用事务机制。
使用事务
在批量插入数据之间,执行’BEGIN TRANSACTION’,最后进行’COMMIT’,程序性能大幅提升,插入10万行数据,只需3秒。
这里有一个细节需要注意,‘BEGIN TRANSACTION’要在prepare语句之间调用。
const sqlite3 = require('sqlite3').verbose();
// 将勾股数保存到数据库
function gougu2db(limit) {
const db = new sqlite3.Database('gougu.db');
db.serialize(() => {
db.run('DROP TABLE IF EXISTS gougu');
db.run('CREATE TABLE IF NOT EXISTS gougu (a INTEGER, b INTEGER, c INTEGER)');
db.run('BEGIN TRANSACTION'); // 注意开始事务的这条语句要出现在prepare之前!!!
const stmt = db.prepare('INSERT INTO gougu (a, b, c) VALUES (?, ?, ?)');
for (const triplet of pythagoreanTriplets(limit)) {
const [a, b, c] = triplet;
stmt.run(a, b, c);
}
db.run('COMMIT');
stmt.finalize();
db.close();
});
}
gougu2db(100000);
后面的测试使用100万行数据,启用事务之后,耗时27秒。
去掉verbose()
require(‘sqlite3’).verbose()这条语句是用于调试的,生产环境里不要写verbose(),刚才的程序插入100万行数据,减少到18秒。
网上很多资料说在建表之前执行下面两条语句,可以提升性能,但在我的程序里,并没发现明显的变化。
db.run('PRAGMA synchronous = OFF');
db.run('PRAGMA journal_mode = WAL');
分批次插入
一个事务里插入100万行数据,可能会占用较多的内存,可以分批次插入数据,比如每1万行、10万行为一个事务。这里引入了Promise的写法,好像代码增加了许多。
插入100万行数据,共需要21秒。
const sqlite3 = require('sqlite3');
async function createDatabase(dbFile) {
return new Promise((resolve, reject) => {
const db = new sqlite3.Database(dbFile);
db.exec(`
DROP TABLE IF EXISTS gougu;
CREATE TABLE IF NOT EXISTS gougu (a INTEGER, b INTEGER, c INTEGER);
`,
(err) => (err ? reject(err) : resolve(db))
);
});
}
async function insertData(db, triplets) {
return new Promise((resolve, reject) => {
db.serialize(() => {
db.run('BEGIN TRANSACTION'); // 开始事务
const stmt = db.prepare('INSERT INTO gougu (a, b, c) VALUES (?, ?, ?)');
try {
for (const triplet of triplets) {
const [a, b, c] = triplet;
stmt.run(a, b, c, (err) => {
if (err) {
throw err; // 抛出错误,触发回滚
}
});
}
db.run('COMMIT', (err) => {
if (err) {
reject(err); // 提交事务失败
} else {
resolve(triplets.length); // 返回插入的行数
}
});
stmt.finalize(); // 结束预处理语句
} catch (err) {
db.run('ROLLBACK', () => {
reject(err); // 回滚事务并返回错误
});
} finally {
}
});
});
}
// 将勾股数保存到数据库
async function gougu2db(limit) {
const start = performance.now();
const db = await createDatabase('gougu.db');
try {
let recordCount = 0;
BATCH_SIZE = 100000;
batch = [];
for (const triplet of pythagoreanTriplets(limit)) {
batch.push(triplet);
if (batch.length >= BATCH_SIZE) {
const insertedRows = await insertData(db, batch);
recordCount += insertedRows;
batch = [];
}
}
if (batch.length > 0) {
const insertedRows = await insertData(db, batch);
recordCount += insertedRows;
}
const duration = (performance.now() - start) / 1000;
console.log(`成功插入 ${recordCount} 条数据,执行时间: ${duration.toFixed(2)} 秒`);
} catch (err) {
console.error('插入数据时发生错误:', err);
} finally {
db.close(); // 关闭数据库连接
}
}
gougu2db(1000000);
sqlite库,方便使用async/await
node里还有一个Sqlite库,它是基于sqlite3 的高级封装库,提供了更现代化的 API 和更便捷的功能,基本上不用自己写Promise对象。
const sqlite3 = require('sqlite3');
const sqlite = require('sqlite');
async function createDatabase(dbFile) {
const db = await sqlite.open({
filename: dbFile,
driver: sqlite3.Database
});
await db.exec(`
DROP TABLE IF EXISTS gougu;
CREATE TABLE IF NOT EXISTS gougu (a INTEGER, b INTEGER, c INTEGER);
`);
return db;
}
async function insertData(db, triplets) {
await db.run('BEGIN TRANSACTION');
const stmt = await db.prepare('INSERT INTO gougu (a, b, c) VALUES (?, ?, ?)');
try {
for (const triplet of triplets) {
const [a, b, c] = triplet;
await stmt.run(a, b, c);
}
await db.run('COMMIT');
} catch (err) {
console.error('插入数据时发生错误:', err);
await db.run('ROLLBACK');
} finally {
await stmt.finalize();
}
}
// 将勾股数保存到数据库
async function gougu2db(limit) {
const start = performance.now();
const db = await createDatabase('gougu.db');
let recordCount = 0;
BATCH_SIZE = 100000;
batch = [];
try {
for (const triplet of pythagoreanTriplets(limit)) {
batch.push(triplet);
if (batch.length >= BATCH_SIZE) {
await insertData(db, batch);
recordCount += batch.length;
batch = [];
}
}
if (batch.length > 0) {
await insertData(db, batch);
recordCount += batch.length;
}
const duration = (performance.now() - start) / 1000;
console.log(`成功插入 ${recordCount} 条数据,执行时间: ${duration.toFixed(2)} 秒`);
} catch (err) {
console.error('插入数据时发生错误:', err);
} finally {
db.close(); // 关闭数据库连接
}
}
gougu2db(1000000);
不过,这个程序插入100万行数据,耗时为28秒。
问题出在await stmt.run(a, b, c)那一行里,可以利用Promise.all()改为异步执行,因此修改insertData()函数。
async function insertData(db, triplets) {
await db.run('BEGIN TRANSACTION');
const stmt = await db.prepare('INSERT INTO gougu (a, b, c) VALUES (?, ?, ?)');
try {
const promises = triplets.map(triplet => stmt.run(...triplet));
await Promise.all(promises);
await db.run('COMMIT');
} catch (err) {
console.error('插入数据时发生错误:', err);
await db.run('ROLLBACK');
} finally {
await stmt.finalize();
}
}
这样修改之后,插入100万行数据,重新回到22秒。
效率最高的库:better-sqlite3
还有一个更快的sqlite读写库:better-sqlite3,虽然不是异步操作,但效率比sqlite3快出许多倍,下面的程序写入100万行数据,只需1秒多!!!
function* pythagoreanTriplets(count) {
let countTriples = 0;
for (let m = 2; ; m++) {
for (let n = 1; n < m; n++) {
if (++countTriples > count) return;
yield [m * m - n * n, 2 * m * n, m * m + n * n];
}
}
}
const Database = require('better-sqlite3');
function createDatabase(dbFile) {
const db = new Database(dbFile);
db.exec(`
DROP TABLE IF EXISTS gougu;
CREATE TABLE IF NOT EXISTS gougu (a INTEGER, b INTEGER, c INTEGER);
`);
return db;
}
function insertData(db, insertStmt, triplets) {
const transaction = db.transaction((items) => {
for (const item of items) {
insertStmt.run(...item);
}
});
transaction(triplets);
}
// 将勾股数保存到数据库
function gougu2db(limit) {
const start = performance.now();
const db = createDatabase('gougu.db');
let recordCount = 0;
BATCH_SIZE = 10000;
batch = [];
try {
const insertStmt = db.prepare('INSERT INTO gougu (a, b, c) VALUES (?, ?, ?)');
for (const triplet of pythagoreanTriplets(limit)) {
batch.push(triplet);
if (batch.length >= BATCH_SIZE) {
insertData(db, insertStmt, batch);
recordCount += batch.length;
batch = [];
}
}
if (batch.length > 0) {
insertData(db, insertStmt, batch);
recordCount += batch.length;
}
const duration = (performance.now() - start) / 1000;
console.log(`成功插入 ${recordCount} 条数据,执行时间: ${duration.toFixed(2)} 秒`);
} catch (err) {
console.error('插入数据时发生错误:', err);
} finally {
db.close(); // 关闭数据库连接
}
}
gougu2db(1000000);
还可以在建库之后,执行两条pragma命令,让速度再快一点。写入100万行数据,不到1秒。
db.pragma('synchronous = OFF');
db.pragma('journal_mode = WAL');
小结
插入100万行数据,执行效率对比:
- sqlite3无事务,760秒
- sqlite3启用事务,27秒
- sqlite3去掉verbose(),18秒
- sqlite3分批插入,21秒
- better-sqlite3,1秒
几个库的官网使用链接: