From cf195de72cd00b876f7b03adb8ba126e0d2091be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 20 Apr 2026 14:02:02 +0200 Subject: [PATCH] catalog-backend: fix facets endpoint performance regression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Route the EXISTS-based filters through final_entities (one row per entity) instead of correlating against the search table directly. This avoids the pathological case where correlated subqueries scan the much larger search table for every row in the outer facets query. Signed-off-by: Fredrik Adelöw Made-with: Cursor --- .changeset/fix-facets-perf-regression.md | 5 +++++ .../src/service/DefaultEntitiesCatalog.ts | 15 +++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-facets-perf-regression.md diff --git a/.changeset/fix-facets-perf-regression.md b/.changeset/fix-facets-perf-regression.md new file mode 100644 index 0000000000..71f293271a --- /dev/null +++ b/.changeset/fix-facets-perf-regression.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fixed a performance regression in the `/entity-facets` endpoint when filters or permission conditions are applied, by routing the EXISTS-based filter through `final_entities` instead of correlating against the much larger `search` table. diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts index 7fda0ac0f1..830120e486 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts @@ -690,13 +690,24 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { .groupBy(['search.key', 'search.original_value']); if (request.filter || request.query) { + // Build a subquery that finds matching entity IDs via + // final_entities, so that the EXISTS-based filters correlate + // against one-row-per-entity rather than the much larger search + // table. This keeps the facets aggregation fast even with many + // filter clauses or permission conditions. + const entityIdSubquery = this.database('final_entities') + .select('final_entities.entity_id') + .whereNotNull('final_entities.final_entity'); + applyEntityFilterToQuery({ filter: request.filter, query: request.query, - targetQuery: query, - onEntityIdField: 'search.entity_id', + targetQuery: entityIdSubquery, + onEntityIdField: 'final_entities.entity_id', knex: this.database, }); + + query.whereIn('search.entity_id', entityIdSubquery); } const rows = await query;