# Ranking And Stats Performance Audit ## Scope Audit date: - 2026-06-09 Target endpoints: - `/api/ranking?timeframe=weekly&server_id=all&metric=kills&limit=20` - `/api/ranking?timeframe=weekly&server_id=all&metric=kd_ratio&limit=20` - `/api/ranking?timeframe=monthly&server_id=all&metric=kills_per_match&limit=20` - `/api/ranking?timeframe=annual&year=2026&server_id=all&metric=kills&limit=20` - `/api/stats/players/search?q=Chi&limit=10` - `/api/stats/players/3530e19cf8a9dd6a9fada4599592fbf8?timeframe=weekly` ## Environment And Method - Backend HTTP server was not running on `http://127.0.0.1:8000` during the audit. - Request timing was measured in-process through `app.routes.resolve_get_payload(...)`. - SQL timing was measured by instrumenting the SQLite read connections used by: - `backend/app/rcon_historical_leaderboards.py` - `backend/app/rcon_historical_player_stats.py` - `backend/app/rcon_annual_rankings.py` - `EXPLAIN QUERY PLAN` was obtained successfully from SQLite. - Storage inspected: - `backend/data/hll_vietnam_dev.sqlite3` Important coverage note: - The latest materialized match rows for `source_basis="admin-log-match-ended"` end on `2026-05-20T23:21:45.816Z`. - The weekly/monthly public requests on `2026-06-09` therefore read windows with `0` qualifying matches. - Measured latency is real for the current dataset, but it understates future production cost because the active public windows are empty. ## Measured Baseline | Endpoint | Avg request ms | Avg SQL ms | SQL queries | Result | | --- | ---: | ---: | ---: | --- | | `/api/ranking` weekly `kills` | 4.045 | 0.882 | 6 | `snapshot_status=ready`, `items=0` | | `/api/ranking` weekly `kd_ratio` | 3.801 | 0.837 | 6 | `snapshot_status=ready`, `items=0` | | `/api/ranking` monthly `kills_per_match` | 3.612 | 0.816 | 6 | `snapshot_status=ready`, `items=0` | | `/api/ranking` annual `kills` | 3.326 | 0.629 | 3 | `snapshot_status=ready`, `items=2` | | `/api/stats/players/search?q=Chi&limit=10` | 6.731 | 3.593 | 3 | `items=10` | | `/api/stats/players/{player_id}` weekly | 8.924 | 4.494 | 16 | weekly profile payload | Slowest endpoint: - `/api/stats/players/{player_id}` weekly - Main reason: 16 SQL statements per request, including repeated window-count queries and two ranking subqueries. ## Relevant Table Size And Coverage Row counts: - `rcon_materialized_matches`: `58` - `rcon_match_player_stats`: `3,824` - `rcon_annual_ranking_snapshots`: `2` - `rcon_annual_ranking_snapshot_items`: `2` Rows actually used by runtime ranking/stats: - `rcon_materialized_matches` with `source_basis="admin-log-match-ended"`: `22` Observed covered range for runtime ranking/stats: - first materialized match end/start: `2026-05-19T11:16:10.281Z` - latest materialized match end/start: `2026-05-20T23:21:45.816Z` Current public window coverage: - weekly window `2026-06-01T00:00:00Z` to `2026-06-08T00:00:00Z`: `0` matches - monthly window `2026-06-01T00:00:00Z` to `2026-06-09T23:59:59Z`: `0` matches Annual snapshot presence: - `2026 / all-servers / kills / ready / limit_size=2 / source_matches_count=22` ## Existing Indexes `rcon_materialized_matches` - unique `(target_key, match_key)` - non-unique `(target_key, ended_at, ended_server_time)` `rcon_match_player_stats` - unique `(target_key, match_key, player_id)` - non-unique `(target_key, match_key)` `rcon_annual_ranking_snapshots` - unique `(year, server_key, metric)` - non-unique `(year, server_key, metric)` - non-unique `(status)` `rcon_annual_ranking_snapshot_items` - unique `(snapshot_id, ranking_position)` - unique `(snapshot_id, player_id)` - non-unique `(snapshot_id, ranking_position)` - non-unique `(snapshot_id, player_id)` ## Current Query Shapes Weekly/monthly ranking runtime path: - 4x `COUNT(*)` over `rcon_materialized_matches` to choose weekly/monthly windows - 1x aggregate join from `rcon_match_player_stats` to `rcon_materialized_matches` - 1x source-range query over `rcon_materialized_matches` Annual ranking path: - 1x snapshot lookup in `rcon_annual_ranking_snapshots` - 1x item count in `rcon_annual_ranking_snapshot_items` - 1x ordered item read in `rcon_annual_ranking_snapshot_items` Stats player search path: - 1x grouped search query joining `rcon_match_player_stats` to `rcon_materialized_matches` - 1x latest-name lookup for returned `player_id` set - 1x `servers_seen` lookup for returned `player_id` set Stats player detail path: - 12x `COUNT(*)` window-selection queries over `rcon_materialized_matches` - 1x player aggregate query - 1x source-range query - 2x ranking-position subqueries using grouped leaderboard logic ## Execution Plan Findings SQLite `EXPLAIN QUERY PLAN` showed: Ranking count queries: - `SCAN matches` Weekly/monthly leaderboard aggregate queries: - `SCAN matches` - `SEARCH stats USING INDEX idx_rcon_match_player_stats_match (target_key=? AND match_key=?)` - `USE TEMP B-TREE FOR GROUP BY` - `USE TEMP B-TREE FOR count(DISTINCT)` - `USE TEMP B-TREE FOR ORDER BY` Stats search primary query: - `SCAN matches` - `SEARCH stats USING INDEX idx_rcon_match_player_stats_match (target_key=? AND match_key=?)` - `USE TEMP B-TREE FOR GROUP BY` - `USE TEMP B-TREE FOR count(DISTINCT)` - `USE TEMP B-TREE FOR ORDER BY` Stats player detail aggregate query: - `SCAN stats` - `SEARCH matches USING INDEX sqlite_autoindex_rcon_materialized_matches_1 (target_key=? AND match_key=?)` - `USE TEMP B-TREE FOR count(DISTINCT)` Stats player detail ranking subquery: - `SCAN stats USING INDEX sqlite_autoindex_rcon_match_player_stats_1` - `SEARCH matches USING INDEX sqlite_autoindex_rcon_materialized_matches_1 (target_key=? AND match_key=?)` - `USE TEMP B-TREE FOR GROUP BY` - `USE TEMP B-TREE FOR count(DISTINCT)` - `USE TEMP B-TREE FOR ORDER BY` Annual snapshot lookup: - `SEARCH rcon_annual_ranking_snapshots USING INDEX sqlite_autoindex_rcon_annual_ranking_snapshots_1 (year=? AND server_key=? AND metric=?)` ## Root Cause Assessment Current measured latency is low because the runtime weekly/monthly windows are empty on `2026-06-09`, not because the runtime read path is already efficient at scale. The most likely root causes for future slowdown are: - no index that matches `source_basis + time window` on `rcon_materialized_matches` - repeated public-request window counting against `rcon_materialized_matches` - grouped leaderboard/search queries that spill to temp B-trees for `GROUP BY`, `COUNT(DISTINCT)` and `ORDER BY` - no direct `player_id` read index for the stats player-detail aggregate path The main non-performance operational issue exposed by the audit: - runtime weekly/monthly ranking currently has stale coverage for the current public window because the latest materialized match data ends on `2026-05-20`, while the requests were evaluated on `2026-06-09` ## Recommendation For TASK-189 Priority indexes justified by the current plans: - add a time-window read index on `rcon_materialized_matches` keyed by `source_basis` plus the runtime time field used by public reads - add a scoped time-window index on `rcon_materialized_matches` that also includes `target_key` - add a direct `player_id` index on `rcon_match_player_stats` Concrete candidate coverage to evaluate in implementation: - `rcon_materialized_matches(source_basis, ended_at)` - `rcon_materialized_matches(source_basis, started_at)` - `rcon_materialized_matches(target_key, source_basis, ended_at)` - `rcon_materialized_matches(target_key, source_basis, started_at)` - `rcon_match_player_stats(player_id)` Notes: - `(target_key, match_key)` is already covered and should be preserved. - annual snapshot item lookup is already covered by `(snapshot_id, ranking_position)` and `(snapshot_id, player_id)`. - a simple `player_name` B-tree is not likely to materially help the current search query because the query uses `LOWER(...) LIKE '%term%'` with a leading wildcard. If search becomes a real bottleneck later, a normalized-prefix strategy or FTS is more promising than a plain index. ## TASK-189 Applied Indexes Applied in storage initialization for both SQLite and PostgreSQL: - `idx_rcon_materialized_matches_source_window_text` - `idx_rcon_materialized_matches_target_source_window_text` - `idx_rcon_materialized_matches_external_source_window_text` - `idx_rcon_match_player_stats_player_id_match` Not added: - plain `player_name` index Reason: - the current search query uses `LOWER(player_name) LIKE '%term%'`, so a simple B-tree on `player_name` is not expected to materially help the current search shape ## TASK-189 Post-Index Check Observed plan change: - weekly count query now uses `SEARCH matches USING COVERING INDEX idx_rcon_materialized_matches_source_window_text (source_basis=? AND >? AND