mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-27 16:08:58 -07:00
[rooms] improve interaction query
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
-- Index `interaction` by room, covering the two counters.
|
||||||
|
--
|
||||||
|
-- Generated from packages/domain/src/rooms-db.ts (ROOM_SCHEMA_DDL) — keep in sync.
|
||||||
|
--
|
||||||
|
-- The table's primary key is (player_id, room_id), which serves the per-player reads
|
||||||
|
-- (toggle a cheer, list my favorites) but is useless to the per-ROOM aggregate that every
|
||||||
|
-- room read runs (`getRoomStats`: `SELECT room_id, SUM(cheered), SUM(favorited) … WHERE
|
||||||
|
-- room_id IN (…) GROUP BY room_id`). Without this index that query scanned the whole
|
||||||
|
-- table on every call — 1.29B rows read over five days. With it, each room id is one
|
||||||
|
-- index range, and the two counters are in the index so the table itself is never read.
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_interaction_room ON interaction (room_id, cheered, favorited);
|
||||||
@@ -87,6 +87,11 @@ export const ROOM_SCHEMA_DDL: string[] = [
|
|||||||
last_visited_at TEXT,
|
last_visited_at TEXT,
|
||||||
PRIMARY KEY (player_id, room_id)
|
PRIMARY KEY (player_id, room_id)
|
||||||
)`,
|
)`,
|
||||||
|
// Per-ROOM access (migrations/0021_interaction_room_index.sql): the primary key
|
||||||
|
// serves the per-player reads, but the cheer/favorite aggregate every room read runs
|
||||||
|
// (`getRoomStats`) filters by room_id, which without this scanned the whole table.
|
||||||
|
// Covering — both counters are in the index, so the aggregate never touches the table.
|
||||||
|
`CREATE INDEX IF NOT EXISTS idx_interaction_room ON interaction (room_id, cheered, favorited)`,
|
||||||
// Per-room player bans (migrations/0010_room_ban.sql). One row per (room, player),
|
// Per-room player bans (migrations/0010_room_ban.sql). One row per (room, player),
|
||||||
// so re-banning someone already banned updates their row rather than appending.
|
// so re-banning someone already banned updates their row rather than appending.
|
||||||
// `ban_mask` is the client's `banMask` field kept verbatim — its meaning isn't known
|
// `ban_mask` is the client's `banMask` field kept verbatim — its meaning isn't known
|
||||||
@@ -2205,9 +2210,6 @@ interface RoomStatsRow {
|
|||||||
/** The counters a room starts life with (and the shape the client expects). */
|
/** The counters a room starts life with (and the shape the client expects). */
|
||||||
const ZERO_STATS = { CheerCount: 0, FavoriteCount: 0, VisitorCount: 0, VisitCount: 0 }
|
const ZERO_STATS = { CheerCount: 0, FavoriteCount: 0, VisitorCount: 0, VisitCount: 0 }
|
||||||
|
|
||||||
/** D1 caps a query at 100 bound parameters, and a feed page can carry more ids than that. */
|
|
||||||
const STATS_ID_LIMIT = 90
|
|
||||||
|
|
||||||
/** A room's RoomId, or 0 for a blob without one. */
|
/** A room's RoomId, or 0 for a blob without one. */
|
||||||
const roomIdOf = (room: Room): number => (typeof room.RoomId === 'number' ? room.RoomId : 0)
|
const roomIdOf = (room: Room): number => (typeof room.RoomId === 'number' ? room.RoomId : 0)
|
||||||
|
|
||||||
@@ -2232,10 +2234,14 @@ export async function recordRoomVisit(db: D1Database, roomId: number): Promise<v
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cheer/favorite counts per room, aggregated from `interaction` in ONE grouped query.
|
* Cheer/favorite counts per room, aggregated from `interaction` by a grouped query.
|
||||||
* Restricted to `roomIds` when given (a feed page), otherwise covering every room —
|
* Restricted to `roomIds` when given (a feed page), otherwise covering every room.
|
||||||
* which is also what a page too large to bind gets, since scanning the whole table is
|
* Rooms nobody has interacted with are absent.
|
||||||
* cheaper than splitting the query. Rooms nobody has interacted with are absent.
|
*
|
||||||
|
* The filtered form is an index range per id on `idx_interaction_room`, whose columns
|
||||||
|
* cover the counters — the table is never read. That's why a page with more ids than
|
||||||
|
* D1 lets us bind is split into batched queries rather than served by the unfiltered
|
||||||
|
* scan: every room read runs this, and the scan cost the whole table per call.
|
||||||
*/
|
*/
|
||||||
export async function getRoomStats(
|
export async function getRoomStats(
|
||||||
db: D1Database,
|
db: D1Database,
|
||||||
@@ -2243,18 +2249,24 @@ export async function getRoomStats(
|
|||||||
): Promise<Map<number, RoomStats>> {
|
): Promise<Map<number, RoomStats>> {
|
||||||
const byRoom = new Map<number, RoomStats>()
|
const byRoom = new Map<number, RoomStats>()
|
||||||
if (roomIds && roomIds.length === 0) return byRoom
|
if (roomIds && roomIds.length === 0) return byRoom
|
||||||
const ids = roomIds && roomIds.length <= STATS_ID_LIMIT ? roomIds : []
|
const statement = (ids: number[]): D1PreparedStatement => {
|
||||||
const where =
|
const where = ids.length > 0 ? `WHERE room_id IN (${bindPlaceholders(ids)})` : ''
|
||||||
ids.length > 0 ? `WHERE room_id IN (${ids.map((_, i) => `?${i + 1}`).join(',')})` : ''
|
return db
|
||||||
const { results } = await db
|
.prepare(
|
||||||
.prepare(
|
`SELECT room_id, SUM(cheered) AS cheers, SUM(favorited) AS favorites
|
||||||
`SELECT room_id, SUM(cheered) AS cheers, SUM(favorited) AS favorites
|
FROM interaction ${where} GROUP BY room_id`
|
||||||
FROM interaction ${where} GROUP BY room_id`
|
)
|
||||||
)
|
.bind(...ids)
|
||||||
.bind(...ids)
|
}
|
||||||
.all<RoomStatsRow>()
|
const chunks = roomIds ? chunkForBinds(roomIds) : [[]]
|
||||||
for (const r of results) {
|
const batches =
|
||||||
byRoom.set(r.room_id, { CheerCount: r.cheers ?? 0, FavoriteCount: r.favorites ?? 0 })
|
chunks.length === 1
|
||||||
|
? [await statement(chunks[0]!).all<RoomStatsRow>()]
|
||||||
|
: await db.batch<RoomStatsRow>(chunks.map(statement))
|
||||||
|
for (const { results } of batches) {
|
||||||
|
for (const r of results) {
|
||||||
|
byRoom.set(r.room_id, { CheerCount: r.cheers ?? 0, FavoriteCount: r.favorites ?? 0 })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return byRoom
|
return byRoom
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user