From 68c09b761e9e819992746df74e2a9af42ccb91be Mon Sep 17 00:00:00 2001 From: Moritz Date: Thu, 8 Jan 2026 15:58:53 +0100 Subject: [PATCH] perf: optimize load_user_counts with DB-side aggregation Replace Elixir-side counting with Ecto GROUP BY COUNT query for better performance. This avoids loading all users into memory and performs the aggregation directly in the database. --- lib/mv_web/live/role_live/index.ex | 35 +++++++++++++----------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/lib/mv_web/live/role_live/index.ex b/lib/mv_web/live/role_live/index.ex index 718aa34..9d75da6 100644 --- a/lib/mv_web/live/role_live/index.ex +++ b/lib/mv_web/live/role_live/index.ex @@ -126,33 +126,28 @@ defmodule MvWeb.RoleLive.Index do end end - # Loads all user counts for roles in a single query to avoid N+1 queries - # TODO: Optimize to use DB-side aggregation instead of loading all users + # Loads all user counts for roles using DB-side aggregation for better performance @spec load_user_counts([Mv.Authorization.Role.t()], map() | nil) :: %{ Ecto.UUID.t() => non_neg_integer() } - defp load_user_counts(roles, actor) do + defp load_user_counts(roles, _actor) do role_ids = Enum.map(roles, & &1.id) - # Load all users with role_id in a single query - opts = opts_with_actor([], actor, Mv.Accounts) + # Use Ecto directly for efficient GROUP BY COUNT query + # This is much more performant than loading all users and counting in Elixir + # Note: We bypass Ash here for performance, but this is a simple read-only query + import Ecto.Query - users = - case Ash.read( - Accounts.User - |> Ash.Query.filter(role_id in ^role_ids) - |> Ash.Query.select([:role_id]), - opts - ) do - {:ok, users_list} -> users_list - {:error, _} -> [] - end + query = + from u in Accounts.User, + where: u.role_id in ^role_ids, + group_by: u.role_id, + select: {u.role_id, count(u.id)} - # Group by role_id and count - users - |> Enum.group_by(& &1.role_id) - |> Enum.map(fn {role_id, users_list} -> {role_id, length(users_list)} end) - |> Map.new() + results = Mv.Repo.all(query) + + results + |> Enum.into(%{}, fn {role_id, count} -> {role_id, count} end) end # Gets user count from preloaded assigns map