Question about nested loops..

Поиск
Список
Период
Сортировка
От Frits Jalvingh
Тема Question about nested loops..
Дата
Msg-id CAKhTGFURW_AvZgWR20x-kWfSeGts=OWcuOnanhM-dwU9RjiRPw@mail.gmail.com
обсуждение исходный текст
Ответы Re: Question about nested loops..
Список pgsql-performance
Dear list,

We have databases with rather large tables, and we do bulk queries on them, i.e. those queries always have a large result set.
We execute those queries with enable_nestloop set to false, because having nested loops in those queries always cause large performance issues. This usually leads to perfectly acceptable execution plans which use table scans, hash joins and merge joins. We have work_mem set to 64MB to 512MB (depending on the actual database size) to help with these.

I have a query that does not want to work at all without a nested loop and a materialize, while a trivial (but for me strange) change will allow it to make a good plan, and I'm wondering why that is.
The below examples are stripped versions of the real queries (which have a lot more filter conditions) but they exhibit the issue. I tested them on both Postgres 16 and 18, on Ubuntu 25.04, with the same results.

The slow query is this one:

select
    eenheid.id_s as id_s
,   eenheid.identificatie as id
,   exists(
    select
        1
    from datavault.l_cluster_eenheid_ssm
    inner join datavault.s_h_huurovereenkomst_ssm huurovereenkomst_s
        on eenheid.id_h_eenheid = huurovereenkomst_s._l_eenheid
    where eenheid.id_h_eenheid = l_cluster_eenheid_ssm.id_h_eenheid
    and l_cluster_eenheid_ssm.dv_start_dts <= timestamp '2025-10-08 00:00:00'
    and l_cluster_eenheid_ssm.dv_end_dts > timestamp '2025-10-08 00:00:00'
) as qd001_e
from datavault.s_h_eenheid_ssm eenheid

It produces the following execution plan:
Seq Scan on s_h_eenheid_ssm eenheid  (cost=0.00..172110004562434.50 rows=17211 width=21)
  SubPlan 1
    ->  Nested Loop  (cost=10000000000.00..10000007950.48 rows=30 width=0)
          ->  Seq Scan on l_cluster_eenheid_ssm  (cost=0.00..6768.46 rows=15 width=0)
                Filter: ((dv_start_dts <= '2025-10-08 00:00:00'::timestamp without time zone) AND (dv_end_dts > '2025-10-08 00:00:00'::timestamp without time zone) AND (id_h_eenheid = eenheid.id_h_eenheid))
          ->  Materialize  (cost=0.00..1181.65 rows=2 width=0)
                ->  Seq Scan on s_h_huurovereenkomst_ssm huurovereenkomst_s  (cost=0.00..1181.64 rows=2 width=0)
                      Filter: (_l_eenheid = eenheid.id_h_eenheid)

I can make the query very fast by adding a self-join to the root table (s_h_eenheid_ssm) inside the subquery:

select
    eenheid.id_s as id_s
,   eenheid.identificatie as id
,   exists(
    select
        1
    from datavault.s_h_eenheid_ssm eenheid_s
    inner join datavault.l_cluster_eenheid_ssm l_cluster_eenheid_ssm
        on eenheid_s.id_h_eenheid = l_cluster_eenheid_ssm.id_h_eenheid
    inner join datavault.s_h_huurovereenkomst_ssm huurovereenkomst_s
        on eenheid_s.id_h_eenheid = huurovereenkomst_s._l_eenheid
    where eenheid.id_h_eenheid = eenheid_s.id_h_eenheid
    and l_cluster_eenheid_ssm.dv_start_dts <= timestamp '2025-10-08 00:00:00'
    and l_cluster_eenheid_ssm.dv_end_dts > timestamp '2025-10-08 00:00:00'
) as DQ87_e
from datavault.s_h_eenheid_ssm eenheid

which produces a way better plan:

QUERY PLAN
Seq Scan on s_h_eenheid_ssm eenheid  (cost=0.00..344220005300951.31 rows=17211 width=21)
  SubPlan 2
    ->  Hash Join  (cost=3244.40..14039.76 rows=371258 width=4)
          Hash Cond: (l_cluster_eenheid_ssm.id_h_eenheid = eenheid_s.id_h_eenheid)
          ->  Seq Scan on l_cluster_eenheid_ssm  (cost=0.00..6139.82 rows=251455 width=4)
                Filter: ((dv_start_dts <= '2025-10-08 00:00:00'::timestamp without time zone) AND (dv_end_dts > '2025-10-08 00:00:00'::timestamp without time zone))
          ->  Hash  (cost=2926.76..2926.76 rows=25411 width=8)
                ->  Hash Join  (cost=1459.25..2926.76 rows=25411 width=8)
                      Hash Cond: (huurovereenkomst_s._l_eenheid = eenheid_s.id_h_eenheid)
                      ->  Seq Scan on s_h_huurovereenkomst_ssm huurovereenkomst_s  (cost=0.00..1118.11 rows=25411 width=4)
                      ->  Hash  (cost=1244.11..1244.11 rows=17211 width=4)
                            ->  Seq Scan on s_h_eenheid_ssm eenheid_s  (cost=0.00..1244.11 rows=17211 width=4)

The difference in execution speed is large; the slow query takes about 30 seconds on a test system while the fast one is under a second.

I do not understand why the simpler query (without the self join) produces a plan that seems to require nested loops, I hope someone can explain.

В списке pgsql-performance по дате отправления: