From 20c4951847c7a51f675bca7516f0f4b375ee0b83 Mon Sep 17 00:00:00 2001 From: DarkSky Date: Wed, 31 Dec 2025 22:59:52 +0800 Subject: [PATCH] feat: improve workspace list perf --- .../migration.sql | 36 +++++++++++++++++++ packages/backend/server/schema.prisma | 2 ++ packages/backend/server/src/models/doc.ts | 7 ++-- .../backend/server/src/models/workspace.ts | 2 +- 4 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 packages/backend/server/migrations/20251231145656_snapshot_size/migration.sql diff --git a/packages/backend/server/migrations/20251231145656_snapshot_size/migration.sql b/packages/backend/server/migrations/20251231145656_snapshot_size/migration.sql new file mode 100644 index 0000000000..f67272064e --- /dev/null +++ b/packages/backend/server/migrations/20251231145656_snapshot_size/migration.sql @@ -0,0 +1,36 @@ +-- Add persisted size column for snapshots (nullable for backwards compatibility) +ALTER TABLE "snapshots" ADD COLUMN IF NOT EXISTS "size" BIGINT; + +-- Ensure size is populated on insert/update even for older app versions +CREATE OR REPLACE FUNCTION snapshots_set_size() RETURNS TRIGGER AS $$ +BEGIN + NEW."size" := COALESCE(NEW."size", octet_length(NEW."blob")); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS snapshots_set_size_before_write ON "snapshots"; +CREATE TRIGGER snapshots_set_size_before_write +BEFORE INSERT OR UPDATE OF "blob" ON "snapshots" +FOR EACH ROW +EXECUTE FUNCTION snapshots_set_size(); + +-- Backfill existing rows +UPDATE "snapshots" +SET "size" = octet_length("blob") +WHERE "size" IS NULL; + +-- Support faster admin aggregates +CREATE INDEX IF NOT EXISTS "idx_wur_owner" ON "workspace_user_permissions" ("workspace_id") +WHERE "type" = 99 AND "status" = 'Accepted'::"WorkspaceMemberStatus"; + +CREATE INDEX IF NOT EXISTS "idx_wur_workspace" ON "workspace_user_permissions" ("workspace_id"); + +CREATE INDEX IF NOT EXISTS "idx_blobs_active" ON "blobs" ("workspace_id") +WHERE "deleted_at" IS NULL AND "status" = 'completed'; + +CREATE INDEX IF NOT EXISTS "idx_workspace_pages_public" ON "workspace_pages" ("workspace_id") +WHERE "public" = TRUE; + +CREATE INDEX IF NOT EXISTS "idx_workspace_features_activated" ON "workspace_features" ("workspace_id") +WHERE "activated" = TRUE; diff --git a/packages/backend/server/schema.prisma b/packages/backend/server/schema.prisma index 0a4c55cc0e..4b0c4e8e01 100644 --- a/packages/backend/server/schema.prisma +++ b/packages/backend/server/schema.prisma @@ -273,6 +273,8 @@ model Snapshot { workspaceId String @map("workspace_id") @db.VarChar id String @default(uuid()) @map("guid") @db.VarChar blob Bytes @db.ByteA + // persisted size of blob to avoid summing over bytea + size BigInt? @map("size") @db.BigInt state Bytes? @db.ByteA createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) // the `updated_at` field will not record the time of record changed, diff --git a/packages/backend/server/src/models/doc.ts b/packages/backend/server/src/models/doc.ts index e910133a54..ec77e07f71 100644 --- a/packages/backend/server/src/models/doc.ts +++ b/packages/backend/server/src/models/doc.ts @@ -149,6 +149,7 @@ export class DocModel extends BaseModel { async upsert(doc: Doc) { const { spaceId, docId, blob, timestamp, editorId } = doc; const updatedAt = new Date(timestamp); + const size = blob.byteLength ?? blob.length; // CONCERNS: // i. Because we save the real user's last seen action time as `updatedAt`, // it's possible to simply compare the `updatedAt` to determine if the snapshot is older than the one we are going to save. @@ -158,10 +159,10 @@ export class DocModel extends BaseModel { // where: { workspaceId_id: {}, updatedAt: { lt: updatedAt } } // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ const result: { updatedAt: Date }[] = await this.db.$queryRaw` - INSERT INTO "snapshots" ("workspace_id", "guid", "blob", "created_at", "updated_at", "created_by", "updated_by") - VALUES (${spaceId}, ${docId}, ${blob}, DEFAULT, ${updatedAt}, ${editorId}, ${editorId}) + INSERT INTO "snapshots" ("workspace_id", "guid", "blob", "size", "created_at", "updated_at", "created_by", "updated_by") + VALUES (${spaceId}, ${docId}, ${blob}, ${size}, DEFAULT, ${updatedAt}, ${editorId}, ${editorId}) ON CONFLICT ("workspace_id", "guid") - DO UPDATE SET "blob" = ${blob}, "updated_at" = ${updatedAt}, "updated_by" = ${editorId} + DO UPDATE SET "blob" = ${blob}, "size" = ${size}, "updated_at" = ${updatedAt}, "updated_by" = ${editorId} WHERE "snapshots"."workspace_id" = ${spaceId} AND "snapshots"."guid" = ${docId} AND "snapshots"."updated_at" <= ${updatedAt} RETURNING "snapshots"."workspace_id" as "workspaceId", "snapshots"."guid" as "id", "snapshots"."updated_at" as "updatedAt" `; diff --git a/packages/backend/server/src/models/workspace.ts b/packages/backend/server/src/models/workspace.ts index 6bf7528f20..639c976b5c 100644 --- a/packages/backend/server/src/models/workspace.ts +++ b/packages/backend/server/src/models/workspace.ts @@ -211,7 +211,7 @@ export class WorkspaceModel extends BaseModel { ), snapshot_stats AS ( SELECT workspace_id, - SUM(octet_length(blob)) AS snapshot_size, + SUM(COALESCE(size, octet_length(blob))) AS snapshot_size, COUNT(*) AS snapshot_count FROM snapshots GROUP BY workspace_id