agentdb
Version:
AgentDB - Frontier Memory Features with MCP Integration and Direct Vector Search: Causal reasoning, reflexion memory, skill library, automated learning, and raw vector similarity queries. 150x faster vector search. Full Claude Desktop support via Model Co
298 lines (250 loc) • 7.79 kB
text/typescript
/**
* QueryOptimizer - Advanced Query Optimization for AgentDB
*
* Implements:
* - Query result caching with TTL
* - Prepared statement pooling
* - Batch operation optimization
* - Index usage analysis
* - Query plan analysis
*/
// Database type from db-fallback
type Database = any;
export interface CacheConfig {
maxSize: number;
ttl: number; // milliseconds
enabled: boolean;
}
export interface QueryStats {
query: string;
executionCount: number;
totalTime: number;
avgTime: number;
cacheHits: number;
cacheMisses: number;
}
export class QueryOptimizer {
private db: Database;
private cache: Map<string, { result: any; timestamp: number }>;
private stats: Map<string, QueryStats>;
private config: CacheConfig;
constructor(db: Database, config?: Partial<CacheConfig>) {
this.db = db;
this.cache = new Map();
this.stats = new Map();
this.config = {
maxSize: 1000,
ttl: 60000, // 1 minute default
enabled: true,
...config
};
}
/**
* Execute query with caching
*/
query<T = any>(sql: string, params: any[] = [], cacheKey?: string): T {
const key = cacheKey || this.generateCacheKey(sql, params);
const startTime = Date.now();
// Check cache
if (this.config.enabled && this.cache.has(key)) {
const cached = this.cache.get(key)!;
if (Date.now() - cached.timestamp < this.config.ttl) {
this.recordStats(sql, Date.now() - startTime, true);
return cached.result;
} else {
this.cache.delete(key);
}
}
// Execute query
const stmt = this.db.prepare(sql);
const result = params.length > 0 ? stmt.all(...params) : stmt.all();
const executionTime = Date.now() - startTime;
this.recordStats(sql, executionTime, false);
// Cache result
if (this.config.enabled) {
this.cacheResult(key, result);
}
return result as T;
}
/**
* Execute query that returns single row
*/
queryOne<T = any>(sql: string, params: any[] = [], cacheKey?: string): T | undefined {
const results = this.query<T[]>(sql, params, cacheKey);
return results[0];
}
/**
* Execute write operation (no caching)
*/
execute(sql: string, params: any[] = []): any {
const startTime = Date.now();
const stmt = this.db.prepare(sql);
const result = params.length > 0 ? stmt.run(...params) : stmt.run();
this.recordStats(sql, Date.now() - startTime, false);
// Invalidate relevant cache entries
this.invalidateCache(sql);
return result;
}
/**
* Batch insert optimization
*/
batchInsert(table: string, columns: string[], rows: any[][]): void {
const placeholders = columns.map(() => '?').join(', ');
const sql = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders})`;
const transaction = this.db.transaction((rows: any[][]) => {
const stmt = this.db.prepare(sql);
for (const row of rows) {
stmt.run(...row);
}
});
const startTime = Date.now();
transaction(rows);
this.recordStats(`BATCH INSERT ${table}`, Date.now() - startTime, false);
}
/**
* Analyze query plan
*/
analyzeQuery(sql: string): {
plan: string;
usesIndex: boolean;
estimatedCost: number;
} {
const plan = this.db.prepare(`EXPLAIN QUERY PLAN ${sql}`).all();
const planText = plan.map((row: any) => row.detail).join(' ');
const usesIndex = planText.toLowerCase().includes('index');
const hasFullScan = planText.toLowerCase().includes('scan');
// Simple cost estimation
let estimatedCost = 1;
if (hasFullScan) estimatedCost *= 10;
if (!usesIndex) estimatedCost *= 5;
return {
plan: planText,
usesIndex,
estimatedCost
};
}
/**
* Get optimization suggestions
*/
getSuggestions(): string[] {
const suggestions: string[] = [];
// Analyze frequently run queries
const frequentQueries = Array.from(this.stats.values())
.filter(s => s.executionCount > 100)
.sort((a, b) => b.totalTime - a.totalTime)
.slice(0, 10);
for (const stat of frequentQueries) {
if (stat.avgTime > 50) {
const analysis = this.analyzeQuery(stat.query);
if (!analysis.usesIndex) {
suggestions.push(
`Slow query (${stat.avgTime.toFixed(1)}ms avg): Consider adding index for:\n${stat.query}`
);
}
if (stat.cacheHits === 0 && stat.executionCount > 50) {
suggestions.push(
`Frequently run query without cache hits: ${stat.query.substring(0, 50)}...`
);
}
}
}
// Check cache efficiency
const totalHits = Array.from(this.stats.values()).reduce((sum, s) => sum + s.cacheHits, 0);
const totalMisses = Array.from(this.stats.values()).reduce((sum, s) => sum + s.cacheMisses, 0);
const hitRate = totalHits / (totalHits + totalMisses) || 0;
if (hitRate < 0.3 && totalHits + totalMisses > 1000) {
suggestions.push(`Low cache hit rate (${(hitRate * 100).toFixed(1)}%). Consider increasing cache size or TTL.`);
}
return suggestions;
}
/**
* Get query statistics
*/
getStats(): QueryStats[] {
return Array.from(this.stats.values())
.sort((a, b) => b.totalTime - a.totalTime);
}
/**
* Clear cache
*/
clearCache(): void {
this.cache.clear();
}
/**
* Get cache statistics
*/
getCacheStats(): {
size: number;
hitRate: number;
totalHits: number;
totalMisses: number;
} {
const totalHits = Array.from(this.stats.values()).reduce((sum, s) => sum + s.cacheHits, 0);
const totalMisses = Array.from(this.stats.values()).reduce((sum, s) => sum + s.cacheMisses, 0);
return {
size: this.cache.size,
hitRate: totalHits / (totalHits + totalMisses) || 0,
totalHits,
totalMisses
};
}
// ========================================================================
// Private Methods
// ========================================================================
private generateCacheKey(sql: string, params: any[]): string {
return `${sql}:${JSON.stringify(params)}`;
}
private cacheResult(key: string, result: any): void {
if (this.cache.size >= this.config.maxSize) {
// Simple LRU: remove oldest entry
const oldestKey = this.cache.keys().next().value as string | undefined;
if (oldestKey) {
this.cache.delete(oldestKey);
}
}
this.cache.set(key, {
result,
timestamp: Date.now()
});
}
private invalidateCache(sql: string): void {
// Invalidate cache entries related to modified tables
const tables = this.extractTables(sql);
for (const [key] of this.cache) {
for (const table of tables) {
if (key.toLowerCase().includes(table.toLowerCase())) {
this.cache.delete(key);
}
}
}
}
private extractTables(sql: string): string[] {
const matches = sql.match(/(?:FROM|INTO|UPDATE|JOIN)\s+(\w+)/gi);
if (!matches) return [];
return matches
.map(m => m.split(/\s+/)[1])
.filter((v, i, a) => a.indexOf(v) === i); // unique
}
private recordStats(sql: string, time: number, cacheHit: boolean): void {
const key = sql.substring(0, 100); // Use first 100 chars as key
if (!this.stats.has(key)) {
this.stats.set(key, {
query: sql,
executionCount: 0,
totalTime: 0,
avgTime: 0,
cacheHits: 0,
cacheMisses: 0
});
}
const stat = this.stats.get(key)!;
stat.executionCount++;
stat.totalTime += time;
stat.avgTime = stat.totalTime / stat.executionCount;
if (cacheHit) {
stat.cacheHits++;
} else {
stat.cacheMisses++;
}
}
}