perf(admin): cache global admin list totals via redisCache
CI / check (push) Successful in 2m37s
CI / deploy (push) Successful in 18s
CI / publish-container (push) Successful in 1m24s

Introduce getCachedAdminCount to cache un-filtered table count(*) queries in Redis for admin lists (starting with UsersPage), avoiding heavy full table scans on every request while keeping exact counts for search/filtered queries.
This commit is contained in:
openhands committed 2026-09-13 13:32:51 +02:00
1 parent ec742a3f72
commit cbf056838f
2 files changed
+31 -4

No files matched your search

+5 -4
View File
@@ -1,4 +1,4 @@
import { and, count, desc, eq, gte, like, or } from "drizzle-orm";
import { and, desc, eq, gte, like, or } from "drizzle-orm";
import { Users } from "lucide-react";
import { getTranslations } from "next-intl/server";
import { AdminPageShell } from "@/components/admin/admin-page-shell";
@@ -6,6 +6,7 @@ import { fetchAdminList } from "@/lib/admin-list";
import { db, User } from "@/lib/db";
import { formatNumber } from "@/lib/format-date";
import { canAccess, PERMS } from "@/lib/permissions";
import { getCachedAdminCount } from "@/lib/services/admin-stats";
import { CreateUserDialog } from "./_components/create-user-dialog";
import { UsersTable } from "./users-table";
@@ -43,7 +44,7 @@ export default async function UsersPage({
conditions.push(eq(User.online, rawParams.filter_online));
}
const where = conditions.length > 0 ? and(...conditions) : undefined;
const [rows, totals] = await Promise.all([
const [rows, total] = await Promise.all([
db
.select({
id: User.id,
@@ -64,9 +65,9 @@ export default async function UsersPage({
.orderBy(desc(User.id))
.limit(perPage)
.offset((page - 1) * perPage),
db.select({ total: count() }).from(User).where(where),
getCachedAdminCount(User, "users:total", where),
]);
return { rows, total: totals[0]?.total ?? 0 };
return { rows, total };
},
},
paramsPromise,
+26
View File
@@ -0,0 +1,26 @@
import { count, type SQL } from "drizzle-orm";
import type { MySqlTable } from "drizzle-orm/mysql-core";
import { db } from "@/lib/db";
import { redisCache } from "@/lib/redis-cache";
/**
* Caches admin list totals when no filters are applied (no 'where' clause),
* preventing heavy COUNT(*) table scans on every request for large tables.
* Filtered queries bypass cache to ensure exact search result counts.
*/
export async function getCachedAdminCount(
table: MySqlTable,
cacheKey: string,
where?: SQL | undefined,
): Promise<number> {
// Only cache the global count (where = undefined)
if (where) {
const [row] = await db.select({ total: count() }).from(table).where(where);
return Number(row?.total ?? 0);
}
return await redisCache(`admin:count:${cacheKey}`, 60, async () => {
const [row] = await db.select({ total: count() }).from(table);
return Number(row?.total ?? 0);
});
}