From 3f55b73e329dc1c1c2c71010849c672421c1aee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 8 May 2026 11:17:08 +0200 Subject: [PATCH] fix(catalog-backend): use INNER JOIN for filtered entity facets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the `WHERE search.entity_id IN (...)` form with an INNER JOIN against the filtered final_entities subquery in DefaultEntitiesCatalog#facets. Results are unchanged; the planner gets more freedom to pick cheaper plans, which on large catalogs leads to substantial speedups (1.2× to 7×+ in adversarial testing on a ~13.8M-row search table) and avoids the materialize-then-spill pattern of the IN form. Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Fredrik Adelöw --- .changeset/facets-inner-join.md | 5 +++++ .../src/service/DefaultEntitiesCatalog.ts | 12 +++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .changeset/facets-inner-join.md diff --git a/.changeset/facets-inner-join.md b/.changeset/facets-inner-join.md new file mode 100644 index 0000000000..5b4b98bda5 --- /dev/null +++ b/.changeset/facets-inner-join.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Improved the performance of the entity facets endpoint when filters are applied. The filtered entity set is now combined with the search table through an inner join rather than a `WHERE entity_id IN (subquery)`. Results are unchanged; on large catalogs the query planner is able to choose dramatically cheaper plans, with measured improvements ranging from roughly 1.2× on already-fast cases to 7× or more on high-cardinality facets. diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts index 6636096bb9..b5a65d6d05 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts @@ -707,7 +707,17 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { knex: this.database, }); - query.whereIn('search.entity_id', entityIdSubquery); + // Use INNER JOIN rather than `WHERE search.entity_id IN (...)`. The + // results are the same but the JOIN form gives the planner more + // freedom in join shape and ordering. On PostgreSQL with large + // search tables, the IN form tends to materialize the full filtered + // entity set up front and spill to temp; the JOIN form lets the + // planner pick a much cheaper plan based on actual selectivities. + query.innerJoin( + entityIdSubquery.as('filtered_entities'), + 'search.entity_id', + 'filtered_entities.entity_id', + ); } const rows = await query;