perf(admin): cache global admin list totals via redisCache
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:
1 parent
ec742a3f72
commit
cbf056838f
2 files changed
+31
-4
No files matched your search
@@ -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 { Users } from "lucide-react";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
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 { db, User } from "@/lib/db";
|
||||||
import { formatNumber } from "@/lib/format-date";
|
import { formatNumber } from "@/lib/format-date";
|
||||||
import { canAccess, PERMS } from "@/lib/permissions";
|
import { canAccess, PERMS } from "@/lib/permissions";
|
||||||
|
import { getCachedAdminCount } from "@/lib/services/admin-stats";
|
||||||
import { CreateUserDialog } from "./_components/create-user-dialog";
|
import { CreateUserDialog } from "./_components/create-user-dialog";
|
||||||
import { UsersTable } from "./users-table";
|
import { UsersTable } from "./users-table";
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ export default async function UsersPage({
|
|||||||
conditions.push(eq(User.online, rawParams.filter_online));
|
conditions.push(eq(User.online, rawParams.filter_online));
|
||||||
}
|
}
|
||||||
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
||||||
const [rows, totals] = await Promise.all([
|
const [rows, total] = await Promise.all([
|
||||||
db
|
db
|
||||||
.select({
|
.select({
|
||||||
id: User.id,
|
id: User.id,
|
||||||
@@ -64,9 +65,9 @@ export default async function UsersPage({
|
|||||||
.orderBy(desc(User.id))
|
.orderBy(desc(User.id))
|
||||||
.limit(perPage)
|
.limit(perPage)
|
||||||
.offset((page - 1) * 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,
|
paramsPromise,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in new issue
Block a user