feat: v0.2.4 异步Compaction + 槽位压缩 + EXPLAIN + 慢查询日志
This commit is contained in:
@@ -142,6 +142,7 @@ export class MetonaSqlark {
|
|||||||
/** 执行 SQL 字符串查询 */
|
/** 执行 SQL 字符串查询 */
|
||||||
async query(sql: string): Promise<unknown> {
|
async query(sql: string): Promise<unknown> {
|
||||||
this.ensureReady();
|
this.ensureReady();
|
||||||
|
const startTime = this.debug ? Date.now() : 0;
|
||||||
|
|
||||||
await this.pluginManager.trigger('beforeQuery', sql);
|
await this.pluginManager.trigger('beforeQuery', sql);
|
||||||
|
|
||||||
@@ -150,6 +151,12 @@ export class MetonaSqlark {
|
|||||||
|
|
||||||
await this.pluginManager.trigger('afterQuery', sql, result);
|
await this.pluginManager.trigger('afterQuery', sql, result);
|
||||||
|
|
||||||
|
if (this.debug) {
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
const rows = Array.isArray(result) ? (result as any[]).length : 0;
|
||||||
|
this._debug(`query [${elapsed}ms] ${rows} rows: ${sql.slice(0, 100)}`);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export class LSM {
|
|||||||
private sstableStore: SSTableStore;
|
private sstableStore: SSTableStore;
|
||||||
private operationCount = 0;
|
private operationCount = 0;
|
||||||
private initialized = false;
|
private initialized = false;
|
||||||
|
private compacting = false; // 防止重复触发 compaction
|
||||||
|
|
||||||
constructor(config: LSMConfig) {
|
constructor(config: LSMConfig) {
|
||||||
this.memtable = new MemTable(config.memtableSizeThreshold ?? DEFAULT_MEMTABLE_SIZE);
|
this.memtable = new MemTable(config.memtableSizeThreshold ?? DEFAULT_MEMTABLE_SIZE);
|
||||||
@@ -182,11 +183,33 @@ export class LSM {
|
|||||||
this.levels[0].push(meta);
|
this.levels[0].push(meta);
|
||||||
this.immutableMemtable = null;
|
this.immutableMemtable = null;
|
||||||
|
|
||||||
if (this.levels[0].length >= 4) {
|
// 异步触发 compaction(不阻塞当前写入)
|
||||||
this.compactLevelSync(0);
|
if (this.levels[0].length >= 4 && !this.compacting) {
|
||||||
|
this.scheduleCompact(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 异步调度 compaction,使用 setTimeout 分片执行 */
|
||||||
|
private scheduleCompact(level: number): void {
|
||||||
|
if (level >= MAX_LSM_LEVELS - 1 || this.compacting) return;
|
||||||
|
this.compacting = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
this.compactLevelSync(level);
|
||||||
|
} finally {
|
||||||
|
this.compacting = false;
|
||||||
|
// 连续触发:如果 compaction 后仍然超标,继续调度
|
||||||
|
if (this.levels[level].length >= 4) {
|
||||||
|
this.scheduleCompact(level);
|
||||||
|
}
|
||||||
|
// 检查下一级是否需要 compaction
|
||||||
|
if (level + 1 < MAX_LSM_LEVELS - 1 && this.levels[level + 1].length >= 4) {
|
||||||
|
this.scheduleCompact(level + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
// 读取
|
// 读取
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
|
|||||||
@@ -128,6 +128,52 @@ export function freeSlot(buf: ArrayBuffer, slotIndex: number): void {
|
|||||||
setSlotEntry(buf, slotIndex, { offset: 0, length: 0 });
|
setSlotEntry(buf, slotIndex, { offset: 0, length: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 压缩页面槽位:移除已删除 slot,整理碎片空间。
|
||||||
|
* 将有效数据紧凑排列,释放空洞。
|
||||||
|
*/
|
||||||
|
export function compactSlots(buf: ArrayBuffer): number {
|
||||||
|
const view = new DataView(buf);
|
||||||
|
const slotCount = view.getUint16(9, false);
|
||||||
|
if (slotCount === 0) return 0;
|
||||||
|
|
||||||
|
// 收集有效 slot(offset>0 的)
|
||||||
|
const validSlots: { index: number; offset: number; length: number; data: Uint8Array }[] = [];
|
||||||
|
for (let i = 0; i < slotCount; i++) {
|
||||||
|
const entry = getSlotEntry(buf, i);
|
||||||
|
if (entry.offset > 0 && entry.length > 0) {
|
||||||
|
const data = new Uint8Array(buf, entry.offset, entry.length);
|
||||||
|
validSlots.push({ index: i, offset: entry.offset, length: entry.length, data: new Uint8Array(data) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validSlots.length === slotCount) return 0; // 无碎片
|
||||||
|
|
||||||
|
// 从页面底部重新紧凑排列
|
||||||
|
let dataEnd = PAGE_SIZE;
|
||||||
|
const newSlots: { offset: number; length: number }[] = [];
|
||||||
|
|
||||||
|
for (let i = validSlots.length - 1; i >= 0; i--) {
|
||||||
|
const s = validSlots[i];
|
||||||
|
dataEnd -= s.length;
|
||||||
|
new Uint8Array(buf).set(s.data, dataEnd);
|
||||||
|
newSlots.unshift({ offset: dataEnd, length: s.length });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重写 slot directory
|
||||||
|
view.setUint16(9, validSlots.length, false); // slotCount
|
||||||
|
view.setUint16(7, dataEnd, false); // freeEnd
|
||||||
|
for (let i = 0; i < validSlots.length; i++) {
|
||||||
|
setSlotEntry(buf, i, newSlots[i]);
|
||||||
|
}
|
||||||
|
// 清除剩余 slot 条目
|
||||||
|
for (let i = validSlots.length; i < slotCount; i++) {
|
||||||
|
setSlotEntry(buf, i, { offset: 0, length: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return slotCount - validSlots.length; // 回收的 slot 数量
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 读取指定 slot 的行数据。
|
* 读取指定 slot 的行数据。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export class QueryExecutor {
|
|||||||
async execute(stmt: Statement): Promise<unknown> {
|
async execute(stmt: Statement): Promise<unknown> {
|
||||||
switch (stmt.type) {
|
switch (stmt.type) {
|
||||||
case 'SELECT': return this.executeSelect(stmt);
|
case 'SELECT': return this.executeSelect(stmt);
|
||||||
|
case 'EXPLAIN': return this.executeExplain(stmt as any);
|
||||||
case 'INSERT': return this.executeInsert(stmt);
|
case 'INSERT': return this.executeInsert(stmt);
|
||||||
case 'UPDATE': return this.executeUpdate(stmt);
|
case 'UPDATE': return this.executeUpdate(stmt);
|
||||||
case 'DELETE': return this.executeDelete(stmt);
|
case 'DELETE': return this.executeDelete(stmt);
|
||||||
@@ -35,6 +36,31 @@ export class QueryExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** EXPLAIN: 输出查询计划 */
|
||||||
|
private async executeExplain(stmt: { query: Statement }): Promise<Record<string, unknown>> {
|
||||||
|
const plan = compileStatement(stmt.query.type === 'SELECT' ? stmt.query : stmt.query as any);
|
||||||
|
const startTime = Date.now();
|
||||||
|
let result: unknown = null;
|
||||||
|
try {
|
||||||
|
result = await this.execute(stmt.query);
|
||||||
|
} catch { /* explain 即使执行失败也返回计划 */ }
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
const rows = Array.isArray(result) ? result.length : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: stmt.query.type,
|
||||||
|
table: plan.table,
|
||||||
|
columns: plan.columns,
|
||||||
|
where: plan.where || {},
|
||||||
|
orderBy: plan.orderBy || [],
|
||||||
|
limit: plan.limit,
|
||||||
|
offset: plan.offset,
|
||||||
|
usingIndex: plan.table ? 'auto' : 'none',
|
||||||
|
estimatedRows: rows,
|
||||||
|
actualTimeMs: elapsed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
// SELECT
|
// SELECT
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user