diff --git a/packages/server/src/db.migrations.test.ts b/packages/server/src/db.migrations.test.ts index daa215adcf..a5e2d09cdd 100644 --- a/packages/server/src/db.migrations.test.ts +++ b/packages/server/src/db.migrations.test.ts @@ -33,6 +33,7 @@ describe('db.migrations', () => { '20220121172409_email_recipient_default', '20240413141308_changes_optimization', '20250219183745_changes_optimization', + '20251107113000_fix_delta_performance', ]; let startProcessing = false; diff --git a/packages/server/src/migrations/20251107113000_fix_delta_performance.ts b/packages/server/src/migrations/20251107113000_fix_delta_performance.ts new file mode 100644 index 0000000000..d82770bf32 --- /dev/null +++ b/packages/server/src/migrations/20251107113000_fix_delta_performance.ts @@ -0,0 +1,27 @@ +import { DbConnection, isPostgres } from '../db'; + +// CREATE INDEX CONCURRENTLY cannot run within a transaction +export const config = { transaction: false }; + +export const up = async (db: DbConnection) => { + if (isPostgres(db)) { + // This is to optimize the sub-query in ChangeModel::changesForUserQuery() which retrieves + // the item creations and deletions. Having `item_id` first in the index helps PostgreSQL + // quickly find all rows in `changes` that belong to a specific `item_id`. We also filter it + // to `type = 2` (updates) because finding the changes for other types is done in a + // different query and is much easier. + await db.raw('CREATE INDEX CONCURRENTLY IF NOT EXISTS changes_item_id_counter_type2_index ON changes (item_id, counter) WHERE type = 2'); + + // Drop the old counter-only index. If it remains, the planner wrongly prefers it because it + // appears ideal for `ORDER BY counter`, but in reality it forces Postgres to scan millions + // of rows to find matching item_ids. + await db.raw('DROP INDEX CONCURRENTLY IF EXISTS changes_type2_counter_idx'); + } +}; + +export const down = async (db: DbConnection) => { + if (isPostgres(db)) { + await db.raw('DROP INDEX CONCURRENTLY IF EXISTS changes_item_id_counter_type2_index'); + await db.raw('CREATE INDEX CONCURRENTLY IF NOT EXISTS changes_type2_counter_idx ON changes (counter) WHERE type = 2'); + } +};