Skip to content

Commit 33c61ba

Browse files
⚡ [Performance] Batch execution of DROP INDEX operations (#254)
* perf: Batch execution of DROP INDEX operations Batched multiple `DROP INDEX IF EXISTS` statements into a single `executeQuery` call to eliminate N+1 query transaction overhead for multiple dependent indexes. Co-authored-by: zknpr <96851588+zknpr@users.noreply.github.com> * perf: Batch execution of DROP INDEX operations securely Batches multiple `DROP INDEX IF EXISTS` statements into a single `executeQuery` call within the transaction block. This safely eliminates N+1 query overhead for multiple dependent indexes while preserving atomicity in case of rollback. Co-authored-by: zknpr <96851588+zknpr@users.noreply.github.com> --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent 08d025b commit 33c61ba

2 files changed

Lines changed: 94 additions & 7 deletions

File tree

src/core/sqlite-db.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -646,17 +646,18 @@ class WasmDatabaseEngine implements DatabaseOperations {
646646

647647
const escapedTable = escapeIdentifier(table);
648648

649-
// Drop specified dependent indexes first
650-
if (dropDependentIndexes && dropDependentIndexes.length > 0) {
651-
for (const indexName of dropDependentIndexes) {
652-
await this.executeQuery(`DROP INDEX IF EXISTS ${escapeIdentifier(indexName)}`);
653-
}
654-
}
655-
656649
// Now drop the columns within a single transaction for better performance
657650
// This avoids N+1 query transaction overhead for multiple columns
658651
await this.executeQuery('BEGIN TRANSACTION');
659652
try {
653+
// Drop specified dependent indexes first inside the transaction
654+
if (dropDependentIndexes && dropDependentIndexes.length > 0) {
655+
const dropIndexStatements = dropDependentIndexes
656+
.map((indexName) => `DROP INDEX IF EXISTS ${escapeIdentifier(indexName)};`)
657+
.join('\n');
658+
await this.executeQuery(dropIndexStatements);
659+
}
660+
660661
for (const col of columns) {
661662
const sql = `ALTER TABLE ${escapedTable} DROP COLUMN ${escapeIdentifier(col)}`;
662663
await this.executeQuery(sql);
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import { createDatabaseEngine, WasmDatabaseEngine } from '../../src/core/sqlite-db';
4+
5+
async function setupTestDb(db: WasmDatabaseEngine, numIndexes: number) {
6+
await db.executeQuery(`DROP TABLE IF EXISTS test_table`);
7+
await db.executeQuery(`CREATE TABLE test_table (id INTEGER PRIMARY KEY, col TEXT)`);
8+
for (let i = 0; i < numIndexes; i++) {
9+
await db.executeQuery(`CREATE INDEX idx_${i} ON test_table(col)`);
10+
}
11+
const indexNames = Array.from({ length: numIndexes }, (_, i) => `idx_${i}`);
12+
return indexNames;
13+
}
14+
15+
async function measureUnbatched(db: WasmDatabaseEngine, indexNames: string[]) {
16+
const start = performance.now();
17+
for (const indexName of indexNames) {
18+
await db.executeQuery(`DROP INDEX IF EXISTS "${indexName}"`);
19+
}
20+
return performance.now() - start;
21+
}
22+
23+
async function measureBatched(db: WasmDatabaseEngine, indexNames: string[]) {
24+
const start = performance.now();
25+
if (indexNames.length > 0) {
26+
const dropStatements = indexNames.map(name => `DROP INDEX IF EXISTS "${name}";`).join('\n');
27+
await db.executeQuery(dropStatements);
28+
}
29+
return performance.now() - start;
30+
}
31+
32+
async function runBenchmark() {
33+
console.log('Starting Index Drop Benchmark (Hygienic)...');
34+
35+
try {
36+
const wasmBinary = fs.readFileSync(path.resolve(__dirname, '../../node_modules/sql.js/dist/sql-wasm.wasm'));
37+
const engineResult = await createDatabaseEngine({ wasmBinary });
38+
const db = engineResult.operations as WasmDatabaseEngine;
39+
40+
const numIndexes = 50;
41+
const iterations = 10;
42+
43+
console.log('Warming up...');
44+
for (let i = 0; i < 3; i++) {
45+
let idxs = await setupTestDb(db, numIndexes);
46+
await measureUnbatched(db, idxs);
47+
idxs = await setupTestDb(db, numIndexes);
48+
await measureBatched(db, idxs);
49+
}
50+
51+
let unbatchedTotal = 0;
52+
let batchedTotal = 0;
53+
54+
console.log(`Running ${iterations} iterations (Alternating)...`);
55+
for (let i = 0; i < iterations; i++) {
56+
// Unbatched first
57+
let idxs = await setupTestDb(db, numIndexes);
58+
unbatchedTotal += await measureUnbatched(db, idxs);
59+
60+
// Batched second
61+
idxs = await setupTestDb(db, numIndexes);
62+
batchedTotal += await measureBatched(db, idxs);
63+
64+
// Batched first (reverse order)
65+
idxs = await setupTestDb(db, numIndexes);
66+
batchedTotal += await measureBatched(db, idxs);
67+
68+
// Unbatched second
69+
idxs = await setupTestDb(db, numIndexes);
70+
unbatchedTotal += await measureUnbatched(db, idxs);
71+
}
72+
73+
const avgUnbatched = unbatchedTotal / (iterations * 2);
74+
const avgBatched = batchedTotal / (iterations * 2);
75+
76+
console.log('--- Results ---');
77+
console.log(`Average Unbatched (${numIndexes} indexes): ${avgUnbatched.toFixed(2)}ms`);
78+
console.log(`Average Batched (${numIndexes} indexes): ${avgBatched.toFixed(2)}ms`);
79+
console.log(`Improvement: ${((avgUnbatched - avgBatched) / avgUnbatched * 100).toFixed(2)}%`);
80+
81+
} catch (e) {
82+
console.error('Benchmark failed:', e);
83+
}
84+
}
85+
86+
runBenchmark();

0 commit comments

Comments
 (0)