From 99aecc6b742d3bf0152ff1545bf9d0f5f761285b Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sat, 26 Sep 2026 18:45:39 -0500 Subject: [PATCH] fix(dashboard): activity log table on large MySQL databases - Read activity and map_id as signed: a row with map_id -1 made the page fail with a server error when sorted by map. - A search counts and pages through the characters it matches (index on character_id) instead of joining every row of the log to a name; when the matches are a large part of the log the page still walks the log in order. - Sorted by character name, only the row ids are sorted and the page's rows are read after. On a log of about 800k rows this takes searches with few matches from about 1 s to 0.1 s and name sorts from about 3 s to 0.5 s. Co-Authored-By: Claude Opus 5.5 --- .../GameDatabase/MySQL/Tables/ActivityLog.cpp | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/dDatabase/GameDatabase/MySQL/Tables/ActivityLog.cpp b/dDatabase/GameDatabase/MySQL/Tables/ActivityLog.cpp index a7e9bc323..b9fd75f14 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/ActivityLog.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/ActivityLog.cpp @@ -13,6 +13,9 @@ std::string MySQLDatabase::GetActivityLogTable(uint32_t start, uint32_t length, std::string baseQuery = "SELECT a.id, a.character_id, c.name as character_name, a.activity, a.time, a.map_id FROM activity_log a LEFT JOIN charinfo c ON a.character_id = c.id"; std::string whereClause; if (!search.empty()) whereClause = " WHERE (c.name LIKE CONCAT('%', ?, '%') OR a.character_id = ?)"; + // The same search as the characters it matches: few characters are read through the character_id index instead of + // joining every row of the log to a name to test it + const std::string matchJoin = " JOIN (SELECT id FROM charinfo WHERE name LIKE CONCAT('%', ?, '%') UNION SELECT ?) m ON m.id = a.character_id"; std::string orderColumnName = "a.id"; switch (orderColumn) { @@ -30,6 +33,10 @@ std::string MySQLDatabase::GetActivityLogTable(uint32_t start, uint32_t length, if (search.empty() && orderColumn != 1) { mainQuery = "SELECT a.id, a.character_id, c.name as character_name, a.activity, a.time, a.map_id FROM (SELECT * FROM activity_log a" + orderClause + " LIMIT ?, ?) a " "LEFT JOIN charinfo c ON a.character_id = c.id" + orderClause + ";"; + } else if (search.empty()) { + // Sorted by name, every row has to be joined to its name, but only the ids need sorting; the page's rows are read after + mainQuery = "SELECT a.id, a.character_id, c.name as character_name, a.activity, a.time, a.map_id FROM activity_log a JOIN (SELECT a.id FROM activity_log a " + "LEFT JOIN charinfo c ON a.character_id = c.id" + orderClause + " LIMIT ?, ?) p ON p.id = a.id LEFT JOIN charinfo c ON a.character_id = c.id" + orderClause + ";"; } auto totalCountResult = ExecuteSelect("SELECT COUNT(*) as count FROM activity_log;"); @@ -37,8 +44,14 @@ std::string MySQLDatabase::GetActivityLogTable(uint32_t start, uint32_t length, uint32_t filteredRecords = totalRecords; if (!search.empty()) { - auto filteredCountResult = ExecuteSelect("SELECT COUNT(*) as count FROM activity_log a LEFT JOIN charinfo c ON a.character_id = c.id WHERE (c.name LIKE CONCAT('%', ?, '%') OR a.character_id = ?);", search, searchId); + auto filteredCountResult = ExecuteSelect("SELECT COUNT(*) as count FROM activity_log a" + matchJoin + ";", search, searchId); filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0; + // When the matches are a small part of the log, sorting just them beats walking the log in order until a page of + // them turns up (which reads nearly all of it when there are few or none) + if (static_cast(filteredRecords) * 20 < totalRecords) { + mainQuery = "SELECT a.id, a.character_id, c.name as character_name, a.activity, a.time, a.map_id FROM activity_log a" + matchJoin + + " LEFT JOIN charinfo c ON a.character_id = c.id" + orderClause + " LIMIT ?, ?;"; + } } auto result = !search.empty() @@ -50,9 +63,9 @@ std::string MySQLDatabase::GetActivityLogTable(uint32_t start, uint32_t length, dataArray.push_back({ {"id", result->getUInt("id")}, {"character_name", result->getString("character_name")}, - {"activity", result->getUInt("activity")}, + {"activity", result->getInt("activity")}, {"time", result->getUInt64("time")}, - {"map_id", result->getUInt("map_id")} + {"map_id", result->getInt("map_id")} }); } return nlohmann::json({{"draw", 0}, {"recordsTotal", totalRecords}, {"recordsFiltered", filteredRecords}, {"data", dataArray}}).dump();