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 1wmTo5-000o5q-28 for pgsql-bugs@arkaria.postgresql.org; Wed, 22 Jul 2026 10:04:06 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wmTo4-00BKL6-37 for pgsql-bugs@arkaria.postgresql.org; Wed, 22 Jul 2026 10:04:04 +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 1wmBUz-007skU-0G for pgsql-bugs@lists.postgresql.org; Tue, 21 Jul 2026 14:31:08 +0000 Received: from mahout.postgresql.org ([2001:4800:3e1:1::227]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1wmBUv-00000001HQn-2ONJ for pgsql-bugs@lists.postgresql.org; Tue, 21 Jul 2026 14:31:07 +0000 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=postgresql.org; s=20171124; h=Message-ID:Date:Reply-To:Cc:From:To:Subject: Content-Transfer-Encoding:MIME-Version:Content-Type:Sender:Content-ID: Content-Description:In-Reply-To:References; bh=DqJgZnRTL/0XnJSgAVWcgSFCvDofEBnviB+52ddggqA=; b=P5BgVr6Op92S+p+7g8t+Se6yMI VbhbtOG6QZwGPHxGsl+lfYKmsNEaSQgM/vTZhxC63eZmMw4iCZ/EAc0AXLFfgzs1fqbsS1TQL1Xmu OZxzP7QO2nqvu5JYj+Rpu7JvxW7PQggjZwvI6NtnKlRTIGRu0lAS1pM27hkVQGTXs2VSa9jjvuPSr pdT7cuuJ4cvulfOUJe0QDchc84OUwdAQnqPdzV820xSoZSKQTWr31i+CNi0t1SKia/QuiZUlqDibV sR2GAs1nSjsRyOvsB9/ZimdLIYhN3s8TThaqITJl14fRRgMSQWxvrGv+aRo3pLsUd389JipP1qFGp uvvcbGUQ==; Received: from wrigleys.postgresql.org ([2a02:16a8:dc51::60]) by mahout.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wmBUu-001QPu-2f for pgsql-bugs@lists.postgresql.org; Tue, 21 Jul 2026 14:31:05 +0000 Received: from localhost ([127.0.0.1] helo=wrigleys.postgresql.org) by wrigleys.postgresql.org with esmtp (Exim 4.98.2) (envelope-from ) id 1wmBUt-00000001M2t-29lS for pgsql-bugs@lists.postgresql.org; Tue, 21 Jul 2026 14:31:03 +0000 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Subject: BUG #19564: Semantically redundant DISTINCT in an IN subquery changes the join strategy and improves execution t To: pgsql-bugs@lists.postgresql.org From: PG Bug reporting form Cc: 2320415112@qq.com Reply-To: 2320415112@qq.com, pgsql-bugs@lists.postgresql.org Date: Tue, 21 Jul 2026 14:30:47 +0000 Message-ID: <19564-f9f1c827f56c1261@postgresql.org> X-Auto-Response-Suppress: All Auto-Submitted: auto-generated List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk The following bug has been logged on the website: Bug reference: 19564 Logged by: cl hl Email address: 2320415112@qq.com PostgreSQL version: 17.10 Operating system: Linux LAPTOP-2SQAVLB0 6.6.87.2-microsoft-standard- Description: =20 ## Description This issue concerns two queries that differ only by an explicit `DISTINCT` inside an `IN` subquery. Duplicate values from an `IN` subquery cannot change membership semantics, so the two forms are logically equivalent. Despite this, PostgreSQL selects substantially different execution plans, and the form containing the redundant `DISTINCT` is approximately 20 times faster. ### Expected Behaviour PostgreSQL should recognize the duplicate-insensitive semantics of `IN` and consider the same efficient deduplicated or hashed semijoin strategies whether or not `DISTINCT` is written explicitly. The redundant keyword should not be required to obtain the better plan, and the two equivalent forms should have comparable execution time. ### Actual Behaviour The query without `DISTINCT` uses a `Nested Loop Semi Join`. Adding `DISTINCT` introduces `Sort -> Unique -> Hash` on the subquery result and changes the top-level operation to a `Hash Join`. This plan change reduces median execution time from 30.720 ms to 1.529 ms, a 20.0874x improvement. The difference remains present in both execution orders: | Measurement | Without DISTINCT | With DISTINCT | |---|---:|---:| | Average | 30.669 ms | 1.427 ms | | Median | 30.720 ms | 1.529 ms | | Original executed first | 18.5066x slower | baseline | | DISTINCT executed first | 31.9321x slower | baseline | The benchmark classified the result as order-stable. This is a query-plan quality/performance issue, not a result-correctness issue. ## How to repeat Run the following standalone SQL in a new PostgreSQL session. It creates all required objects and data, refreshes statistics, and executes both equivalent queries with runtime instrumentation. ```sql DROP TABLE IF EXISTS distinct_mre_outer; DROP TABLE IF EXISTS distinct_mre_inner; CREATE TABLE distinct_mre_outer ( v INTEGER NOT NULL ); CREATE TABLE distinct_mre_inner ( a INTEGER NOT NULL, b INTEGER NOT NULL, v INTEGER NOT NULL ); -- None of these values occur in the inner relation. This makes the semijoin -- inspect its complete inner input for every outer row. INSERT INTO distinct_mre_outer (v) SELECT 1000000 + g FROM generate_series(1, 1000) AS g; -- a and b are perfectly correlated. The predicate a=3D1 AND b=3D1 returns = 1,000 -- rows, although single-column statistics estimate approximately one row. INSERT INTO distinct_mre_inner (a, b, v) SELECT g % 1000, g % 1000, g FROM generate_series(1, 1000000) AS g; ANALYZE distinct_mre_outer; ANALYZE distinct_mre_inner; -- Original form: DISTINCT is absent because IN is duplicate-insensitive. EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS, TIMING OFF) SELECT COUNT(*) FROM distinct_mre_outer AS o WHERE o.v IN ( SELECT i.v FROM distinct_mre_inner AS i WHERE i.a =3D 1 AND i.b =3D 1 ); -- Semantically equivalent form with redundant DISTINCT. EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS, TIMING OFF) SELECT COUNT(*) FROM distinct_mre_outer AS o WHERE o.v IN ( SELECT DISTINCT i.v FROM distinct_mre_inner AS i WHERE i.a =3D 1 AND i.b =3D 1 ); ``` Both queries return `0`. On PostgreSQL 17.10 in the tested container, the first query produced: ```text Nested Loop Semi Join Rows Removed by Join Filter: 1000000 Execution Time: 40.369 ms ``` The query containing redundant `DISTINCT` produced: ```text Hash Join -> Hash -> Unique Execution Time: 10.849 ms ``` The minimized case is therefore approximately 3.72x faster with the redundant `DISTINCT`. Exact timings vary by host, but the plan difference is deterministic with the tested version and statistics.