Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1vdewR-003E6W-2s for pgsql-performance@arkaria.postgresql.org; Thu, 08 Jan 2026 01:36:00 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1vdewQ-00GeJg-2l for pgsql-performance@arkaria.postgresql.org; Thu, 08 Jan 2026 01:35:59 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1vdewQ-00GeJY-1B for pgsql-performance@lists.postgresql.org; Thu, 08 Jan 2026 01:35:59 +0000 Received: from mail-pf1-x42e.google.com ([2607:f8b0:4864:20::42e]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.96) (envelope-from ) id 1vdewP-004o41-1S for pgsql-performance@lists.postgresql.org; Thu, 08 Jan 2026 01:35:57 +0000 Received: by mail-pf1-x42e.google.com with SMTP id d2e1a72fcca58-7f121c00dedso2450204b3a.0 for ; Wed, 07 Jan 2026 17:35:57 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1767836155; x=1768440955; darn=lists.postgresql.org; h=content-transfer-encoding:subject:from:to:content-language :user-agent:mime-version:date:message-id:from:to:cc:subject:date :message-id:reply-to; bh=wVZk8g8SXVwKwzWZQm1v8jfzp3Zm2I0ReQLTGLShEPY=; b=dFUaMZ56hs4YoVBMCaumyWoO2tN+ixbrhVh72CkyJVHP27MtUUZPm4Ntu8dEOgrMlQ D6heM2nh451CHgN/0nUp9B52QbeGPBZM3Bs7YfN3Mne8A8sK3T8I2EiY+evyJDOfSuJA sFpjzLxAJNjUgeoCRnvOVUDckOq7/RShhdlEQPbB+kz6IoOZhpx+9TNo0Cb33468SaSQ a/v0ZkwpyMgXvYzkWWL8hLCfLTg0K4wnsT8KVYnzcC5sdhA7pWL6p3xb86qjByC06Yhn j64dTVU+EJ222flx9L1eXpWIUbYPMMQ7jHvBRoC/6NyPZCYeaGbHsEHwlCsW6VYq4+4O jj2Q== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1767836155; x=1768440955; h=content-transfer-encoding:subject:from:to:content-language :user-agent:mime-version:date:message-id:x-gm-gg:x-gm-message-state :from:to:cc:subject:date:message-id:reply-to; bh=wVZk8g8SXVwKwzWZQm1v8jfzp3Zm2I0ReQLTGLShEPY=; b=n0UA+5Fc9nRs6NxAL2z9ayOETD4IHzPIo7nJI/0nBmKXGL9HaFOgdM6n8S8t91akr3 cy7mE6tUgc6qtjZ1rGNOAM4Mx7+VeYcs399cKK4TWQG5yE+G8YCRH+9duuvvPFAZsZEs q0nuiN4tYZhwd/QXy4ls/5vzANebveu95ZjQhSun5agHlNl/eq7TQ7zd2oiCqpg8pRvg ELVoJDDtp1m4EAIQcd5aNh1HxRhqNlLyQUcA0tEy+kERzaqYjgurzB0E3wb6U5cbJ1Lj FdL1RaahYaPKXezQ9RLK7X8z0nMzVM3avQNraGlgSex2gQkVQ/23r8skb+MaFIkhShiH SNzg== X-Gm-Message-State: AOJu0YxT+Uc9m8yZocKW3zCKTCczfXWQlhaW0VGOwVjgPgly7Ej1kiZt LptE+mHUQ7pkJFAwIv817Z1bOGXQW5vQtdV2obDdD5PNHmd9sKfiCP0lKw5Qyg== X-Gm-Gg: AY/fxX5Wp2DeKcZG9j2d6Fy2VUkLGJr7V5oSWiAenasaBfcMPqKiJJuFchE6kSOCp+z o20EiMqP1V+fPr9UW+HmQszliefIsI8Ps4J70krLJ3pU5k7dRr0BQDVvehxJhyz9u4vdER5Jirv RO8YTd99yECvC0+uXh4kEGzqiOHTUQYRsio03dmsF9TKiziHX+gZp5vdkQcPtXBQg5whHsJO0F8 jkCLe/ANVOh7sbBDejSDJcdR3IgIajySOFo7XqYGVBRExoydiO+r3HQCWR23Y6zwRebwRwDCOUT 5BWf1hVZ/EFamigpFJqJS6fKrb4ibE9tIfdWI68/uC9zXAmMdVUM3xyIhmM5QS9Tj3G2LN3eDgA 3eqKRTWRqTIiRjpO8/qxtEq33eAjVzZwmXb0SrJUCVfo42GMPV+ZkX26XnIlBu9x+nIpY6+cwjO 6Q/tu4lEsP2Vyb7WtE3lW5UG0QZ5EMPCV9mr5O2GonpxbKjYV9+7quTOCuolWrxOLfOw== X-Google-Smtp-Source: AGHT+IFYnNj0JQVbXQC2WZHH4oXJXnuRGbf5wF+8zr/rJX3UumgqEqpxWW7qPQ93l1mZznUFOAsfwQ== X-Received: by 2002:a05:6a00:414f:b0:7f0:4694:1c21 with SMTP id d2e1a72fcca58-81b7de5c5a2mr3825837b3a.20.1767836155172; Wed, 07 Jan 2026 17:35:55 -0800 (PST) Received: from ?IPV6:2404:4400:414b:1700:a5b4:2777:4a35:4815? ([2404:4400:414b:1700:a5b4:2777:4a35:4815]) by smtp.gmail.com with ESMTPSA id d2e1a72fcca58-819c5302c61sm5978455b3a.42.2026.01.07.17.35.53 for (version=TLS1_3 cipher=TLS_AES_128_GCM_SHA256 bits=128/128); Wed, 07 Jan 2026 17:35:54 -0800 (PST) Message-ID: <71afdb09-0583-4af2-a3b3-50da4f987b9b@gmail.com> Date: Thu, 8 Jan 2026 14:35:50 +1300 MIME-Version: 1.0 User-Agent: Mozilla Thunderbird Content-Language: en-US To: pgsql-performance@lists.postgresql.org From: Mark Kirkwood Subject: Unexpected planner choice in simple JOIN Content-Type: text/plain; charset=UTF-8; format=flowed Content-Transfer-Encoding: 8bit List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk I have a 2 table parent child setup (tab0 -< tab1) with a fairly small (100 K rows) parent and big child (100 M rows).. Exact setup is included below. If I do a simple range scan on small part of the pk of tab0  the planner chooses an index scan (pretty much as expected): test0=# EXPLAIN ANALYZE SELECT t0.id0 FROM tab0 AS t0 WHERE t0.id0 < 5;                                                         QUERY PLAN ---------------------------------------------------------------------------------------------------------------------------  Index Only Scan using tab0_pkey on tab0 t0  (cost=0.29..4.38 rows=5 width=4) (actual time=0.006..0.007 rows=5.00 loops=1)    Index Cond: (id0 < 5)    Heap Fetches: 0    Index Searches: 1    Buffers: shared hit=3  Planning:    Buffers: shared hit=65  Planning Time: 0.383 ms  Execution Time: 0.038 ms (9 rows) However joining it to tab1 changes this to a parallel seq scan: test0=# EXPLAIN ANALYZE SELECT t0.id0, t1.val FROM tab0 AS t0 JOIN tab1 AS t1 ON (t0.id0 = t1.id0) WHERE t0.id0 < 5; QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------------------  Gather  (cost=1097.61..42577.77 rows=5000 width=98) (actual time=0.324..7.486 rows=3500.00 loops=1)    Workers Planned: 1    Workers Launched: 1    Buffers: shared hit=1751    ->  Nested Loop  (cost=97.61..41077.77 rows=2941 width=98) (actual time=1.273..3.723 rows=1750.00 loops=2)          Buffers: shared hit=1751          ->  Parallel Seq Scan on tab0 t0 (cost=0.00..2375.29 rows=3 width=4) (actual time=1.247..3.232 rows=2.50 loops=2)                Filter: (id0 < 5)                Rows Removed by Filter: 49998                Buffers: shared hit=1640          ->  Bitmap Heap Scan on tab1 t1 (cost=97.61..12867.78 rows=3305 width=98) (actual time=0.031..0.148 rows=700.00 loops=5)                Recheck Cond: (t0.id0 = id0)                Heap Blocks: exact=97                Buffers: shared hit=111                ->  Bitmap Index Scan on tab1_id0_hash (cost=0.00..96.79 rows=3305 width=0) (actual time=0.024..0.024 rows=700.00 loops=5)                      Index Cond: (id0 = t0.id0)                      Index Searches: 5                      Buffers: shared hit=14  Planning:    Buffers: shared hit=160  Planning Time: 0.506 ms  Execution Time: 7.658 ms (22 rows) However, disabling seq scan gets back to the index scan again, and what looks to be a lower cost overall plan: test0=# SET enable_seqscan=off; SET Time: 0.133 ms test0=# EXPLAIN ANALYZE SELECT t0.id0, t1.val FROM tab0 AS t0 JOIN tab1 AS t1 ON (t0.id0 = t1.id0) WHERE t0.id0 < 5;  QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------------  Nested Loop  (cost=97.91..64508.51 rows=5000 width=98) (actual time=0.044..1.093 rows=3500.00 loops=1)    Buffers: shared hit=113    ->  Index Only Scan using tab0_pkey on tab0 t0 (cost=0.29..4.38 rows=5 width=4) (actual time=0.004..0.007 rows=5.00 loops=1)          Index Cond: (id0 < 5)          Heap Fetches: 0          Index Searches: 1          Buffers: shared hit=3    ->  Bitmap Heap Scan on tab1 t1  (cost=97.61..12867.78 rows=3305 width=98) (actual time=0.029..0.153 rows=700.00 loops=5)          Recheck Cond: (t0.id0 = id0)          Heap Blocks: exact=97          Buffers: shared hit=110          ->  Bitmap Index Scan on tab1_id0_hash (cost=0.00..96.79 rows=3305 width=0) (actual time=0.022..0.022 rows=700.00 loops=5)                Index Cond: (id0 = t0.id0)                Index Searches: 5                Buffers: shared hit=13  Planning:    Buffers: shared hit=9  Planning Time: 0.187 ms  Execution Time: 1.262 ms (19 rows) This is unexpected (to me anyway). This is 19devel from earlier this week, All parameters default. I note that I'm running on NVMe storage, and should ideally lower random_page_cost - but this does not materially effect the plan for these queries. To be fair, both possible plans are pretty fast, I am intrigued that what *looks* like a higher cost plan is being chosen! The Setup ------------- CREATE TABLE tab0 (   id0 SERIAL PRIMARY KEY,   val VARCHAR(100) ); CREATE TABLE tab1 (   id1 BIGSERIAL PRIMARY KEY,   val VARCHAR(100),   id0 INTEGER REFERENCES tab0(id0) ); -- Make 100,000 rows INSERT INTO tab0 (id0, val) SELECT generate_series(0,99999), 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; -- Make 10,000,000 rows INSERT INTO tab1 (id0, val) SELECT generate_series(0,9999999)/100, 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; -- Now make another 90,000,000 rows INSERT INTO tab1 (id0, val) SELECT id0, val FROM tab1 LIMIT 10000000; INSERT INTO tab1 (id0, val) SELECT id0, val FROM tab1 LIMIT 10000000; INSERT INTO tab1 (id0, val) SELECT id0, val FROM tab1 LIMIT 10000000; INSERT INTO tab1 (id0, val) SELECT id0, val FROM tab1 LIMIT 10000000; INSERT INTO tab1 (id0, val) SELECT id0, val FROM tab1 LIMIT 10000000; INSERT INTO tab1 (id0, val) SELECT id0, val FROM tab1 LIMIT 10000000; INSERT INTO tab1 (id0, val) SELECT id0, val FROM tab1 LIMIT 10000000; INSERT INTO tab1 (id0, val) SELECT id0, val FROM tab1 LIMIT 10000000; -- Create index on foreign key INSERT INTO tab1 (id0, val) SELECT id0, val FROM tab1 LIMIT 10000000; CREATE INDEX tab1_id0_hash ON tab1 USING HASH (id0); VACUUM; ANALYZE;