From 9a0a7e62a76640c4591e057d224f414229398d1e Mon Sep 17 00:00:00 2001 From: Uku Taht Date: Sat, 7 Mar 2026 17:59:47 +0200 Subject: [PATCH 1/2] [perf] skip reading engagements when not necessary --- lib/plausible/stats/sql/where_builder.ex | 38 ++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/lib/plausible/stats/sql/where_builder.ex b/lib/plausible/stats/sql/where_builder.ex index 18a15546c0b1..ee9c4835bfce 100644 --- a/lib/plausible/stats/sql/where_builder.ex +++ b/lib/plausible/stats/sql/where_builder.ex @@ -19,6 +19,23 @@ defmodule Plausible.Stats.SQL.WhereBuilder do ] @doc "Builds WHERE clause for a given Query against sessions or events table" + def build(:events, query) do + base_condition = filter_site_time_range(:events, query) + + engagement_condition = + if skip_engagement_events?(query) do + dynamic([e], e.name != "engagement") + else + true + end + + query.filters + |> Enum.map(&add_filter(:events, query, &1)) + |> Enum.reduce(dynamic([], ^base_condition and ^engagement_condition), fn condition, acc -> + dynamic([], ^acc and ^condition) + end) + end + def build(table, query) do base_condition = filter_site_time_range(table, query) @@ -27,6 +44,27 @@ defmodule Plausible.Stats.SQL.WhereBuilder do |> Enum.reduce(base_condition, fn condition, acc -> dynamic([], ^acc and ^condition) end) end + # Engagement events exist solely for time_on_page and scroll_depth tracking. + # Because 'name' is part of the events_v2 primary key, adding WHERE name != 'engagement' + # lets ClickHouse skip entire index granules with only engagement rows. + # This is safe whenever none of the requested metrics or goal filters need engagement rows. + defp skip_engagement_events?(query) do + :time_on_page not in query.metrics and + :scroll_depth not in query.metrics and + not scroll_goal_involved?(query) + end + + defp scroll_goal_involved?(%{preloaded_goals: %{matching_toplevel_filters: goals}} = query) do + Enum.any?(goals, fn goal -> Plausible.Goal.type(goal) == :scroll end) and + (Enum.any?(query.filters, fn + [_, "event:goal", _] -> true + _ -> false + end) or + "event:goal" in query.dimensions) + end + + defp scroll_goal_involved?(_query), do: false + @doc """ Builds WHERE clause condition based off of a filter and a custom column name Used for special business logic cases From a49b51d20c861d3761f3f89f4f5df24c11d7f8b3 Mon Sep 17 00:00:00 2001 From: Uku Taht Date: Sat, 7 Mar 2026 19:29:07 +0200 Subject: [PATCH 2/2] Fix funnel query --- extra/lib/plausible/stats/funnel.ex | 4 ++++ lib/plausible/stats/sql/where_builder.ex | 10 +++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/extra/lib/plausible/stats/funnel.ex b/extra/lib/plausible/stats/funnel.ex index a9fae4ca90d6..46ee57b1676d 100644 --- a/extra/lib/plausible/stats/funnel.ex +++ b/extra/lib/plausible/stats/funnel.ex @@ -28,6 +28,10 @@ defmodule Plausible.Stats.Funnel do end def funnel(_site, query, %Funnel{} = funnel) do + funnel_goals = Enum.map(funnel.steps, & &1.goal) + + query = %{query | preloaded_goals: %{all: funnel_goals}} + funnel_data = query |> Base.base_event_query() diff --git a/lib/plausible/stats/sql/where_builder.ex b/lib/plausible/stats/sql/where_builder.ex index ee9c4835bfce..9cf057c1d49a 100644 --- a/lib/plausible/stats/sql/where_builder.ex +++ b/lib/plausible/stats/sql/where_builder.ex @@ -54,13 +54,9 @@ defmodule Plausible.Stats.SQL.WhereBuilder do not scroll_goal_involved?(query) end - defp scroll_goal_involved?(%{preloaded_goals: %{matching_toplevel_filters: goals}} = query) do - Enum.any?(goals, fn goal -> Plausible.Goal.type(goal) == :scroll end) and - (Enum.any?(query.filters, fn - [_, "event:goal", _] -> true - _ -> false - end) or - "event:goal" in query.dimensions) + defp scroll_goal_involved?(%{preloaded_goals: %{all: goals}} = query) do + Enum.any?(goals, fn goal -> Plausible.Goal.type(goal) == :scroll end) or + "event:goal" in query.dimensions end defp scroll_goal_involved?(_query), do: false