mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 04:04:06 -08:00
⚡ Performance improvements for executions count on Postgres (#1888)
* Performance improvements for executions count on Postgres As reported by a community member https://community.n8n.io/t/stress-load-testing/4846/5 and https://github.com/n8n-io/n8n/issues/1578, when using postgres with a big volume of executions, the executions list's performance degrades. This PR is aimed at Postgres specifically by querying postgres' stats collector instead of running a full table scan, providing a good estimate. More can be read here: https://www.citusdata.com/blog/2016/10/12/count-performance/ * Removed order of magnitude so we display closer numbers * Making count based on statistics only when not applying filters * ⚡ Minor styling improvements Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
parent
297d9fe77d
commit
8d235e94cb
|
@ -188,6 +188,7 @@ export interface IExecutionsListResponse {
|
||||||
count: number;
|
count: number;
|
||||||
// results: IExecutionShortResponse[];
|
// results: IExecutionShortResponse[];
|
||||||
results: IExecutionsSummary[];
|
results: IExecutionsSummary[];
|
||||||
|
estimated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IExecutionsStopData {
|
export interface IExecutionsStopData {
|
||||||
|
|
|
@ -33,6 +33,7 @@ import {
|
||||||
CredentialsHelper,
|
CredentialsHelper,
|
||||||
CredentialsOverwrites,
|
CredentialsOverwrites,
|
||||||
CredentialTypes,
|
CredentialTypes,
|
||||||
|
DatabaseType,
|
||||||
Db,
|
Db,
|
||||||
ExternalHooks,
|
ExternalHooks,
|
||||||
GenericHelpers,
|
GenericHelpers,
|
||||||
|
@ -88,6 +89,7 @@ import {
|
||||||
IRunData,
|
IRunData,
|
||||||
IWorkflowBase,
|
IWorkflowBase,
|
||||||
IWorkflowCredentials,
|
IWorkflowCredentials,
|
||||||
|
LoggerProxy,
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
@ -1612,8 +1614,7 @@ class App {
|
||||||
executingWorkflowIds.push(...this.activeExecutionsInstance.getActiveExecutions().map(execution => execution.id.toString()) as string[]);
|
executingWorkflowIds.push(...this.activeExecutionsInstance.getActiveExecutions().map(execution => execution.id.toString()) as string[]);
|
||||||
|
|
||||||
const countFilter = JSON.parse(JSON.stringify(filter));
|
const countFilter = JSON.parse(JSON.stringify(filter));
|
||||||
countFilter.select = ['id'];
|
countFilter.id = Not(In(executingWorkflowIds));
|
||||||
countFilter.where = {id: Not(In(executingWorkflowIds))};
|
|
||||||
|
|
||||||
const resultsQuery = await Db.collections.Execution!
|
const resultsQuery = await Db.collections.Execution!
|
||||||
.createQueryBuilder("execution")
|
.createQueryBuilder("execution")
|
||||||
|
@ -1645,10 +1646,10 @@ class App {
|
||||||
|
|
||||||
const resultsPromise = resultsQuery.getMany();
|
const resultsPromise = resultsQuery.getMany();
|
||||||
|
|
||||||
const countPromise = Db.collections.Execution!.count(countFilter);
|
const countPromise = getExecutionsCount(countFilter);
|
||||||
|
|
||||||
const results: IExecutionFlattedDb[] = await resultsPromise;
|
const results: IExecutionFlattedDb[] = await resultsPromise;
|
||||||
const count = await countPromise;
|
const countedObjects = await countPromise;
|
||||||
|
|
||||||
const returnResults: IExecutionsSummary[] = [];
|
const returnResults: IExecutionsSummary[] = [];
|
||||||
|
|
||||||
|
@ -1667,8 +1668,9 @@ class App {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
count,
|
count: countedObjects.count,
|
||||||
results: returnResults,
|
results: returnResults,
|
||||||
|
estimated: countedObjects.estimate,
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -2161,3 +2163,35 @@ export async function start(): Promise<void> {
|
||||||
await app.externalHooks.run('n8n.ready', [app]);
|
await app.externalHooks.run('n8n.ready', [app]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getExecutionsCount(countFilter: IDataObject): Promise<{ count: number; estimate: boolean; }> {
|
||||||
|
|
||||||
|
const dbType = await GenericHelpers.getConfigValue('database.type') as DatabaseType;
|
||||||
|
const filteredFields = Object.keys(countFilter).filter(field => field !== 'id');
|
||||||
|
|
||||||
|
// Do regular count for other databases than pgsql and
|
||||||
|
// if we are filtering based on workflowId or finished fields.
|
||||||
|
if (dbType !== 'postgresdb' || filteredFields.length > 0) {
|
||||||
|
const count = await Db.collections.Execution!.count(countFilter);
|
||||||
|
return { count, estimate: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get an estimate of rows count.
|
||||||
|
const estimateRowsNumberSql = "SELECT n_live_tup FROM pg_stat_all_tables WHERE relname = 'execution_entity';";
|
||||||
|
const rows: Array<{ n_live_tup: string }> = await Db.collections.Execution!.query(estimateRowsNumberSql);
|
||||||
|
|
||||||
|
const estimate = parseInt(rows[0].n_live_tup, 10);
|
||||||
|
// If over 100k, return just an estimate.
|
||||||
|
if (estimate > 100000) {
|
||||||
|
// if less than 100k, we get the real count as even a full
|
||||||
|
// table scan should not take so long.
|
||||||
|
return { count: estimate, estimate: true };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
LoggerProxy.warn('Unable to get executions count from postgres: ' + err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = await Db.collections.Execution!.count(countFilter);
|
||||||
|
return { count, estimate: false };
|
||||||
|
}
|
||||||
|
|
|
@ -325,6 +325,7 @@ export interface IExecutionShortResponse {
|
||||||
export interface IExecutionsListResponse {
|
export interface IExecutionsListResponse {
|
||||||
count: number;
|
count: number;
|
||||||
results: IExecutionsSummary[];
|
results: IExecutionsSummary[];
|
||||||
|
estimated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IExecutionsCurrentSummaryExtended {
|
export interface IExecutionsCurrentSummaryExtended {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<span>
|
<span>
|
||||||
<el-dialog :visible="dialogVisible" append-to-body width="80%" :title="`Workflow Executions (${combinedExecutions.length}/${combinedExecutionsCount})`" :before-close="closeDialog">
|
<el-dialog :visible="dialogVisible" append-to-body width="80%" :title="`Workflow Executions ${combinedExecutions.length}/${finishedExecutionsCountEstimated === true ? '~' : ''}${combinedExecutionsCount}`" :before-close="closeDialog">
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="4" class="filter-headline">
|
<el-col :span="4" class="filter-headline">
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
|
|
||||||
<div class="selection-options">
|
<div class="selection-options">
|
||||||
<span v-if="checkAll === true || isIndeterminate === true">
|
<span v-if="checkAll === true || isIndeterminate === true">
|
||||||
Selected: {{numSelected}}/{{finishedExecutionsCount}}
|
Selected: {{numSelected}} / <span v-if="finishedExecutionsCountEstimated === true">~</span>{{finishedExecutionsCount}}
|
||||||
<el-button type="danger" title="Delete Selected" icon="el-icon-delete" size="mini" @click="handleDeleteSelected" circle></el-button>
|
<el-button type="danger" title="Delete Selected" icon="el-icon-delete" size="mini" @click="handleDeleteSelected" circle></el-button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -142,7 +142,7 @@
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div class="load-more" v-if="finishedExecutionsCount > finishedExecutions.length">
|
<div class="load-more" v-if="finishedExecutionsCount > finishedExecutions.length || finishedExecutionsCountEstimated === true">
|
||||||
<el-button title="Load More" @click="loadMore()" size="small" :disabled="isDataLoading">
|
<el-button title="Load More" @click="loadMore()" size="small" :disabled="isDataLoading">
|
||||||
<font-awesome-icon icon="sync" /> Load More
|
<font-awesome-icon icon="sync" /> Load More
|
||||||
</el-button>
|
</el-button>
|
||||||
|
@ -200,6 +200,7 @@ export default mixins(
|
||||||
return {
|
return {
|
||||||
finishedExecutions: [] as IExecutionsSummary[],
|
finishedExecutions: [] as IExecutionsSummary[],
|
||||||
finishedExecutionsCount: 0,
|
finishedExecutionsCount: 0,
|
||||||
|
finishedExecutionsCountEstimated: false,
|
||||||
|
|
||||||
checkAll: false,
|
checkAll: false,
|
||||||
autoRefresh: true,
|
autoRefresh: true,
|
||||||
|
@ -256,7 +257,7 @@ export default mixins(
|
||||||
return returnData;
|
return returnData;
|
||||||
},
|
},
|
||||||
combinedExecutionsCount (): number {
|
combinedExecutionsCount (): number {
|
||||||
return this.activeExecutions.length + this.finishedExecutionsCount;
|
return 0 + this.activeExecutions.length + this.finishedExecutionsCount;
|
||||||
},
|
},
|
||||||
numSelected (): number {
|
numSelected (): number {
|
||||||
if (this.checkAll === true) {
|
if (this.checkAll === true) {
|
||||||
|
@ -489,16 +490,19 @@ export default mixins(
|
||||||
}
|
}
|
||||||
this.finishedExecutions = this.finishedExecutions.filter(execution => !gaps.includes(parseInt(execution.id, 10)) && lastId >= parseInt(execution.id, 10));
|
this.finishedExecutions = this.finishedExecutions.filter(execution => !gaps.includes(parseInt(execution.id, 10)) && lastId >= parseInt(execution.id, 10));
|
||||||
this.finishedExecutionsCount = results[0].count;
|
this.finishedExecutionsCount = results[0].count;
|
||||||
|
this.finishedExecutionsCountEstimated = results[0].estimated;
|
||||||
},
|
},
|
||||||
async loadFinishedExecutions (): Promise<void> {
|
async loadFinishedExecutions (): Promise<void> {
|
||||||
if (this.filter.status === 'running') {
|
if (this.filter.status === 'running') {
|
||||||
this.finishedExecutions = [];
|
this.finishedExecutions = [];
|
||||||
this.finishedExecutionsCount = 0;
|
this.finishedExecutionsCount = 0;
|
||||||
|
this.finishedExecutionsCountEstimated = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = await this.restApi().getPastExecutions(this.workflowFilterPast, this.requestItemsPerRequest);
|
const data = await this.restApi().getPastExecutions(this.workflowFilterPast, this.requestItemsPerRequest);
|
||||||
this.finishedExecutions = data.results;
|
this.finishedExecutions = data.results;
|
||||||
this.finishedExecutionsCount = data.count;
|
this.finishedExecutionsCount = data.count;
|
||||||
|
this.finishedExecutionsCountEstimated = data.estimated;
|
||||||
},
|
},
|
||||||
async loadMore () {
|
async loadMore () {
|
||||||
if (this.filter.status === 'running') {
|
if (this.filter.status === 'running') {
|
||||||
|
@ -526,6 +530,7 @@ export default mixins(
|
||||||
|
|
||||||
this.finishedExecutions.push.apply(this.finishedExecutions, data.results);
|
this.finishedExecutions.push.apply(this.finishedExecutions, data.results);
|
||||||
this.finishedExecutionsCount = data.count;
|
this.finishedExecutionsCount = data.count;
|
||||||
|
this.finishedExecutionsCountEstimated = data.estimated;
|
||||||
|
|
||||||
this.isDataLoading = false;
|
this.isDataLoading = false;
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue