Skip to content

Commit fe08090

Browse files
authored
Merge pull request #33 from sskumar18/enhance/parallelization
parallelized requests
2 parents 5c8f826 + 8c6f832 commit fe08090

File tree

4 files changed

+257
-302
lines changed

4 files changed

+257
-302
lines changed

src/db/database-inspector.ts

Lines changed: 131 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export class DatabaseInspector {
3030

3131
// Get table information
3232
const tablesQuery = `
33-
SELECT
33+
SELECT
3434
schemaname,
3535
tablename,
3636
${includeSizes ? "pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as total_size," : ''}
@@ -49,35 +49,23 @@ export class DatabaseInspector {
4949

5050
if (tablesError) throw tablesError;
5151

52-
// Process each table
53-
for (const table of tables || []) {
54-
const tableInfo: any = {
55-
name: table.tablename,
56-
schema: table.schemaname,
57-
};
52+
// Process all tables in parallel — count/columns/indexes per table
53+
// are independent of each other so fire them all at once.
54+
shape.tables = await Promise.all(
55+
(tables || []).map(async (table: Record<string, any>) => {
56+
const tableInfo: any = {
57+
name: table.tablename,
58+
schema: table.schemaname,
59+
};
5860

59-
if (includeSizes) {
60-
tableInfo.total_size = table.total_size;
61-
tableInfo.table_size = table.table_size;
62-
tableInfo.indexes_size = table.indexes_size;
63-
}
64-
65-
// Get row count
66-
if (includeCounts) {
67-
const { data: countData, error: countError } = await this.supabase.rpc('exec_sql', {
68-
q: `SELECT COUNT(*) as count FROM ${table.tablename}`,
69-
params: {}
70-
});
71-
72-
if (!countError && countData && countData.length > 0) {
73-
tableInfo.row_count = parseInt(countData[0].count);
61+
if (includeSizes) {
62+
tableInfo.total_size = table.total_size;
63+
tableInfo.table_size = table.table_size;
64+
tableInfo.indexes_size = table.indexes_size;
7465
}
75-
}
7666

77-
// Get column information
78-
if (includeColumns) {
7967
const columnsQuery = `
80-
SELECT
68+
SELECT
8169
column_name,
8270
data_type,
8371
is_nullable,
@@ -88,20 +76,8 @@ export class DatabaseInspector {
8876
ORDER BY ordinal_position
8977
`;
9078

91-
const { data: columns, error: columnsError } = await this.supabase.rpc('exec_sql', {
92-
q: columnsQuery,
93-
params: {}
94-
});
95-
96-
if (!columnsError) {
97-
tableInfo.columns = columns;
98-
}
99-
}
100-
101-
// Get indexes
102-
if (includeIndexes) {
10379
const indexesQuery = `
104-
SELECT
80+
SELECT
10581
i.indexname,
10682
i.indexdef,
10783
${includeSizes ? "pg_size_pretty(pg_relation_size(i.schemaname||'.'||i.indexname)) as index_size," : ''}
@@ -113,18 +89,32 @@ export class DatabaseInspector {
11389
ORDER BY i.indexname
11490
`;
11591

116-
const { data: indexes, error: indexesError } = await this.supabase.rpc('exec_sql', {
117-
q: indexesQuery,
118-
params: {}
119-
});
120-
121-
if (!indexesError) {
122-
tableInfo.indexes = indexes;
92+
// Fire count, columns, indexes concurrently for this table
93+
const [countResult, columnsResult, indexesResult] = await Promise.all([
94+
includeCounts
95+
? this.supabase.rpc('exec_sql', { q: `SELECT COUNT(*) as count FROM ${table.tablename}`, params: {} })
96+
: Promise.resolve({ data: null, error: null }),
97+
includeColumns
98+
? this.supabase.rpc('exec_sql', { q: columnsQuery, params: {} })
99+
: Promise.resolve({ data: null, error: null }),
100+
includeIndexes
101+
? this.supabase.rpc('exec_sql', { q: indexesQuery, params: {} })
102+
: Promise.resolve({ data: null, error: null }),
103+
]);
104+
105+
if (!countResult.error && countResult.data?.length > 0) {
106+
tableInfo.row_count = parseInt(countResult.data[0].count);
107+
}
108+
if (!columnsResult.error && columnsResult.data) {
109+
tableInfo.columns = columnsResult.data;
110+
}
111+
if (!indexesResult.error && indexesResult.data) {
112+
tableInfo.indexes = indexesResult.data;
123113
}
124-
}
125114

126-
shape.tables.push(tableInfo);
127-
}
115+
return tableInfo;
116+
})
117+
);
128118

129119
// Get relationships (foreign keys)
130120
const relationshipsQuery = `
@@ -178,127 +168,103 @@ export class DatabaseInspector {
178168
timestamp: new Date().toISOString(),
179169
};
180170

181-
// Cache hit ratio
182-
if (includeCacheStats) {
183-
const cacheQuery = `
184-
SELECT
185-
sum(heap_blks_read) as heap_read,
186-
sum(heap_blks_hit) as heap_hit,
187-
sum(heap_blks_hit) / NULLIF((sum(heap_blks_hit) + sum(heap_blks_read)), 0) as cache_hit_ratio
188-
FROM pg_statio_user_tables
189-
`;
190-
191-
const { data: cacheData, error: cacheError } = await this.supabase.rpc('exec_sql', {
192-
q: cacheQuery,
193-
params: {}
194-
});
195-
196-
if (!cacheError && cacheData && cacheData.length > 0) {
197-
stats.cache_stats = {
198-
hit_ratio: parseFloat(cacheData[0].cache_hit_ratio || 0).toFixed(4),
199-
heap_blocks_hit: parseInt(cacheData[0].heap_hit || 0),
200-
heap_blocks_read: parseInt(cacheData[0].heap_read || 0),
201-
};
202-
}
171+
// Build all four queries upfront (empty string = skipped)
172+
const cacheQuery = `
173+
SELECT
174+
sum(heap_blks_read) as heap_read,
175+
sum(heap_blks_hit) as heap_hit,
176+
sum(heap_blks_hit) / NULLIF((sum(heap_blks_hit) + sum(heap_blks_read)), 0) as cache_hit_ratio
177+
FROM pg_statio_user_tables
178+
`;
179+
180+
const tableStatsQuery = `
181+
SELECT
182+
schemaname,
183+
relname as table_name,
184+
seq_scan, seq_tup_read, idx_scan, idx_tup_fetch,
185+
n_tup_ins as inserts, n_tup_upd as updates, n_tup_del as deletes,
186+
n_live_tup as live_rows, n_dead_tup as dead_rows
187+
FROM pg_stat_user_tables
188+
WHERE schemaname = 'public'
189+
${tableName ? `AND relname = '${tableName}'` : ''}
190+
ORDER BY seq_scan + idx_scan DESC
191+
`;
192+
193+
const indexUsageQuery = `
194+
SELECT
195+
schemaname, tablename as table_name, indexname as index_name,
196+
idx_scan as scans, idx_tup_read as rows_read, idx_tup_fetch as rows_fetched
197+
FROM pg_stat_user_indexes
198+
WHERE schemaname = 'public'
199+
${tableName ? `AND tablename = '${tableName}'` : ''}
200+
ORDER BY idx_scan DESC
201+
`;
202+
203+
const unusedIndexesQuery = `
204+
SELECT
205+
schemaname, tablename as table_name, indexname as index_name,
206+
pg_size_pretty(pg_relation_size(schemaname||'.'||indexname)) as index_size
207+
FROM pg_stat_user_indexes
208+
WHERE schemaname = 'public' AND idx_scan = 0 AND indexname NOT LIKE '%_pkey'
209+
ORDER BY pg_relation_size(schemaname||'.'||indexname) DESC
210+
`;
211+
212+
// Fire all independent stat queries in parallel
213+
const [
214+
{ data: cacheData, error: cacheError },
215+
{ data: tableStats, error: tableStatsError },
216+
{ data: indexUsage, error: indexUsageError },
217+
{ data: unusedIndexes, error: unusedError },
218+
] = await Promise.all([
219+
includeCacheStats
220+
? this.supabase.rpc('exec_sql', { q: cacheQuery, params: {} })
221+
: Promise.resolve({ data: null, error: null }),
222+
includeQueryStats
223+
? this.supabase.rpc('exec_sql', { q: tableStatsQuery, params: {} })
224+
: Promise.resolve({ data: null, error: null }),
225+
includeIndexUsage
226+
? this.supabase.rpc('exec_sql', { q: indexUsageQuery, params: {} })
227+
: Promise.resolve({ data: null, error: null }),
228+
includeIndexUsage && !tableName
229+
? this.supabase.rpc('exec_sql', { q: unusedIndexesQuery, params: {} })
230+
: Promise.resolve({ data: null, error: null }),
231+
]);
232+
233+
if (!cacheError && cacheData?.length > 0) {
234+
stats.cache_stats = {
235+
hit_ratio: parseFloat(cacheData[0].cache_hit_ratio || 0).toFixed(4),
236+
heap_blocks_hit: parseInt(cacheData[0].heap_hit || 0),
237+
heap_blocks_read: parseInt(cacheData[0].heap_read || 0),
238+
};
203239
}
204240

205-
// Table access statistics
206-
if (includeQueryStats) {
207-
const tableStatsQuery = `
208-
SELECT
209-
schemaname,
210-
relname as table_name,
211-
seq_scan,
212-
seq_tup_read,
213-
idx_scan,
214-
idx_tup_fetch,
215-
n_tup_ins as inserts,
216-
n_tup_upd as updates,
217-
n_tup_del as deletes,
218-
n_live_tup as live_rows,
219-
n_dead_tup as dead_rows
220-
FROM pg_stat_user_tables
221-
WHERE schemaname = 'public'
222-
${tableName ? `AND relname = '${tableName}'` : ''}
223-
ORDER BY seq_scan + idx_scan DESC
224-
`;
225-
226-
const { data: tableStats, error: tableStatsError } = await this.supabase.rpc('exec_sql', {
227-
q: tableStatsQuery,
228-
params: {}
229-
});
230-
231-
if (!tableStatsError) {
232-
stats.table_stats = tableStats?.map((t: any) => ({
233-
table_name: t.table_name,
234-
sequential_scans: parseInt(t.seq_scan || 0),
235-
sequential_rows_read: parseInt(t.seq_tup_read || 0),
236-
index_scans: parseInt(t.idx_scan || 0),
237-
index_rows_fetched: parseInt(t.idx_tup_fetch || 0),
238-
inserts: parseInt(t.inserts || 0),
239-
updates: parseInt(t.updates || 0),
240-
deletes: parseInt(t.deletes || 0),
241-
live_rows: parseInt(t.live_rows || 0),
242-
dead_rows: parseInt(t.dead_rows || 0),
243-
}));
244-
}
241+
if (!tableStatsError && tableStats) {
242+
stats.table_stats = tableStats.map((t: any) => ({
243+
table_name: t.table_name,
244+
sequential_scans: parseInt(t.seq_scan || 0),
245+
sequential_rows_read: parseInt(t.seq_tup_read || 0),
246+
index_scans: parseInt(t.idx_scan || 0),
247+
index_rows_fetched: parseInt(t.idx_tup_fetch || 0),
248+
inserts: parseInt(t.inserts || 0),
249+
updates: parseInt(t.updates || 0),
250+
deletes: parseInt(t.deletes || 0),
251+
live_rows: parseInt(t.live_rows || 0),
252+
dead_rows: parseInt(t.dead_rows || 0),
253+
}));
245254
}
246255

247-
// Index usage statistics
248-
if (includeIndexUsage) {
249-
const indexUsageQuery = `
250-
SELECT
251-
schemaname,
252-
tablename as table_name,
253-
indexname as index_name,
254-
idx_scan as scans,
255-
idx_tup_read as rows_read,
256-
idx_tup_fetch as rows_fetched
257-
FROM pg_stat_user_indexes
258-
WHERE schemaname = 'public'
259-
${tableName ? `AND tablename = '${tableName}'` : ''}
260-
ORDER BY idx_scan DESC
261-
`;
262-
263-
const { data: indexUsage, error: indexUsageError } = await this.supabase.rpc('exec_sql', {
264-
q: indexUsageQuery,
265-
params: {}
266-
});
267-
268-
if (!indexUsageError) {
269-
stats.index_usage = indexUsage?.map((i: any) => ({
270-
table_name: i.table_name,
271-
index_name: i.index_name,
272-
scans: parseInt(i.scans || 0),
273-
rows_read: parseInt(i.rows_read || 0),
274-
rows_fetched: parseInt(i.rows_fetched || 0),
275-
}));
276-
}
256+
if (!indexUsageError && indexUsage) {
257+
stats.index_usage = indexUsage.map((i: any) => ({
258+
table_name: i.table_name,
259+
index_name: i.index_name,
260+
scans: parseInt(i.scans || 0),
261+
rows_read: parseInt(i.rows_read || 0),
262+
rows_fetched: parseInt(i.rows_fetched || 0),
263+
}));
277264
}
278265

279-
// Find unused indexes
280-
if (includeIndexUsage && !tableName) {
281-
const unusedIndexesQuery = `
282-
SELECT
283-
schemaname,
284-
tablename as table_name,
285-
indexname as index_name,
286-
pg_size_pretty(pg_relation_size(schemaname||'.'||indexname)) as index_size
287-
FROM pg_stat_user_indexes
288-
WHERE schemaname = 'public'
289-
AND idx_scan = 0
290-
AND indexname NOT LIKE '%_pkey'
291-
ORDER BY pg_relation_size(schemaname||'.'||indexname) DESC
292-
`;
293-
294-
const { data: unusedIndexes, error: unusedError } = await this.supabase.rpc('exec_sql', {
295-
q: unusedIndexesQuery,
296-
params: {}
297-
});
298-
299-
if (!unusedError) {
300-
stats.unused_indexes = unusedIndexes;
301-
}
266+
if (!unusedError && unusedIndexes) {
267+
stats.unused_indexes = unusedIndexes;
302268
}
303269

304270
return stats;
@@ -347,7 +313,7 @@ export class DatabaseInspector {
347313
*/
348314
async getConnectionInfo() {
349315
const query = `
350-
SELECT
316+
SELECT
351317
current_database() as database_name,
352318
current_schema() as current_schema,
353319
version() as postgres_version,
@@ -364,4 +330,3 @@ export class DatabaseInspector {
364330
return data && data.length > 0 ? data[0] : null;
365331
}
366332
}
367-

0 commit comments

Comments
 (0)