OrderedDataStore and it's suitability for a specific requirement

I’d like to have a place to store userID, stage (string), difficulty (string) and time (NumberValue) for the purposes of creating multiple billboard leaderboards that would display top 100 data in ascending order.

I do not have much exposure with datastores but from my understanding the GetSortedAsync() method can only return between 1 and 100 keys, and I need to be able to return 100 keys per each difficulty and stage (there are 3 difficulties and 6 stages, so 1800 keys)

I want to limit the amount of data fetching I do on the server and store it in local tables, however i’m uncertain how often this data should be refreshed in order to display accurate data without sacrificing much performance (ie task.delay(60) and then refresh data.)

I don’t know if I need to create 18 individual OrderedDataStores To achieve this or if all this data can be put into a single OrderedDataStore that can then be filtered by some string matching method (and probably re-order as the data will have mixed data)

I’m looking for a general approach to this, and if OrderedDataStore is even a viable service to house this data. Snippet of code here to illustrate a function residing in a module script that could be generating and retrieving data, although I haven’t delved much into the coding aspect of this as i’m uncertain on the approach:

local function sortAndReturnSpeedRunPersonalBestForTop100PlayersForGivenDifficulty(player, speedrunDifficulty)	-- use a structured key system: <UserId>_<Stage>_<Difficulty> for saving (this function is not for saving)	local speedRunPersonalBestForTop100PlayersForGivenDifficulty = {}	local pages = speedRunDataStore:GetSortedAsync(true, 100) -- Ascending order	for _, entry in pages:GetCurrentPage() do	local key = entry.key -- name of the key, will be <UserId>_<Stage>_<Difficulty>	local timeInMilliseconds = entry.value -- key's value, will store time in milliseconds as datastores cannot save decimals, so conversion is needed	local timeInSeconds = timeInMilliseconds / 1000 -- Convert milliseconds to seconds	local userId = tonumber(string.match(key, "^%d+")) -- Extract the UserId from the key	local difficulty = string.match(key, "%w+$") -- Extract the difficulty from the key	if difficulty == speedrunDifficulty then	table.insert(speedRunPersonalBestForTop100PlayersForGivenDifficulty, {userId, stage, timeInSeconds})	end	end	return speedRunPersonalBestForTop100PlayersForGivenDifficulty end 
1 Like

You’ll have to create unique ones per category you track. Any trick to combine them will make it hard to reliably get the information back out if the times vary significantly. As for updates, honestly just update them rarely. You can probably leave a leaderboard out of date for like 15 minutes no problem. If a player breaks a record, just update the local one immediately and store with deferred updates. Then just update the rest with respect to data store budgets. I often will update one board a minute myself but I usually only have a couple and want the budget for a few other things.

2 Likes

It is possible to use normal datastores for leaderboards, in a pretty efficient way. How to do it is, something like this

Datastore "Leaderboard_Stage1_Hard" : { { time = 10, userId = -1, }, { time = 12.3, userId = -2, } } Datastore "Leaderboard_Stage1_Easy" : { { time = 10, userId = -1, }, { time = 12.3, userId = -2, } } 

Basically a sorted array of the top players in the leaderboard. You could put the 3 difficulties under the same datastore key if you want to reduce the amount of datastore requests further

Retreiving the data is simple, just do a GetAsync. Updating the data when someone breaks a record is also fairly simple. First, compare their score with the current leaderboard (there is no point in saving their score to the leaderboard datastore, if it wont appear in the leaderboard). If their score is good enough to make it into the leaderboard, use UpdateAsync to insert their data into the sorted array. If they are already on the leaderboard, check if their new score is better than their old score. Since the array is updated instead of overwritten, the order in which updates are done isn’t important, so no session locking shenanigans or anything needed. UpdateAsync ensures race conditions cannot happen, and that is all that is needed

With such a system, if you have 18 datastores, updating every minute, that is 18 read requests per minute. The limit for GetAsync() is 60 + 10 * #Players, so about 1/3 of the limit is used for an empty server. There is also a throughput read limit of 25MB per minute, which you shouldn’t come close to if you limit your leaderboards to a couple hundreds of entries (well, even with thousands of entries, it wouldn’t be an issue)

For write limits, since writes only happen when someone breaks a record, write limits happen less often. It will depend on how often records are broken

This method is harder to use when the scores can go down. Doing so requires overwriting the previous score from said user


I’ve used this system to rank games in sorts in Game Discovery Hub and BloxBox. Wubby also uses this technique (if hazel didn’t change it) for ranking active worlds, but using MemoryStores instead of datastores, as the worlds (since they were all active), were updating the array too often, causing throttling limits (not a write limit, but rather a limit on how often a single key can get updated), and memory store expiration was perfect for ensuring inactive worlds don’t get stuck on the list

2 Likes