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 1wi1E9-000XTM-04 for pgsql-hackers@arkaria.postgresql.org; Fri, 10 Jul 2026 02:44:33 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wi1E5-00FV3e-1k for pgsql-hackers@arkaria.postgresql.org; Fri, 10 Jul 2026 02:44:30 +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 1wi1E4-00FV3W-2y for pgsql-hackers@lists.postgresql.org; Fri, 10 Jul 2026 02:44:29 +0000 Received: from mail-lf1-x12d.google.com ([2a00:1450:4864:20::12d]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1wi1E3-00000000PRC-2Cau for pgsql-hackers@lists.postgresql.org; Fri, 10 Jul 2026 02:44:28 +0000 Received: by mail-lf1-x12d.google.com with SMTP id 2adb3069b0e04-5aeb77b3afbso422814e87.1 for ; Thu, 09 Jul 2026 19:44:27 -0700 (PDT) ARC-Seal: i=1; a=rsa-sha256; t=1783651465; cv=none; d=google.com; s=arc-20260327; b=mcYSEfwPaCWFvo8D5rH+OrMwpVaMMNLbW2CHSZzdPZX2hrfB54D7EdGMDWU9HnFA10 lhDJelITP76CRzbyGzOB96dq+bKPc6gaGoZ1fPAIHRnwUjSKZkvJu+fJ+GClxjgk/WEd ZXeVS9ag2LAPx8vgzCQr7eheZrkkZnwA+fwKHUwqd2QWUefcYPmUa0RX8gPmK2p+tSP4 PTrwh4H+v/7Bb2GYppLtJC36W1fKq/R2H/uhkxNm7n8qYfGLgWcchBzxIwa20NJYXED2 SBA8um9RIFeHf+9uMG1Kd0K0IadW3ns/FXKZCHaL3S9e5yjerKJK5HLI9aEpD8/SANfu 0REQ== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20260327; h=content-transfer-encoding:cc:to:subject:message-id:date:from :in-reply-to:references:mime-version:dkim-signature; bh=fSTaBeF2QZmfNKTDQ4cKI+/MqK9p7xXP2jW9MmODlPY=; fh=ZMRay1VlzwCgVKTLkndEpJjjKQYGdJachlHsegHhjCA=; b=K2m0UyeB0YoL7hb+Z8JXCaIcDHAy8qgkegoVXoyWGaqhJshs9eMHcplZ8XaDtY0e9G DHxu7dLx1eOkmLQrV+ArfuKppSuswa6dkJPvthS0APt021biNuVcFV4hYscHHsNMwFL9 hBjwzReCSGDnuV/qCY+TWw7rYe45pkwye1f6gC41sezRjwScWS/GSlql9cJf4i4/sppj mOmMtj+F65iFGkjWE8wm/3wUO4YH1HJj88EaP7fajNDsebyOPWayk4HBeIqqvsvzS2WF 8g+VSqy0gREx5P+2Xz2ZFtFPyaxT0vPUIG/+1ur1rNVJFzKJ2Qn8fkxJTDFSXxgqGm9B tXkA==; darn=lists.postgresql.org ARC-Authentication-Results: i=1; mx.google.com; arc=none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1783651465; x=1784256265; darn=lists.postgresql.org; h=content-transfer-encoding:content-type:cc:to:subject:message-id :date:from:in-reply-to:references:mime-version:from:to:cc:subject :date:message-id:reply-to:content-type; bh=fSTaBeF2QZmfNKTDQ4cKI+/MqK9p7xXP2jW9MmODlPY=; b=pOFQqu0MrcD3VowLqxzhMWfwLfbyU4+iRfNcmeUV6YFqidRBGuJwaTGE5w2PAW2tKW nPUHIzGQe+8XkIwJKqri7hpS5EXxxSRwn83ctbE6Bzf+ki5cjD+iSmNiP1OzL9Yr+5+u V1A/WqfIJWyi4/S4njc1M1d3ZQbkW1rs3u+tBLYajayZ1Dsw4zGVJQv4aY2t7KPzgSKm xziwmQA9ubD9mUTHoOd+uSHcM/GMchIGsRDRBB59WYraRIW5wnTvwXoL+euPUfnBpoak yMR5WSiGyZvIgrGnS2SgvGOMnDnIQ6s2I9dHxhi+E3FQk0aInTflcYCaaiLB8S41Ayv+ RM3Q== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1783651465; x=1784256265; h=content-transfer-encoding:content-type:cc:to:subject:message-id :date:from:in-reply-to:references:mime-version:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to :content-type; bh=fSTaBeF2QZmfNKTDQ4cKI+/MqK9p7xXP2jW9MmODlPY=; b=b8nV7KjIuCjeKkJlVVd9eHwyGRQd9GsQtxMwFpE2o4rG6XJgwUasCYrzHdpF2DdE/Q 4YZL+qXeelPlbMCY1vwePUcbZQJUVtaFKQyxzYfmQA6m/8szq6QLNlFy/Q3fbrRICZN1 Rscoz9XzGTjN0cZAtAgRJEuajoQRLxweBvBXgpdh0luCz8DruNHxVpEGTGCu28J4y9m4 agye4kuyhgozZ2K62aAgX0SNZRnHxmQbKR2X/jFwwhTVmLEHD22rR0Ti1IAP9F2DxdqQ FBaZgGPFUQOBsxBveZoZpFlIr/Tb5T3PcD4v8DhDngOyX2kuUm4k+GPxl1XAsiB0Aden 2+8w== X-Gm-Message-State: AOJu0YzX4OQHkd1K3Mlm9zRX+ide7ywvvsCNatBtfetg7ICVAF5zYZra 6DW/XCRQeVBaGpwnkKe4rSI4adAAYfCR258GgXNc85WIwIH54UB3Xt7fQK5zmcwuMOUdqDSDnnN 3ial3xmfGm6kZQfl8ybCpsp1WnSRqmN1B9S7TkaFG2Q== X-Gm-Gg: AfdE7cm4HPFH+cqQZalOZC4Zk748DdqNSp7sNS9hAkLJ52CkxVq75zF/KVsfLFuG/EN n1BSbWRIJa7yR0CmXWe8dznTslHGnbMDuFtg21wFqo0aUU6CkuBABFlsMdJFvHF/oNisVuKxuAa qYPMi6O7wgcVWgoE+VVFXqTypTt1Z2C0wegtDFLIGqiYT2v31yJE++122g5T6i0PNkyc0GND3jH eroY0QYJO0bTKtSSge4hZKP4t+LZuPpvRjCnK8s40J1Igeayu0dB6S0qC4wMwuF5q0WunTbYK8H 1cihndzFDIb9LGUc+pZ9d2/ne8P2Ao2dvwsaC9qzfXUheNraqf9ej6AbiGixbfdM7f7utjBv X-Received: by 2002:a05:6512:66d7:10b0:5ae:baa7:5ed2 with SMTP id 2adb3069b0e04-5b0114b9155mr1743652e87.63.1783651464223; Thu, 09 Jul 2026 19:44:24 -0700 (PDT) MIME-Version: 1.0 References: <5D2C9081-5DFE-4E27-AB14-7358238EA1BC@gmail.com> In-Reply-To: From: Ewan Young Date: Fri, 10 Jul 2026 10:44:12 +0800 X-Gm-Features: AUfX_mwciJNAkw4X2HJInID36ffonwIoO_b77dpBsZf7SFUKhuUHmS1WhTPn66A Message-ID: Subject: Re: Fix duplicate detection for null-treatment window functions To: Chao Li Cc: PostgreSQL-development , Tatsuo Ishii Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk Hi Chao, On Thu, Jul 9, 2026 at 8:35=E2=80=AFPM Chao Li wro= te: > > > > > On Jul 7, 2026, at 08:14, Chao Li wrote: > > > > Hi, > > > > I just spotted an oversight from =E2=80=9C[25a30bbd4] Add IGNORE NULLS/= RESPECT NULLS option to Window functions=E2=80=9D. > > > > In ExecInitWindowAgg(), there is logic to detect duplicate functions: > > ``` > > if (i <=3D wfuncno && wfunc->ignore_nulls =3D=3D perfunc[i].ignore_null= s) > > { > > /* Found a match to an existing entry, so just mark it */ > > wfuncstate->wfuncno =3D i; > > continue; > > } > > ``` > > > > However, when appending function info to perfunc, ignore_nulls is not c= opied: > > ``` > > /* Fill in the perfuncstate data */ > > perfuncstate->wfuncstate =3D wfuncstate; > > perfuncstate->wfunc =3D wfunc; > > perfuncstate->numArguments =3D list_length(wfuncstate->args); > > perfuncstate->winCollation =3D wfunc->inputcollid; > > ``` > > > > As a result, wfunc->ignore_nulls =3D=3D perfunc[i].ignore_nulls can nev= er be true for duplicate IGNORE NULLS or explicit RESPECT NULLS calls. This= means duplicate detection doesn't work for those calls. This bug is easy t= o prove by adding temporary logs, and the fix is straightforward: copy igno= re_nulls when filling in the perfuncstate data. > > > > This is a simple repro: > > > > Without the fix: > > ``` > > evantest=3D# WITH t(x) AS (VALUES (NULL::int), (1), (2)) > > evantest-# SELECT first_value(x) IGNORE NULLS OVER w AS a, > > evantest-# first_value(x) IGNORE NULLS OVER w AS b > > evantest-# FROM t > > evantest-# WINDOW w AS (ORDER BY x NULLS FIRST > > evantest(# ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED = FOLLOWING); > > INFO: WindowAgg duplicate cache miss: no previous match, ignore_nulls = 1 > > INFO: WindowAgg duplicate cache miss: matched wfuncno 0, ignore_nulls = 1, cached ignore_nulls 0 > > a | b > > ---+--- > > 1 | 1 > > 1 | 1 > > 1 | 1 > > (3 rows) > > ``` > > > > With the fix: > > ``` > > evantest=3D# WITH t(x) AS (VALUES (NULL::int), (1), (2)) > > evantest-# SELECT first_value(x) IGNORE NULLS OVER w AS a, > > evantest-# first_value(x) IGNORE NULLS OVER w AS b > > evantest-# FROM t > > evantest-# WINDOW w AS (ORDER BY x NULLS FIRST > > evantest(# ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED = FOLLOWING); > > INFO: WindowAgg duplicate cache miss: no previous match, ignore_nulls = 1 > > INFO: WindowAgg duplicate cache hit: existing wfuncno 0, ignore_nulls = 1 > > a | b > > ---+--- > > 1 | 1 > > 1 | 1 > > 1 | 1 > > (3 rows) > > ``` > > > > See the attached patch for details. The actual fix is only one line. Th= e INFO logs above were produced with temporary debug logs added around the = duplicate-function lookup, those logs are not part of the proposed fix. I l= eft those logs in the patch with TODO comments only so reviewers can see th= e behavior before and after the fix. They should be removed before pushing. > > > > Best regards, > > -- > > Chao Li (Evan) > > HighGo Software Co., Ltd. > > https://www.highgo.com/ > > > > > > > > > > > > I realized that leaving the temp log code in the patch is not friendly to= the CF test. So, splitting the temp log part into a diff file. Nice catch, and v2 does restore the intended de-duplication. While reviewing it, though, I think the check the patch repairs is actually redundant, and the real issue is that the code (both the original commit an= d the patch) keeps a shadow copy of ignore_nulls that has to be maintained by hand. WindowFunc.ignore_nulls is a plain scalar field with no pg_node_attr, so equal() already compares it -- the generated _equalWindowFunc() has: COMPARE_SCALAR_FIELD(winagg); COMPARE_SCALAR_FIELD(ignore_nulls); COMPARE_LOCATION_FIELD(location); That means the equal() call in the dedup loop already distinguishes two WindowFuncs that differ only in null treatment: for (i =3D 0; i <=3D wfuncno; i++) { if (equal(wfunc, perfunc[i].wfunc) && !contain_volatile_functions((Node *) wfunc)) break; } if (i <=3D wfuncno && wfunc->ignore_nulls =3D=3D perfunc[i].ignore_null= s) If ignore_nulls differs, equal() returns false, the loop never breaks on that entry, and we never reach the extra term. If equal() matches, then ignore_nulls necessarily matched too. So "&& wfunc->ignore_nulls =3D=3D perfunc[i].ignore_nulls" can never change the outcome, and WindowStatePerFuncData.ignore_nulls exists only to feed it -- it is read only there and, before this patch, written nowhere (which is exactly why it was always 0). So rather than populating the field, I'd suggest dropping the redundant ter= m and the field, and letting equal() do the work: and the field, and letting equal() do the work: - if (i <=3D wfuncno && wfunc->ignore_nulls =3D=3D perfunc[i].ignore_null= s) + if (i <=3D wfuncno) { /* Found a match to an existing entry, so just mark it */ wfuncstate->wfuncno =3D i; continue; } plus removing the ignore_nulls member from WindowStatePerFuncData and trimming the now-stale "which needs the same ignore_nulls value" comment. That fixes the same bug while removing the duplicated state that caused it, so it can't silently drift again. The one argument for an explicit check is defensiveness: if someone later tags ignore_nulls with a pg_node_attr that excludes it from equal(), the loop would start collapsing functions with different null treatment. If that's a worry, the robust form compares the stored node directly instead o= f a shadow copy, and still needs no separate field: if (i <=3D wfuncno && wfunc->ignore_nulls =3D=3D perfunc[i].wfunc->ignore_nulls) Given ignore_nulls has to stay significant to equal() anyway (two calls wit= h different null treatment really are different functions), I'd lean toward just removing the check. Happy to send a patch along these lines if you agree. > > Best regards, > -- > Chao Li (Evan) > HighGo Software Co., Ltd. > https://www.highgo.com/ > > > > --=20 Regards, Ewan Young