Perf: canonical PV fn přes plpgsql execute format — vynucený plán dle skutečných hodnot
All checks were successful
CI and deploy / migration-check (push) Successful in 21s
CI and deploy / deploy (push) Successful in 51s

set plan_cache_mode=force_custom_plan na SQL funkci v PG 18 nezabral (stále
generický plán, 4.5-9 s / 600k buffers). plpgsql EXECUTE s literály = vždy
čerstvý plán: tělo s konstantami změřeno 0.42 s / 34k buffers. Signatura,
chování i výstup beze změny.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dusan Vojacek
2026-06-12 15:54:44 +02:00
parent 62a5c64f77
commit 8554cd1bc1

View File

@@ -25,27 +25,31 @@ create or replace function ems.fn_forecast_pv_slots_range_canonical_ab(
p_decay_slots int default 16
)
returns jsonb
language sql
language plpgsql
stable
set work_mem = '64MB'
-- PG 18 cachuje plány SQL funkcí → generický plán bez znalosti hodnot parametrů
-- (4.5 s / 607k buffers); s custom planem dle skutečných hodnot 0.4 s / 34k.
set plan_cache_mode = force_custom_plan
as $fn$
declare
v_result jsonb;
begin
-- PG 18 cachuje plán SQL/plpgsql statementů s parametry → generický plán
-- (4.59 s / 600k buffers); force_custom_plan na funkci nepomohl. Execute
-- s literály vynutí čerstvý plán dle skutečných hodnot: 0.4 s / 34k buffers.
execute format($q$
with tz as (
select coalesce(nullif(trim(s.timezone), ''), 'Europe/Prague') as tz_name
from ems.site s
where s.id = p_site_id
where s.id = %1$s
),
bounds as (
select
date_bin(interval '15 minutes', p_from, timestamptz '1970-01-01T00:00:00Z') as ts_from,
date_bin(interval '15 minutes', %2$L::timestamptz, timestamptz '1970-01-01T00:00:00Z') as ts_from,
case
when p_to <= p_from then date_bin(interval '15 minutes', p_from, timestamptz '1970-01-01T00:00:00Z') + interval '15 minutes'
when p_to > p_from + interval '60 days' then date_bin(interval '15 minutes', p_from, timestamptz '1970-01-01T00:00:00Z') + interval '60 days'
else date_bin(interval '15 minutes', p_to, timestamptz '1970-01-01T00:00:00Z')
when %3$L::timestamptz <= %2$L::timestamptz then date_bin(interval '15 minutes', %2$L::timestamptz, timestamptz '1970-01-01T00:00:00Z') + interval '15 minutes'
when %3$L::timestamptz > %2$L::timestamptz + interval '60 days' then date_bin(interval '15 minutes', %2$L::timestamptz, timestamptz '1970-01-01T00:00:00Z') + interval '60 days'
else date_bin(interval '15 minutes', %3$L::timestamptz, timestamptz '1970-01-01T00:00:00Z')
end as ts_to,
date_bin(interval '15 minutes', p_now, timestamptz '1970-01-01T00:00:00Z') as now_slot
date_bin(interval '15 minutes', %4$L::timestamptz, timestamptz '1970-01-01T00:00:00Z') as now_slot
),
slot_spine as (
select gs as interval_start
@@ -68,11 +72,11 @@ as $fn$
),
factor_raw as (
select ems.fn_pv_forecast_correction_factor(
p_site_id,
(p_now - (p_factor_window_h::text || ' hours')::interval)::timestamptz,
p_now,
p_factor_min_clamp,
p_factor_max_clamp
%1$s,
(%4$L::timestamptz - (%9$s::text || ' hours')::interval)::timestamptz,
%4$L::timestamptz,
%10$s,
%11$s
) as j
),
factor as (
@@ -82,11 +86,11 @@ as $fn$
),
profile as (
select ems.fn_pv_forecast_delta_profile_cached(
p_site_id,
p_delta_data_from,
p_delta_data_to,
p_half_life_days,
p_threshold_w
%1$s,
%5$L::timestamptz,
%6$L::timestamptz,
%7$s,
%8$s
) as j
),
delta_by_array as (
@@ -117,16 +121,16 @@ as $fn$
on fpi.interval_start >= b.ts_from
and fpi.interval_start < b.ts_to
and fpi.pv_array_id in (
select apa0.id from ems.asset_pv_array apa0 where apa0.site_id = p_site_id
select apa0.id from ems.asset_pv_array apa0 where apa0.site_id = %1$s
)
inner join ems.forecast_pv_run fpr
on fpr.id = fpi.run_id
and fpr.site_id = p_site_id
and fpr.site_id = %1$s
and fpr.pv_array_id = fpi.pv_array_id
and fpr.status = 'ok'
inner join ems.asset_pv_array apa
on apa.id = fpr.pv_array_id
and apa.site_id = p_site_id
and apa.site_id = %1$s
order by fpi.interval_start, fpr.pv_array_id, fpr.created_at desc
),
fc_with_sod as (
@@ -193,11 +197,11 @@ as $fn$
f.rolling_factor,
case
when ab.interval_start < b.now_slot then 1.0::numeric
when p_decay_slots <= 0 then f.rolling_factor
when %12$s <= 0 then f.rolling_factor
else
case
when ((extract(epoch from (ab.interval_start - b.now_slot)) / 900)::int) >= p_decay_slots then 1.0::numeric
else (1.0::numeric + (f.rolling_factor - 1.0::numeric) * (1.0::numeric - ((extract(epoch from (ab.interval_start - b.now_slot)) / 900)::numeric / p_decay_slots::numeric)))
when ((extract(epoch from (ab.interval_start - b.now_slot)) / 900)::int) >= %12$s then 1.0::numeric
else (1.0::numeric + (f.rolling_factor - 1.0::numeric) * (1.0::numeric - ((extract(epoch from (ab.interval_start - b.now_slot)) / 900)::numeric / %12$s::numeric)))
end
end as rolling_effective_factor
from fc_ab ab
@@ -225,7 +229,23 @@ as $fn$
),
'[]'::jsonb
)
from with_factor w;
from with_factor w
$q$,
p_site_id,
p_from,
p_to,
p_now,
p_delta_data_from,
p_delta_data_to,
p_half_life_days,
p_threshold_w,
p_factor_window_h,
p_factor_min_clamp,
p_factor_max_clamp,
p_decay_slots
) into v_result;
return coalesce(v_result, '[]'::jsonb);
end;
$fn$;
comment on function ems.fn_forecast_pv_slots_range_canonical_ab is