Optimal way to store User Ids?

In my game I save the player’s user id once they join for the first time. This is so I can make easy edits to DataStores and keeping overall total player visit counts.

local ServerStore = game.DataStoreService:GetDataStore("ServerStore")
local IDList = ServerStore:GetAsync("IDlist")
for i, v in pairs(IDList) do 
	print(v) 
end

This function just goes through my data and prints it.
The main thing is that the array of Ids are in a single key, which may cause problems when it goes over the 4MB limit.

	local IDList = ServerStore:GetAsync("IDlist")
	if not IDList then IDList = {} end
	if not table.find(IDList,Player.UserId) then
		table.insert(IDList,Player.UserId)
		ServerStore:SetAsync("IDlist",IDList)
	end

This function is the one I currently have under my ‘UnloadPlayerData’ function that connects when a player is added.

Is there a more effiecent way to store the UserIds and to add them?

Is there a function in Roblox API to do that automatically?

How would you define efficient? Do you want to save on DataStore space? Do you want faster read/write times? What exactly are you hoping to improve on?

I don’t think you can do this natively. Roblox does track players who joined though, and that’s how erasure requests are made, but there isn’t a way to access it.

The absolute best approach would probably be to use an external database, but I would assume that if you owned an external database, you would know how to use it (I don’t so I can’t give an example for that, sorry :( ).

The next best approach would probably be to use DataStores. I wouldn’t store every user ID in a table though. Instead, I would use DataStoreV2 (not to be confused with the DataStore2.0 module), make each key the UserId, make the value true (assuming you aren’t storing any other data elsewhere). Then to view the full list, use DataStore:ListKeysAsync() or this plugin to view the full list.

So for example,

local dataStoreService = game:GetService('DataStoreService')
local players = game:GetService('Players')

local userIdDataStore = dataStoreService:GetDataStore('UserIds')

players.PlayerAdded:Connect(function(player)
    local success, result = pcall(userIdDataStore.SetAsync, userIdDataStore, player.UserId, true) -- sets the key that is the player's UserId and value true
end)

Doing that would also make right to erasure requests easier than if you had them all in a single table.

If you’re already saving data elsewhere, you don’t have to call SetAsync each time the player joins. Instead, you can just use the player’s data as normal since it won’t be equal to nil unless the player hasn’t joined before.

2 Likes

@7z99’s reply is pretty good if you are trying to save on DataStore space.

If you store all the UserIds in one single key, there is a chance you may reach the limit and need to overflow into another key, which could make things messy.

Storing each UserId as its own key makes things easier to work with.

2 Likes
local ds = game:GetService("DataStoreService"):GetDataStore("PlayerVisitData")

local playervisitdata = {}

game.Players.PlayerAdded:Connect(function(player)
    local key = "id_"..player
    local data 
    local success, err = pcall(function()
        data = ds:GetAsync(key)
    end)
    
    if success then
       if not table.find(playervisitsdata, data) then
          table.insert(playervisitdata, data) 
       else
          print(data)
       end
    else
       warn(err)
    end
end)

game.Player.PlayerRemoving:Connect(function(player)
    local key = "id_"..player
    local data 
    local success, err = pcall(function()
        data = ds:SetAsync(key, playervisitdata)
    end)

   if success then
       print("all went well!")
   else
       warn(err)
   end
end)

game:BindToClose(function()
    wait(1)
end)

Considering how trivial it is to index the “UserId” property of a player instance there is no optimal way to store UserIds of players, instead just index that property when necessary, however, for the sake of representation the following implementation records the UserIds of every player in a dictionary (it’s faster to index a dictionary than it is to iterate over an entire array).

local Players = game:GetService("Players")
local DataStores = game:GetService("DataStoreService")
local DataStore = DataStores:GetDataStore("DataStore")

local ProtectedCall = pcall

local UserIds = {}

local function CreateStats(Player)
	local Leaderstats = Instance.new("Folder")
	Leaderstats.Name = "leaderstats"
	Leaderstats.Parent = Player

	local Visits = Instance.new("IntValue")
	Visits.Name = "Visits"
	Visits.Parent = Leaderstats

	return true
end

local function LoadData(Player)
	local Success, Result = ProtectedCall(function()
		return DataStore:GetAsync(UserIds[Player])
	end)

	if Success then
		Player.leaderstats.Visits.Value = Result or 0
	else
		warn(Result)
	end

	return true
end

local function SaveData(Player)
	local Success, Result = ProtectedCall(function()
		return DataStore:SetAsync(UserIds[Player], Player.leaderstats.Visits.Value)
	end)

	if Success then
		print(Result)
	else
		warn(Result)
	end

	return true
end

local function OnPlayerAdded(Player)
	UserIds[Player] = Player.UserId

	local _Stats = CreateStats(Player)
	repeat
		task.wait()
	until _Stats

	local Data = LoadData(Player)
	repeat
		task.wait()
	until Data

	Player.leaderstats.Visits.Value += 1
end

local function OnPlayerRemoving(Player)
	local Data = SaveData(Player)
	repeat
		task.wait()
	until Data

	UserIds[Player] = nil
end

local function OnServerShutdown()
	for _, Player in ipairs(Players:GetPlayers()) do
		SaveData(Player)
	end
	
	UserIds = {}
end

Players.PlayerAdded:Connect(OnPlayerAdded)
Players.PlayerRemoving:Connect(OnPlayerRemoving)
game:BindToClose(OnServerShutdown)

If you mean that you want to conserve space, you can use binary to greatly compress number values.