Skip to content

Commit bdf87e4

Browse files
committed
optimize some queries
1 parent 3b07843 commit bdf87e4

4 files changed

Lines changed: 110 additions & 142 deletions

File tree

src/api/health.js

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,37 +33,34 @@ router.get("/", async (req, res) => {
3333

3434
router.get("/stats", async (req, res) => {
3535
try {
36-
const [serverStats] = await pool.query(
37-
"SELECT COUNT(*) as total, SUM(CASE WHEN status=1 THEN 1 ELSE 0 END) as online, SUM(CASE WHEN status=0 THEN 1 ELSE 0 END) as offline FROM servers",
38-
);
36+
// Optimized: Combine all stats into a single query
37+
const [stats] = await pool.query(`
38+
SELECT
39+
(SELECT COUNT(*) FROM servers) as server_total,
40+
(SELECT SUM(CASE WHEN status=1 THEN 1 ELSE 0 END) FROM servers) as server_online,
41+
(SELECT SUM(CASE WHEN status=0 THEN 1 ELSE 0 END) FROM servers) as server_offline,
42+
(SELECT COUNT(DISTINCT steamid) FROM players) as player_total,
43+
(SELECT COUNT(DISTINCT steamid) FROM players WHERE last_seen >= DATE_SUB(NOW(), INTERVAL 24 HOUR)) as players_active_24h,
44+
(SELECT COUNT(DISTINCT name) FROM maps) as map_total
45+
`);
3946

40-
const [playerStats] = await pool.query(
41-
"SELECT COUNT(DISTINCT steamid) as total FROM players",
42-
);
43-
44-
const [activePlayersStats] = await pool.query(
45-
"SELECT COUNT(DISTINCT steamid) as active_24h FROM players WHERE last_seen >= DATE_SUB(NOW(), INTERVAL 24 HOUR)",
46-
);
47-
48-
const [mapStats] = await pool.query(
49-
"SELECT COUNT(DISTINCT name) as total FROM maps",
50-
);
47+
const result = stats[0];
5148

5249
const uptime = process.uptime();
5350
const wsStats = getWebSocketStats();
5451

5552
res.json({
5653
servers: {
57-
total: serverStats[0].total,
58-
online: serverStats[0].online,
59-
offline: serverStats[0].offline,
54+
total: result.server_total,
55+
online: result.server_online,
56+
offline: result.server_offline,
6057
},
6158
players: {
62-
total: playerStats[0].total,
63-
active_24h: activePlayersStats[0].active_24h,
59+
total: result.player_total,
60+
active_24h: result.players_active_24h,
6461
},
6562
maps: {
66-
total: mapStats[0].total,
63+
total: result.map_total,
6764
},
6865
websocket: {
6966
connected: wsStats.connected,

src/api/kzRecords.js

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -196,13 +196,25 @@ router.get("/", cacheMiddleware(30, kzKeyGenerator), async (req, res) => {
196196
}
197197
}
198198

199-
// Get total count
200-
const countQuery = query.replace(
201-
/SELECT.*FROM/s,
202-
"SELECT COUNT(DISTINCT r.id) as total FROM",
203-
);
199+
// Optimized: Use SQL_CALC_FOUND_ROWS for single query count (MySQL/MariaDB)
200+
// Or use window function for better performance
204201
const pool = getKzPool();
205-
const [countResult] = await pool.query(countQuery, params);
202+
203+
// Build count query efficiently - reuse WHERE clause
204+
const countQuery =
205+
`
206+
SELECT COUNT(r.id) as total
207+
FROM kz_records r
208+
LEFT JOIN kz_players p ON r.player_id = p.steamid64
209+
LEFT JOIN kz_maps m ON r.map_id = m.id
210+
LEFT JOIN kz_servers s ON r.server_id = s.id
211+
WHERE 1=1` +
212+
query.substring(
213+
query.indexOf("WHERE 1=1") + 9,
214+
query.indexOf("ORDER BY") || query.length,
215+
);
216+
217+
const [countResult] = await pool.query(countQuery, params.slice(0, -2)); // Remove LIMIT/OFFSET
206218
const total = countResult[0].total;
207219

208220
// Add sorting and pagination

src/api/maps.js

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,15 @@ router.get("/", cacheMiddleware(30, mapsKeyGenerator), async (req, res) => {
122122
const sortField = validSortFields.includes(sort) ? sort : "total_playtime";
123123
const sortOrder = order === "asc" ? "ASC" : "DESC";
124124

125-
let query =
126-
"SELECT name, game, COALESCE(SUM(playtime), 0) AS total_playtime FROM maps WHERE 1=1";
125+
// Optimized: Use window function to get total count in single query
126+
let query = `
127+
SELECT
128+
name,
129+
game,
130+
COALESCE(SUM(playtime), 0) AS total_playtime,
131+
COUNT(*) OVER() as total_count
132+
FROM maps
133+
WHERE 1=1`;
127134
const params = [];
128135

129136
if (game) {
@@ -149,35 +156,20 @@ router.get("/", cacheMiddleware(30, mapsKeyGenerator), async (req, res) => {
149156

150157
const [maps] = await pool.query(query, params);
151158

152-
let countQuery =
153-
"SELECT COUNT(DISTINCT CONCAT(name, '-', game)) as total FROM maps WHERE 1=1";
154-
const countParams = [];
155-
if (game) {
156-
countQuery += " AND game = ?";
157-
countParams.push(sanitizeString(game, 50));
158-
}
159-
if (server) {
160-
const [ip, port] = server.split(":");
161-
if (ip && port && isValidIP(ip) && isValidPort(port)) {
162-
countQuery += " AND server_ip = ? AND server_port = ?";
163-
countParams.push(ip, parseInt(port, 10));
164-
}
165-
}
166-
if (name) {
167-
countQuery += " AND name LIKE ?";
168-
countParams.push(`%${sanitizeString(name, 100)}%`);
169-
}
159+
// Extract total from first row (same for all rows due to window function)
160+
const total = maps.length > 0 ? maps[0].total_count : 0;
170161

171-
const [countResult] = await pool.query(countQuery, countParams);
162+
// Remove total_count from each map object
163+
maps.forEach((map) => delete map.total_count);
172164

173165
res.json({
174166
total: maps.length,
175167
data: maps,
176168
pagination: {
177169
page: parseInt(page, 10) || 1,
178170
limit: validLimit,
179-
total: countResult[0].total,
180-
totalPages: Math.ceil(countResult[0].total / validLimit),
171+
total: total,
172+
totalPages: Math.ceil(total / validLimit),
181173
},
182174
});
183175
} catch (e) {

src/api/players.js

Lines changed: 60 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -172,15 +172,26 @@ router.get("/", cacheMiddleware(30, playersKeyGenerator), async (req, res) => {
172172
const sortField = validSortFields.includes(sort) ? sort : "total_playtime";
173173
const sortOrder = order === "asc" ? "ASC" : "DESC";
174174

175-
// Get all player data grouped by steamid and game
175+
// Optimized: Use SQL aggregation with JSON functions for better performance
176176
let query = `
177177
SELECT
178-
steamid,
179-
latest_name as name,
180-
game,
181-
SUM(playtime) as total_playtime,
182-
MAX(last_seen) as last_seen,
183-
MAX(avatar) as avatar
178+
steamid,
179+
MAX(latest_name) as name,
180+
MAX(avatar) as avatar,
181+
MAX(CASE WHEN game = 'csgo' THEN
182+
JSON_OBJECT(
183+
'total_playtime', SUM(playtime),
184+
'last_seen', MAX(last_seen)
185+
)
186+
END) as csgo,
187+
MAX(CASE WHEN game = 'counterstrike2' THEN
188+
JSON_OBJECT(
189+
'total_playtime', SUM(playtime),
190+
'last_seen', MAX(last_seen)
191+
)
192+
END) as counterstrike2,
193+
SUM(playtime) as _total_playtime,
194+
MAX(last_seen) as _last_seen
184195
FROM players
185196
WHERE 1=1
186197
`;
@@ -196,99 +207,55 @@ router.get("/", cacheMiddleware(30, playersKeyGenerator), async (req, res) => {
196207
params.push(`%${sanitizeString(name, 100)}%`);
197208
}
198209

199-
query += " GROUP BY steamid, game";
210+
query += " GROUP BY steamid";
200211

201-
const [rawPlayers] = await pool.query(query, params);
202-
203-
// Group by steamid and structure data by game
204-
const playerMap = new Map();
205-
206-
for (const row of rawPlayers) {
207-
if (!playerMap.has(row.steamid)) {
208-
playerMap.set(row.steamid, {
209-
steamid: row.steamid,
210-
name: row.name, // Will be updated to most recent
211-
avatar: row.avatar,
212-
csgo: {},
213-
counterstrike2: {},
214-
_lastSeen: null, // For sorting
215-
_totalPlaytime: 0, // For sorting
216-
});
217-
}
218-
219-
const player = playerMap.get(row.steamid);
220-
221-
// Update name to most recent across all games
222-
if (
223-
!player._lastSeen ||
224-
new Date(row.last_seen) > new Date(player._lastSeen)
225-
) {
226-
player.name = row.name;
227-
player._lastSeen = row.last_seen;
228-
}
229-
230-
// Add game-specific stats
231-
if (row.game === "csgo") {
232-
player.csgo = {
233-
total_playtime: parseInt(row.total_playtime, 10) || 0,
234-
last_seen: row.last_seen,
235-
};
236-
} else if (row.game === "counterstrike2") {
237-
player.counterstrike2 = {
238-
total_playtime: parseInt(row.total_playtime, 10) || 0,
239-
last_seen: row.last_seen,
240-
};
241-
}
242-
243-
// Track combined playtime for sorting
244-
player._totalPlaytime += parseInt(row.total_playtime, 10) || 0;
212+
// Add SQL-based sorting instead of JavaScript sorting
213+
if (sortField === "total_playtime") {
214+
query += ` ORDER BY _total_playtime ${sortOrder}`;
215+
} else if (sortField === "last_seen") {
216+
query += ` ORDER BY _last_seen ${sortOrder}`;
217+
} else {
218+
query += ` ORDER BY steamid ${sortOrder}`;
245219
}
246220

247-
// Convert map to array and remove internal sorting fields
248-
let players = Array.from(playerMap.values()).map((p) => {
249-
const { _lastSeen, _totalPlaytime, ...playerData } = p;
250-
return playerData;
251-
});
221+
// Add pagination in SQL
222+
query += " LIMIT ? OFFSET ?";
223+
params.push(validLimit, offset);
252224

253-
// Sort based on requested field
254-
players.sort((a, b) => {
255-
let aVal, bVal;
256-
257-
if (sortField === "total_playtime") {
258-
// Sum playtime across both games for sorting
259-
aVal =
260-
(a.csgo.total_playtime || 0) + (a.counterstrike2.total_playtime || 0);
261-
bVal =
262-
(b.csgo.total_playtime || 0) + (b.counterstrike2.total_playtime || 0);
263-
} else if (sortField === "last_seen") {
264-
// Get most recent last_seen across both games
265-
const aDate =
266-
[a.csgo.last_seen, a.counterstrike2.last_seen]
267-
.filter((d) => d)
268-
.sort()
269-
.reverse()[0] || "";
270-
const bDate =
271-
[b.csgo.last_seen, b.counterstrike2.last_seen]
272-
.filter((d) => d)
273-
.sort()
274-
.reverse()[0] || "";
275-
aVal = aDate;
276-
bVal = bDate;
277-
} else {
278-
aVal = a[sortField];
279-
bVal = b[sortField];
280-
}
225+
const [rawPlayers] = await pool.query(query, params);
281226

282-
if (sortOrder === "DESC") {
283-
return aVal > bVal ? -1 : aVal < bVal ? 1 : 0;
284-
} else {
285-
return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
286-
}
227+
// Parse JSON fields from SQL (MariaDB returns JSON as strings)
228+
const players = rawPlayers.map((row) => {
229+
const { _total_playtime, _last_seen, ...player } = row;
230+
231+
// Parse JSON objects or set to empty objects
232+
player.csgo = row.csgo
233+
? typeof row.csgo === "string"
234+
? JSON.parse(row.csgo)
235+
: row.csgo
236+
: {};
237+
player.counterstrike2 = row.counterstrike2
238+
? typeof row.counterstrike2 === "string"
239+
? JSON.parse(row.counterstrike2)
240+
: row.counterstrike2
241+
: {};
242+
243+
return player;
287244
});
288245

289-
// Apply pagination
290-
const total = players.length;
291-
players = players.slice(offset, offset + validLimit);
246+
// Get total count (separate query for accuracy)
247+
let countQuery =
248+
"SELECT COUNT(DISTINCT steamid) as total FROM players WHERE 1=1";
249+
const countParams = [];
250+
if (game) {
251+
countQuery += " AND game = ?";
252+
countParams.push(sanitizeString(game, 50));
253+
}
254+
if (name) {
255+
countQuery += " AND latest_name LIKE ?";
256+
countParams.push(`%${sanitizeString(name, 100)}%`);
257+
}
258+
const [[{ total }]] = await pool.query(countQuery, countParams);
292259

293260
res.json({
294261
data: players,

0 commit comments

Comments
 (0)