perf(core): Add partial index on execution_entity to fix executions list CPU spike (#32116)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Savelii
2026-06-15 15:02:24 +02:00
committed by GitHub
parent f20f0c2aa3
commit 3b53949792
4 changed files with 45 additions and 0 deletions
@@ -48,6 +48,7 @@
| idx_execution_entity_wait_till_status_deleted_at | CREATE INDEX idx_execution_entity_wait_till_status_deleted_at ON public.execution_entity USING btree ("waitTill", status, "deletedAt") WHERE (("waitTill" IS NOT NULL) AND ("deletedAt" IS NULL)) |
| idx_execution_entity_stopped_at_status_deleted_at | CREATE INDEX idx_execution_entity_stopped_at_status_deleted_at ON public.execution_entity USING btree ("stoppedAt", status, "deletedAt") WHERE (("stoppedAt" IS NOT NULL) AND ("deletedAt" IS NULL)) |
| IDX_execution_entity_deduplicationKey | CREATE UNIQUE INDEX "IDX_execution_entity_deduplicationKey" ON public.execution_entity USING btree ("deduplicationKey") WHERE ("deduplicationKey" IS NOT NULL) |
| IDX_execution_entity_workflowId_status_id | CREATE INDEX "IDX_execution_entity_workflowId_status_id" ON public.execution_entity USING btree ("workflowId", status, id) WHERE ("deletedAt" IS NULL) |
## Relations
@@ -28,6 +28,9 @@ export type ExecutionDataStorageLocation = 'db' | 'fs' | 's3';
@Index(['finished', 'id'])
@Index(['workflowId', 'finished', 'id'])
@Index(['workflowId', 'waitTill', 'id'])
// Partial index (Postgres only) — supports paginated list queries filtered by
// workflowId + status without full sequential scans. See migration 1784000000029.
@Index(['workflowId', 'status', 'id'], { where: '"deletedAt" IS NULL' })
export class ExecutionEntity {
@Generated()
@PrimaryColumn({ transformer: idStringifier })
@@ -0,0 +1,39 @@
import type { MigrationContext, ReversibleMigration } from '../migration-types';
/**
* Adds a partial composite index on execution_entity(workflowId, status, id DESC)
* filtered to non-deleted rows (deletedAt IS NULL).
*
* Without this index Postgres performs a parallel sequential scan of the full
* execution_entity table for every paginated executions-list request, because
* the CASE-based ORDER BY and the access-control IN/EXISTS subquery prevent the
* planner from using any narrower path.
*
* With this index the planner can do per-workflowId range scans (one index seek
* per accessible workflow) and merge them into the top-N result without touching
* irrelevant rows, dropping the query from a full-table sequential scan to a
* bounded index walk.
*
* Partial (WHERE deletedAt IS NULL) keeps the index small: pruned executions are
* excluded and never appear in list queries.
*
* Postgres-only migration: partial functional indexes are a Postgres feature;
* SQLite is not affected by this performance class of issue at n8n scale.
*/
export class AddExecutionEntityWorkflowStatusIndex1784000000031 implements ReversibleMigration {
async up({ schemaBuilder: { createIndex }, escape }: MigrationContext) {
const col = (c: string) => escape.columnName(c);
await createIndex(
'execution_entity',
['workflowId', 'status', 'id'],
false,
undefined,
`${col('deletedAt')} IS NULL`,
);
}
async down({ schemaBuilder: { dropIndex } }: MigrationContext) {
await dropIndex('execution_entity', ['workflowId', 'status', 'id'], { skipIfMissing: true });
}
}
@@ -52,6 +52,7 @@ import { ChangeWorkflowStatisticsFKToNoAction1767018516000 } from './17670185160
import { ExpandVariablesValueColumnToText1777420800000 } from './1777420800000-ExpandVariablesValueColumnToText';
import { LimitWorkflowVersionTriggerToContent1784000000003 } from './1784000000003-LimitWorkflowVersionTriggerToContent';
import { AddProjectIdToInstanceAiThread1784000000028 } from './1784000000028-AddProjectIdToInstanceAiThread';
import { AddExecutionEntityWorkflowStatusIndex1784000000031 } from './1784000000031-AddExecutionEntityWorkflowStatusIndex';
import { CreateLdapEntities1674509946020 } from '../common/1674509946020-CreateLdapEntities';
import { PurgeInvalidWorkflowConnections1675940580449 } from '../common/1675940580449-PurgeInvalidWorkflowConnections';
import { RemoveResetPasswordColumns1690000000030 } from '../common/1690000000030-RemoveResetPasswordColumns';
@@ -415,4 +416,5 @@ export const postgresMigrations: Migration[] = [
AddProjectIdToInstanceAiThread1784000000028,
AddJsonSizeBytesAndWorkflowVersionIdToExecutionEntity1784000000029,
CreateAgentChatSubscriptions1784000000030,
AddExecutionEntityWorkflowStatusIndex1784000000031,
];