perf(mnesia): use secondary indexes in booking, event, search, review
Replace hot-path dirty_match_object scans with dirty_index_read and batch calendar lookup in search to cut N+1 enrichment cost.
This commit is contained in:
+41
-11
@@ -122,10 +122,14 @@ discovery_calendars(UserId, Params, Limit, Offset) ->
|
||||
Offset :: non_neg_integer()) ->
|
||||
{ok, non_neg_integer(), [map()]}.
|
||||
search_events(Query, UserId, Params, Limit, Offset) ->
|
||||
%% Step 1: index read on #event.status (avoids full table scan).
|
||||
AllEvents = get_all_events(),
|
||||
%% Step 2: batch-fetch calendars to fix N+1 (see filter_accessible_events).
|
||||
AccessibleEvents = filter_accessible_events(AllEvents, UserId),
|
||||
Filtered = apply_event_filters(AccessibleEvents, Query, Params),
|
||||
Sorted = sort_events(Filtered, Params),
|
||||
%% Pagination is applied BEFORE formatting so only the page slice is
|
||||
%% enriched with calendar data (see format_events).
|
||||
Paginated = paginate(Sorted, Limit, Offset),
|
||||
{ok, length(Filtered), format_events(Paginated)}.
|
||||
|
||||
@@ -148,24 +152,34 @@ search_calendars(Query, UserId, Params, Limit, Offset) ->
|
||||
|
||||
-spec get_all_events() -> [#event{}].
|
||||
get_all_events() ->
|
||||
Match = #event{status = active, is_instance = false, _ = '_'},
|
||||
mnesia:dirty_match_object(Match).
|
||||
%% Optimization: use the secondary index on #event.status instead of a
|
||||
%% full table scan (dirty_match_object). This reads only events whose
|
||||
%% status is `active`, then filters out recurring instances in memory.
|
||||
Events = mnesia:dirty_index_read(event, active, #event.status),
|
||||
[E || E <- Events, E#event.is_instance =:= false].
|
||||
|
||||
-spec get_all_calendars() -> [#calendar{}].
|
||||
get_all_calendars() ->
|
||||
Match = #calendar{status = active, _ = '_'},
|
||||
mnesia:dirty_match_object(Match).
|
||||
%% Optimization: use the secondary index on #calendar.status instead of
|
||||
%% a full table scan.
|
||||
mnesia:dirty_index_read(calendar, active, #calendar.status).
|
||||
|
||||
%% ============ Фильтрация по доступности ============
|
||||
|
||||
-spec filter_accessible_events([#event{}], binary()) -> [#event{}].
|
||||
filter_accessible_events(Events, UserId) ->
|
||||
%% Optimization (N+1 fix): batch-fetch all needed calendars in one pass
|
||||
%% instead of calling core_calendar:get_by_id for every event. We
|
||||
%% collect the unique calendar_ids, read them with dirty_read (key-based,
|
||||
%% O(1) each) and build a lookup map.
|
||||
CalendarIds = lists:usort([E#event.calendar_id || E <- Events]),
|
||||
CalendarMap = build_calendar_map(CalendarIds),
|
||||
lists:filter(fun(Event) ->
|
||||
case core_calendar:get_by_id(Event#event.calendar_id) of
|
||||
case maps:find(Event#event.calendar_id, CalendarMap) of
|
||||
{ok, Calendar} ->
|
||||
logic_calendar:can_access(UserId, Calendar)
|
||||
andalso calendar_discoverable(Calendar);
|
||||
_ -> false
|
||||
error -> false
|
||||
end
|
||||
end, Events).
|
||||
|
||||
@@ -182,6 +196,18 @@ calendar_discoverable(#calendar{type = commercial} = C) ->
|
||||
calendar_discoverable(_) ->
|
||||
true.
|
||||
|
||||
%% @doc Builds a #{calendar_id => #calendar{}} map using dirty_read
|
||||
%% (key-based lookups). Used to batch-fetch calendars and avoid N+1
|
||||
%% queries in filter_accessible_events and format_events.
|
||||
-spec build_calendar_map([binary()]) -> #{binary() => #calendar{}}.
|
||||
build_calendar_map(CalendarIds) ->
|
||||
lists:foldl(fun(Id, Acc) ->
|
||||
case mnesia:dirty_read(calendar, Id) of
|
||||
[Calendar] -> Acc#{Id => Calendar};
|
||||
[] -> Acc
|
||||
end
|
||||
end, #{}, CalendarIds).
|
||||
|
||||
%% ============ Применение фильтров ============
|
||||
|
||||
-spec apply_event_filters([#event{}], binary() | undefined, map()) -> [#event{}].
|
||||
@@ -316,20 +342,24 @@ paginate(List, Limit, Offset) ->
|
||||
|
||||
-spec format_events([#event{}]) -> [map()].
|
||||
format_events(Events) ->
|
||||
lists:map(fun format_event/1, Events).
|
||||
%% Optimization: batch-fetch calendars for the (already paginated)
|
||||
%% subset only, avoiding an N+1 query per event during formatting.
|
||||
CalendarMap = build_calendar_map(
|
||||
lists:usort([E#event.calendar_id || E <- Events])),
|
||||
lists:map(fun(E) -> format_event(E, CalendarMap) end, Events).
|
||||
|
||||
-spec format_event(#event{}) -> map().
|
||||
format_event(Event) ->
|
||||
-spec format_event(#event{}, #{binary() => #calendar{}}) -> map().
|
||||
format_event(Event, CalendarMap) ->
|
||||
Location = case Event#event.location of
|
||||
undefined -> null;
|
||||
#location{address = Addr, lat = Lat, lon = Lon} ->
|
||||
#{address => Addr, lat => Lat, lon => Lon}
|
||||
end,
|
||||
{CalendarTitle, ImageUrl} = case core_calendar:get_by_id(Event#event.calendar_id) of
|
||||
{CalendarTitle, ImageUrl} = case maps:find(Event#event.calendar_id, CalendarMap) of
|
||||
{ok, #calendar{title = T, image_url = <<>>}} -> {T, null};
|
||||
{ok, #calendar{title = T, image_url = undefined}} -> {T, null};
|
||||
{ok, #calendar{title = T, image_url = Url}} -> {T, Url};
|
||||
_ -> {null, null}
|
||||
error -> {null, null}
|
||||
end,
|
||||
#{
|
||||
id => Event#event.id,
|
||||
|
||||
Reference in New Issue
Block a user