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 1wi7Vi-0001WS-0F for pgsql-hackers@arkaria.postgresql.org; Fri, 10 Jul 2026 09:27: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 1wi7Ve-00H7K0-1Y for pgsql-hackers@arkaria.postgresql.org; Fri, 10 Jul 2026 09:27:03 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wi7Ve-00H7Jr-0O for pgsql-hackers@lists.postgresql.org; Fri, 10 Jul 2026 09:27:02 +0000 Received: from mail-wr1-x429.google.com ([2a00:1450:4864:20::429]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1wi7Vb-00000000d3v-3yi0 for pgsql-hackers@lists.postgresql.org; Fri, 10 Jul 2026 09:27:02 +0000 Received: by mail-wr1-x429.google.com with SMTP id ffacd0b85a97d-4799b3f7c83so544134f8f.2 for ; Fri, 10 Jul 2026 02:27:00 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cybertec.at; s=google; t=1783675619; x=1784280419; darn=lists.postgresql.org; h=mime-version:user-agent:content-transfer-encoding:content-type :references:in-reply-to:date:to:from:subject:message-id:from:to:cc :subject:date:message-id:reply-to:content-type; bh=v5T+vHREHyX+TjeTdSL9SXtcDM9tIfXENxyU8uyPx0g=; b=Smzzt/HQxIimdll+HuQetaJcYnsSJ2fJnTON8AN27QfkcgJPgLF0XXPK/w5SnqJW4E GpWmKTt2PSPC8mJeOz8As45foH4jadEk4j1Nt0Sh9wBuiquo+ch1hKLmibld4lXl1KCy dLaU+N7w2FJlMulHWmsrXPo7ucdvpGDVuXMC6Zfp9Rcbts2wmkaACa7hsfX2vmU6GaMb ueJ9Y8EWlyi2Q8kg/GjwYI4JDvB/j6m9iIdSRRiKDqLvALMqn/QPpN5E41U3RlJFxoQZ pxZzEQJAXTu9Z/3kKI2QsX6hK0zX/2gzV53y5BmdE3QAvcldiHr8gvUt7aR/MRhaICqU zmwA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1783675619; x=1784280419; h=mime-version:user-agent:content-transfer-encoding:content-type :references:in-reply-to:date:to:from:subject:message-id:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to :content-type; bh=v5T+vHREHyX+TjeTdSL9SXtcDM9tIfXENxyU8uyPx0g=; b=LtAgGreO9iH4Rq5LZYrEOrLwqxfL0utdk1goVzgBf5AKIRfrUo6B5s3upqy3TZkRAA YR73OgOJTZ16wqDwKfl8m59u3mL/Dti7pXaHeKojLzFJgyEwcYnfCoSmJppFw458y7/U mRLNEn1L9GX+KfP7jBNa06Qsnl5ldYG5ADrud6nLKOaZrvxWuNE0N0U9n/WyD4EFyje1 LNazB6yWKmdg9rYkycEuG9Uc1kDoeRy9L/w2USRhC4Be2yTrVGeXnzBwqHt4LuLWLTXT c4qqayohY3TWgQS48BS+rZYmr4FYIuqQ5jipvk7ojgvZvZYtrtxqxmkfIq/QaSQWzY8N 9bqg== X-Forwarded-Encrypted: i=1; AHgh+RoPK8L1vdD4Qe34ckwUN5Mt9STTbsT1RyR44NfxGEE5VmJFKt1YbMK9+ZpH7nhiNrZRHTLamdn6ZqP3bndD@lists.postgresql.org X-Gm-Message-State: AOJu0YzRcPfbgEp/5kVujzBnQgenlsI2Sz8d/VFxQKdPdjMC4lQnboUr +JjYhVif48Xukcx6UWLFJaeC0cMZBKbW7fNE9zo3rS1yfyZaEhiQ184F8Eds+x2pjGM= X-Gm-Gg: AfdE7cllI1+X9YBVzfb7VsjfpUJJ4wUeh+KNyn4f13Eow1nKqWpUQPb40H0wnnVVxc6 PFIgSgg3HVs7LzDz52FDBS5VSY0/bg8xhXtISRYeElS1eDK4XyMI5EyIhjVIVP5/QTvpxsB7cZj 4dWRpt0hSh1q0JOGzEX+beCt7C3icgz1/Zm7eU5zJT4rLMHzhBjAr6DIabnAwK0yA9zQ0WZ3IG8 LEWbMzG1GGHIF3S4in9lHDomY4rwHEmEToN6vf5bfRcJBPA/N3UaGOrtU1ha+ywnBXZLcji26Kl TWI/Prb5Ye8bcyGbeOKumicStmNlRQpw1r3/NteLNPlYPVlLnHiNf8l/tVBYsvOhc3ehnjodM/r fLarNRSlSWC3GvUPpsBdGnzM4bUxa7gfoqL1zXsxklDu+vZVSO+Q7xqkXfFb5b9LwiUm5+Q2cbP oi6/nSfFNuI7CCA7Puw+RU3rJKC6Vz3ePBeBooP+gHDw== X-Received: by 2002:a05:6000:2385:b0:474:13ac:a91b with SMTP id ffacd0b85a97d-47df078957amr10648506f8f.32.1783675619081; Fri, 10 Jul 2026 02:26:59 -0700 (PDT) Received: from laurenz.albe-K4N0CV00F97414D ([2001:871:70:cf80:2440:b21a:5a20:983e]) by smtp.gmail.com with ESMTPSA id ffacd0b85a97d-47a9e4d83bdsm58605777f8f.13.2026.07.10.02.26.58 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Fri, 10 Jul 2026 02:26:58 -0700 (PDT) Message-ID: <3f423bd9038afd4d38d8b916672055ad6c46ed2c.camel@cybertec.at> Subject: Re: COALESCE patch From: Laurenz Albe To: prankware , pgsql-hackers@lists.postgresql.org Date: Fri, 10 Jul 2026 11:26:58 +0200 In-Reply-To: References: Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable User-Agent: Evolution 3.58.3 (3.58.3-1.fc43) MIME-Version: 1.0 List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk On Tue, 2026-06-30 at 16:48 +0300, prankware wrote: > The planner ignores column statistics when an equality has a COALESCE > expression on one side. For a clause like COALESCE(a, b) =3D $1, or a joi= n on > COALESCE(t1.a, t1.b) =3D COALESCE(t2.c, t2.d), there are no statistics on= the > COALESCE node itself, so eqsel() and eqjoinsel() return the generic 0.005 > estimate while the per-column stats for a, b, c and d sit unused. The onl= y way > around this today is an expression index or extended statistics on that e= xact > expression, which doesn't scale across many different COALESCE clauses. > estimate_hash_bucket_stats() has the same gap: a COALESCE hash key gets a > default ndistinct and therefore a default bucket size. Since these expres= sions > are common in joins and filters over nullable or fallback columns, the de= fault > estimate can be far enough off to flip the join order or join method. > > The idea is to estimate straight from the existing per-column stats, with= no > extra statistics object. COALESCE(arg_1, ..., arg_n) returns arg_i only w= hen > arg_1 .. arg_{i-1} are all NULL, so the chance of reaching branch i is th= e > product of stanullfrac over the earlier branches. Selectivity of > COALESCE(l_1..l_M) =3D COALESCE(r_1..r_N) is then the sum over branch pai= rs of > P(reach l_i) * P(reach r_j) * sel(l_i =3D r_j), and each sel(l_i =3D r_j)= is a > recursive call back into eqsel()/eqjoinsel(). A non-COALESCE side is trea= ted as > a one-branch list, so scalar COALESCE(a, b) =3D const falls out of the sa= me code, > and the same decomposition feeds estimate_hash_bucket_stats(). If any bra= nch is > missing stats, the code bails and today's behavior is unchanged. > > Feedback is welcome. I think the idea is good, and the performance cost is incurred only when coalesce() expressions are present. I am a bit worried about the execution time for queries that join two tables over lengthy coalesce clauses, as the cost is O(n*m) because of the sum. But I think that such queries are extre= mely rare, so I don't worry too much. I found that the estimates are good if I use expressions like "coalesce(col1, col2)" in my query, but the estimates are as bad as before with the common case of "coalesce(col, constant)": CREATE TABLE b (col1 integer); /* three quarters NULL, the rest evenly distributed */ INSERT INTO b SELECT CASE WHEN random() >=3D 0.75 THEN random() * 1000 + 1 END FROM generate_series(1, 10000); VACUUM (ANALYZE) b; /* force a hash join regardless of the estimates */ SET work_mem =3D '512MB'; SET enable_mergejoin =3D off; SET enable_nestloop =3D off; EXPLAIN (ANALYZE, SUMMARY OFF, BUFFERS OFF) SELECT * FROM b AS b1 JOIN b AS b2 ON coalesce(b1.col1, 0) =3D coalesce(b2.col1, 0); Hash Join (... rows=3D500000 ...) (actual ... rows=3D55125006.00 ...) Hash Cond: (COALESCE(b1.col1, 0) =3D COALESCE(b2.col1, 0)) -> Seq Scan on b b1 (... rows=3D10000 ...) (actual ... rows=3D10000.00= ..) -> Hash (... rows=3D10000 ...) (actual ... rows=3D10000.00 ...) Buckets: 16384 Batches: 1 Memory Usage: 451kB -> Seq Scan on b b2 (... rows=3D10000 ...) (actual ... rows=3D10= 000.00 ...) EXPLAIN (ANALYZE, SUMMARY OFF, BUFFERS OFF) SELECT * FROM b AS b1 JOIN b AS b2 ON coalesce(b1.col1, 0) =3D coalesce(b2.col1, 1); Hash Join (... rows=3D500000 ...) (actual ... rows=3D16654.00 ...) Hash Cond: (COALESCE(b1.col1, 0) =3D COALESCE(b2.col1, 1)) -> Seq Scan on b b1 (... rows=3D10000 ...) (actual ... rows=3D10000.00= ...) -> Hash (... rows=3D10000 ...) (actual ... rows=3D10000.00 ...) Buckets: 16384 Batches: 1 Memory Usage: 451kB -> Seq Scan on b b2 (... rows=3D10000 ...) (actual ... rows=3D10= 000.00 ...) EXPLAIN (ANALYZE, SUMMARY OFF, BUFFERS OFF) SELECT * FROM b WHERE coalesce(col1, 0) =3D 0; Seq Scan on b (... rows=3D40 ...) (actual ... rows=3D7424.00 ...) Filter: (COALESCE(col1, 0) =3D 0) Rows Removed by Filter: 2576 EXPLAIN (ANALYZE, SUMMARY OFF, BUFFERS OFF) SELECT * FROM b WHERE coalesce(col1, 1) =3D 0; Seq Scan on b (... rows=3D40 ...) (actual ... rows=3D0.00 ...) Filter: (COALESCE(col1, 1) =3D 0) Rows Removed by Filter: 10000 I think that the patch would be much more useful if it could improve such estimates. Yours, Laurenz Albe