feix: optimize queries for groups
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
Simon 2026-01-29 15:22:21 +01:00
parent 124ab295a6
commit b4adf63e83
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
4 changed files with 73 additions and 15 deletions

View file

@ -113,16 +113,22 @@ defmodule MvWeb.GroupLive.Index do
defp load_groups(actor) do
require Ash.Query
# Load groups without aggregates first (faster)
query =
Mv.Membership.Group
|> Ash.Query.load(:member_count)
|> Ash.Query.sort(:name)
opts = ash_actor_opts(actor)
case Ash.read(query, opts) do
{:ok, groups} ->
groups
# Load all member counts in a single batch query (avoids N+1)
member_counts = load_member_counts_batch(groups)
# Attach counts to groups
Enum.map(groups, fn group ->
Map.put(group, :member_count, Map.get(member_counts, group.id, 0))
end)
{:error, _error} ->
require Logger
@ -130,4 +136,33 @@ defmodule MvWeb.GroupLive.Index do
[]
end
end
# Loads all member counts for groups using DB-side aggregation for better performance
# This avoids N+1 queries when loading member_count aggregate for each group
@spec load_member_counts_batch([Mv.Membership.Group.t()]) :: %{
Ecto.UUID.t() => non_neg_integer()
}
defp load_member_counts_batch(groups) do
group_ids = Enum.map(groups, & &1.id)
if Enum.empty?(group_ids) do
%{}
else
# Use Ecto directly for efficient GROUP BY COUNT query
# This is much more performant than loading aggregates for each group individually
# Note: We bypass Ash here for performance, but this is a simple read-only query
import Ecto.Query
query =
from mg in Mv.Membership.MemberGroup,
where: mg.group_id in ^group_ids,
group_by: mg.group_id,
select: {mg.group_id, count(mg.id)}
results = Mv.Repo.all(query)
results
|> Enum.into(%{}, fn {group_id, count} -> {group_id, count} end)
end
end
end

View file

@ -38,7 +38,16 @@ defmodule MvWeb.GroupLive.Show do
end
defp load_group_by_slug(socket, slug, actor) do
case Membership.get_group_by_slug(slug, actor: actor, load: [:members, :member_count]) do
# Load group with members and member_count
# Using explicit load ensures efficient preloading of members relationship
require Ash.Query
query =
Mv.Membership.Group
|> Ash.Query.filter(slug == ^slug)
|> Ash.Query.load([:members, :member_count])
case Ash.read_one(query, actor: actor, domain: Mv.Membership) do
{:ok, nil} ->
{:noreply,
socket