I am dealing with a PostgreSQL (v14) query of this kind
SELECT
...,
EXISTS (
SELECT 1
FROM table2 t2
WHERE t2.fk = t1.id
AND LOWER(t2.table_name) = 't1'
) AS t2_record_exists
FROM table1 t1;
and was hoping to extract the logic in a function to be used in lateral join, in order to compute that field in a more readable way (as it is to be used in a view).
This is the resulting query
SELECT
...,
t2_record_exists.t2_record_exists
FROM table1 t1
LEFT JOIN LATERAL does_t2_record_exist(t1.id, 't1') t2_record_exists(t2_record_exists) ON TRUE;
That makes use of this function
CREATE OR REPLACE FUNCTION does_t2_record_exist(object_id int8, _table_name text)
RETURNS bool
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN EXISTS (
SELECT 1
FROM table2 t2
WHERE t2.fk = object_id
AND LOWER(t2.table_name) = _table_name
);
END $function$;
The second query suffers a severe performance loss, as it executes in about 6000ms, while the first gets it done in 300ms.
I don't know why this would happen, as I naively assumed that the very same operation (the EXISTS subquery) would be executed the same amount of times (once per row).
What is going wrong here? How can one foresee such performance issues beforehand?
EDIT: Here are the query plans (obtained with EXPLAIN ANALYZE) for my specific case
Query 1 (with subquery):
Index Only Scan using cos_table1 on table1 t1 (cost=0.43..3723311.61 rows=1481535 width=9) (actual time=56.299..247.674 rows=1477585 loops=1)
Heap Fetches: 46760
SubPlan 2
-> Seq Scan on table2 t2 (cost=0.00..15.27 rows=2 width=8) (actual time=11.300..11.454 rows=372 loops=1)
Filter: (lower(table_name) = 't1'::text)
Rows Removed by Filter: 113
Planning Time: 0.085 ms
JIT:
Functions: 13
Options: Inlining true, Optimization true, Expressions true, Deforming true
Timing: Generation 0.769 ms, Inlining 17.544 ms, Optimization 24.557 ms, Emission 13.652 ms, Total 56.524 ms
Execution Time: 276.285 ms
Query 2 (with LATERAL JOIN):
Nested Loop Left Join (cost=0.68..56512.74 rows=1481535 width=9) (actual time=0.039..5978.865 rows=1477585 loops=1)
-> Index Only Scan using cos_table1 on table1 t1 (cost=0.43..26881.79 rows=1481535 width=8) (actual time=0.011..179.682 rows=1477585 loops=1)
Heap Fetches: 46760
-> Function Scan on does_t2_record_exist t2_record_exist (cost=0.25..0.26 rows=1 width=1) (actual time=0.004..0.004 rows=1 loops=1477585)
Planning Time: 0.065 ms
Execution Time: 6024.267 ms