From 17d3b5c2fbb5396555c2748d0b79b8de48ba592e Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Thu, 12 Dec 2024 08:41:10 +0200 Subject: [PATCH] fix: make project ui query optimized (#8961) From 13 seconds to 0.1 seconds. 1. Joining 1 million events to projects/features is slow. **Solved by using CTE.** 2. Running grouping on 1 million rows is slow. **Solved by adding index.** --- .../project/project-read-model.test.ts | 1 + .../features/project/project-read-model.ts | 22 +++++++++++++------ ...44-events-index-project-feature-created.js | 20 +++++++++++++++++ 3 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 src/migrations/20241211143944-events-index-project-feature-created.js diff --git a/src/lib/features/project/project-read-model.test.ts b/src/lib/features/project/project-read-model.test.ts index 2f2e17cfbe..234865ccae 100644 --- a/src/lib/features/project/project-read-model.test.ts +++ b/src/lib/features/project/project-read-model.test.ts @@ -46,6 +46,7 @@ afterAll(async () => { beforeEach(async () => { await projectStore.deleteAll(); await flagStore.deleteAll(); + await eventStore.deleteAll(); }); test("it doesn't count flags multiple times when they have multiple events associated with them", async () => { diff --git a/src/lib/features/project/project-read-model.ts b/src/lib/features/project/project-read-model.ts index e4c3eaecc7..37190b8e06 100644 --- a/src/lib/features/project/project-read-model.ts +++ b/src/lib/features/project/project-read-model.ts @@ -83,7 +83,14 @@ export class ProjectReadModel implements IProjectReadModel { userId?: number, ): Promise { const projectTimer = this.timer('getProjectsForAdminUi'); - let projects = this.db(TABLE) + let projects = this.db + .with('latest_events', (qb) => { + qb.select('project', 'feature_name') + .max('created_at as last_updated') + .whereNotNull('feature_name') + .from('events') + .groupBy('project', 'feature_name'); + }) .leftJoin('features', 'features.project', 'projects.id') .leftJoin( 'last_seen_at_metrics', @@ -95,13 +102,14 @@ export class ProjectReadModel implements IProjectReadModel { 'project_settings.project', 'projects.id', ) - .leftJoin('events', (join) => { - join.on('events.feature_name', '=', 'features.name').andOn( - 'events.project', + .leftJoin('latest_events', (join) => { + join.on( + 'latest_events.feature_name', '=', - 'projects.id', - ); + 'features.name', + ).andOn('latest_events.project', '=', 'projects.id'); }) + .from(TABLE) .orderBy('projects.name', 'asc'); if (query?.archived === true) { @@ -122,7 +130,7 @@ export class ProjectReadModel implements IProjectReadModel { 'projects.id, projects.name, projects.description, projects.health, projects.created_at, ' + 'count(DISTINCT features.name) FILTER (WHERE features.archived_at is null) AS number_of_features, ' + 'MAX(last_seen_at_metrics.last_seen_at) AS last_usage, ' + - 'MAX(events.created_at) AS last_updated', + 'MAX(latest_events.last_updated) AS last_updated', ), 'project_settings.project_mode', 'projects.archived_at', diff --git a/src/migrations/20241211143944-events-index-project-feature-created.js b/src/migrations/20241211143944-events-index-project-feature-created.js new file mode 100644 index 0000000000..c04bdacc2c --- /dev/null +++ b/src/migrations/20241211143944-events-index-project-feature-created.js @@ -0,0 +1,20 @@ +'use strict'; + +exports.up = function (db, callback) { + db.runSql( + ` + CREATE INDEX idx_events_project_feature_created + ON events (project, feature_name, created_at DESC); + `, + callback, + ); +}; + +exports.down = function (db, callback) { + db.runSql( + ` + DROP INDEX idx_events_project_feature_created; + `, + callback, + ); +};