What are some practices to help optimize Datastores?

Datastores to me have always been a hassle to work with. A game that I’m working on uses datastores (normal stock datastores, no external modules and/or datastore2) intensely, and I feel as that game is extremely inefficient in how it processes datastore requests. My game uses datastores for game passes, inventory information, currency, etc, yet nearly everything is stored as a bool value (excluding inventory, which is stored as a table). Are there any rules-of-thumb and/or methods to go about to reduce the number of datastore requests my game needs to make? (Storing tables, dictionaries, etc).

4 Likes

Yes, the biggest thing you can do is “session caching,” which is just fancy for storing the data locally in a script during gameplay instead of accessing the datastore constantly.

The general flow:

  1. Someone requests access to data in the data store
  2. Check to see if the data exists in the cache
    a. If it is, return what’s in the cache
    b. If it is not, load it from the datastore and then store in cache
  3. When player leaves, save the value in cache to the datastore

So the net result is that you only call DataStore once to get the data, and very infrequently call it to write data.

Some people will also add an autosaving feature that periodically scans the cache to save data.

A really rudimentary implementation might look like this:

local DataStoreService = game:GetService("DataStoreService")

local cache = {}

local function GetDataStore(player)
	return DataStoreService:GetDataStore("data", tostring(player.UserId))
end

local function LoadFromDataStore(player, key)
	local dataStore = GetDataStore(player)
	return pcall(function()
		return dataStore:GetAsync(key)
	end)
end

local function SaveToDataStore(player, key, value)
	local dataStore = GetDataStore(player)
	return pcall(function()
		return dataStore:SetAsync(player, key, value)
	end)
end

local function GetData(player, key)
	local plrCache = cache[player]
	local success, data = true, plrCache[key]
	if (data == nil) then
		success, data = LoadFromDataStore(player, key)
		if (success) then
			plrCache[key] = data
		end
	end
	return plrCache[key]
end

local function SetData(player, key, value)
	local plrCache = cache[player]
	plrCache[key] = value
end

game.Players.PlayerAdded:Connect(function(player)
	cache[player] = {}
end)

game.Players.PlayerRemoving:Connect(function(player)
	-- Save cache to datastore when player leaves:
	local plrCache = cache[player]
	for k,v in pairs(plrCache) do
		SaveToDataStore(player, k, v)
	end
end)

You can then call SetData(player, key, value) and GetData(player, key). Importantly, GetData returns two values, the first of which is whether or not the fetch was successful.

However, I really recommend using something like DataStore2 or ProfileService, because there’s a lot of weird edge-cases that my above solution fails to address. It doesn’t handle session locking; it doesn’t handle race conditions; it doesn’t handle invalid data; etc.

11 Likes