From 62c67c752b53e77a81547f08d3694ee0898f4072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 11 May 2026 18:41:01 +0200 Subject: [PATCH] catalog-backend: create covering index before dedup for vanilla installs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The index-only GROUP BY scan in Phase 1 of the dedup requires search_key_value_entity_idx (key, value, entity_id) to exist. Previously it was created after dedup, meaning fresh installs that had never manually run preparatory SQL would fall back to a full sequential scan. Move the ensurePgIndex call for search_key_value_entity_idx to before the dedup step so the fast path is guaranteed for all users, not just those who created the index manually in advance. Signed-off-by: Fredrik Adelöw Co-authored-by: Cursor --- ...20260510000000_search_indices_and_dedup.js | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/plugins/catalog-backend/migrations/20260510000000_search_indices_and_dedup.js b/plugins/catalog-backend/migrations/20260510000000_search_indices_and_dedup.js index c72c2b3298..1a35b0f080 100644 --- a/plugins/catalog-backend/migrations/20260510000000_search_indices_and_dedup.js +++ b/plugins/catalog-backend/migrations/20260510000000_search_indices_and_dedup.js @@ -179,7 +179,18 @@ exports.config = { transaction: false }; /** @param {import('knex').Knex} knex */ async function upPostgres(knex) { - // Step 1: Remove duplicate search rows. + // Step 1: Ensure the covering index exists before deduplication. + // This non-unique index on (key, value, entity_id) covers all three dedup + // columns, enabling an index-only GROUP BY scan with zero heap fetches in + // Phase 1 of the dedup. Creating it here guarantees this is true even on + // vanilla installations that have never run any preparatory SQL manually. + await ensurePgIndex(knex, { + name: 'search_key_value_entity_idx', + columns: '(key, value, entity_id)', + unique: false, + }); + + // Step 2: Remove duplicate search rows. // // Fast path: if the UNIQUE index already exists and is valid, Postgres has // been enforcing uniqueness since the index was created, so there are no @@ -258,18 +269,14 @@ async function upPostgres(knex) { await knex.raw('DROP TABLE IF EXISTS _search_dedup_groups'); } - // Step 2: Create covering indices. Each call is idempotent — it checks - // the index state and only does work if needed. + // Step 3: Create remaining covering indices. Each call is idempotent — + // it checks the index state and only does work if needed. + // search_key_value_entity_idx was already created in Step 1. await ensurePgIndex(knex, { name: 'search_entity_key_value_idx', columns: '(entity_id, key, value)', unique: true, }); - await ensurePgIndex(knex, { - name: 'search_key_value_entity_idx', - columns: '(key, value, entity_id)', - unique: false, - }); await ensurePgIndex(knex, { name: 'search_facets_covering_idx', columns: '(key, original_value, entity_id)', @@ -277,7 +284,7 @@ async function upPostgres(knex) { unique: false, }); - // Step 3: Drop superseded indices. + // Step 4: Drop superseded indices. await dropPgIndexIfExists(knex, 'search_key_value_idx'); await dropPgIndexIfExists(knex, 'search_key_original_value_idx'); }