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.94.2) (envelope-from ) id 1uvhJN-00HYF5-S9 for pgsql-performance@arkaria.postgresql.org; Mon, 08 Sep 2025 19:13:58 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.94.2) (envelope-from ) id 1uvhJM-0074eB-FE for pgsql-performance@arkaria.postgresql.org; Mon, 08 Sep 2025 19:13:56 +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.94.2) (envelope-from ) id 1uvhJM-0074e2-2z for pgsql-performance@lists.postgresql.org; Mon, 08 Sep 2025 19:13:56 +0000 Received: from mail-wm1-x331.google.com ([2a00:1450:4864:20::331]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.96) (envelope-from ) id 1uvhJI-001Fp0-2l for pgsql-performance@lists.postgresql.org; Mon, 08 Sep 2025 19:13:56 +0000 Received: by mail-wm1-x331.google.com with SMTP id 5b1f17b1804b1-45dcff2f313so29197135e9.0 for ; Mon, 08 Sep 2025 12:13:53 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=mattlong.org; s=google; t=1757358832; x=1757963632; darn=lists.postgresql.org; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:from:to:cc:subject:date:message-id:reply-to; bh=5cqvCIR631n4O/vvdx4B+vZ7QoqYnmlfCkCv6AlWNuw=; b=Fz4toPi5tRZdMFTcLXC2bnmYUTj8UKj34KL6pxbpoQuLiaXwwXmiTyTlEEranZca3G TmQZu3GDehwwOaj7lz9WD8dbpykBdmTZvG0udxwttZ6Tvi3SPDavWDyuWms4XP8aRoXt B2D6I3cfguaE8xgkkhQWlmUWrgj0tWY9ImR8Z8/w7QABYwkTAy9njq6zVOJHXO3KaN3/ uJDKN7ncQ9ZMXQ3ZH6Qm/WjGR6gyfPu6WWXcjWYj3XMXnNEJz8j3+VOEj4OMXYJjlzUE mD2cpQd8pmA/WYui/eMTl95jR0h94VqEWfBeEtNlGSdMVEhjGY+PkZfTfyVHpRrqnrY+ ZfGg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1757358832; x=1757963632; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to; bh=5cqvCIR631n4O/vvdx4B+vZ7QoqYnmlfCkCv6AlWNuw=; b=X0+L5pELyDs+7hihb+EdbVGuyKrK1uFuUAPAz+DhHp0vd2FA9P4ff12qNlz6oV1Bik kWkqiZw+jtH1VraTiuJE99fV72vh740Ifbb1uZyf7zyzPLSn31rpnx4Qj9IWJn9TU1Md yr9KcT3NZwcVKuWznsDW7a8fpii0BvQWIFrY96TAGcjhZrrs64a1hoPmQPf7pF2iWdBo cglqv13GoF+RlOmzPL8yYaVtynrTyl9c2NDCesvFa8zY3ce5v9fghD8hupicPEDnHFou cfOTXMNFvSZa7opw7XwOH41tltnIexHCU+aBM5hiuqW7GExKkWd/5BmIf49ww17N1oib zp6Q== X-Forwarded-Encrypted: i=1; AJvYcCUmDj173LQj+/K7kVksXj8DVBOGhlYAk0VmowowASfcjrcwdm+h63KMf4Q8P0/Oa6V9PTZaJ/kt5bxpyDIoGVSsoA==@lists.postgresql.org X-Gm-Message-State: AOJu0YwIF1kNT+cyyXNvkLjufI8tb34R4zBgcAsjmbUKiZFI+zA59wiO wW/KtAL85osKiHipTxrJFfZmNaweplWFGT1FxaVx5rBpOG44bO41SqiN49Zqg0Y0t6BAe6rH+F8 hT/miHMGjKWESVhyqkN4CtPTE0l1DzELFsIaPNi6yDst6gOnjRRHmZHo= X-Gm-Gg: ASbGncvBX2QpR9pBSgBuJvrlKYxVmj+buCMp6fg28hig6Lb++FEdrFccbwgaVP0xhlm Aut/nEs+uVqq01LlvvACqaSqqHJ+OPcmxbcsiBjFB0ebI1SZ+GaDw0+TrtMYfk8nXmiqheJK3ez dBK/vPrtLNqed//7BJLrDhUJqZbjSJrra2Lj66XMon8e3ugOhZGtohsAhJ811dRnkE2udwcaS78 tSId6TfJBnTpnlx X-Google-Smtp-Source: AGHT+IGwWhwJaQfFM4Ju629zfP3J7059kBlxmsZEeEJypwMEzAJ84ewHTJkODc+ulro6R1liGOP1STNtwr/QSRKhHOQ= X-Received: by 2002:a05:600c:5249:b0:45d:d68c:2a43 with SMTP id 5b1f17b1804b1-45de3c66dd3mr62977535e9.33.1757358832133; Mon, 08 Sep 2025 12:13:52 -0700 (PDT) MIME-Version: 1.0 References: <3e539c8b-c95a-4ba9-8462-04045b2da2b0@dalibo.com> <1751511.1749259794@sss.pgh.pa.us> <2194366.1752875645@sss.pgh.pa.us> In-Reply-To: <2194366.1752875645@sss.pgh.pa.us> From: Matt Long Date: Mon, 8 Sep 2025 12:13:41 -0700 X-Gm-Features: AS18NWB0ET17tOMapYVrZT8EP3RsDd5SG5MUnthyyaLPGPPbZyNH2EHzbTpg1Jw Message-ID: Subject: Re: Poor row estimates from planner, stat `most_common_elems` sometimes missing for a text[] column To: Tom Lane Cc: Mark Frost , "pgsql-performance@lists.postgresql.org" Content-Type: multipart/alternative; boundary="000000000000eebac8063e4efc63" List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --000000000000eebac8063e4efc63 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable Not to let perfect be the enemy of better, but we're facing a variant of this issue that would not be addressed by the proposed patch. If you're interested, the real world use case is described in https://github.com/medplum/medplum/issues/7310. Rows have an array consisting of one common element present in every row and one unique element. With a statistics target at the default 100, this pushes the threshold for vastly overestimated row counts for the unique elements down to 7,937 rows. In this case, the effects of the proposed patch are not applied since the most_common_elems array is not empty. I'm not a statistician, so maybe this wouldn't be valid, but it seems like using the highest frequency of an element that did not qualify for the mce list instead of the 0.5% default frequency could be an elegant, but more invasive solution. Simple repro of the problem: CREATE TABLE public.test( id integer, tags text[] ); INSERT INTO public.test (id, tags) SELECT n, ARRAY['common', TO_CHAR(n,'fm00000000')] FROM ( SELECT generate_series(1,7_937) AS n ); ANALYZE public.test; SELECT most_common_elems, most_common_elem_freqs FROM pg_stats WHERE schemaname =3D 'public' AND tablename =3D 'test' AND attname =3D 'tags'; EXPLAIN (ANALYZE,BUFFERS,VERBOSE) SELECT * FROM test WHERE tags && ARRAY['common']; EXPLAIN (ANALYZE,BUFFERS,VERBOSE) SELECT * FROM test WHERE tags && ARRAY['00000001']; With 7,936 rows: most_common_elems has 1,000 entries and looks like {00000001,00000003,00000009,...,common} most_common_elem_freqs has 1,003 entries and looks like {0.00012600806,0.00012600806,0.00012600806,...,1,0.00012600806,1,0} With 7,937+ rows: most_common_elems is {common} most_common_elem_freqs is {1,1,1,0} On Mon, Sep 8, 2025 at 11:27=E2=80=AFAM Tom Lane wrote: > I wrote: > > Well, we don't have a most common element in this scenario --- the > > whole point is that the occurrence counts resulting from the lossy > > counting algorithm are too low to be trustworthy. However, what we > > do have is the cutoff frequency, and it seems to me that we could use > > that as the estimate of the maximum frequency of the non-MCEs. > > Here's a less crude patch for that. The array_typanalyze.c changes > are the same as before, but I reconsidered what to do about this > stumbling block: > > > Assuming we like this direction, the main thing that makes this a hack > > not a polished patch is that I had to strongarm the code into storing > > a zero-length values array. What update_attstats would normally do > > is leave the values column of the MCELEM stats slot NULL, which then > > causes get_attstatsslot to throw a that-shouldn't-be-null error. > > An alternative answer is to change get_attstatsslot to allow a null, > > but I'm not sure that that's any cleaner. Either way it seems like > > there's a hazard of breaking some code that isn't expecting the case. > > I concluded that letting get_attstatsslot accept nulls is too risky; > there is probably code that assumes that get_attstatsslot would've > rejected that. Instead, this version changes update_attstats' rule > for when to store an array rather than a null. Now it will do so > if the passed-in stavalues pointer isn't null, even if numvalues > is zero. I think that this doesn't change the outcome for any > existing analyze routines because they wouldn't pass a data pointer > if they have no values; and even if it does, storing an empty array > not a null seems like it should be pretty harmless. > > > An alternative that feels cleaner but a good bit more invasive > > is to get rid of the convention of storing the min/max/nulls > > frequencies as extra entries in the MCELEM numbers entry --- > > which surely is a hack too --- and put them into some new slot > > type. I'm not sure if that's enough nicer to be worth the > > conversion pain. > > A year ago I would have seriously considered doing it that way. > But now that we have code to dump-n-restore stats, that code would > have to be adjusted to convert the old representation. It's not > worth it for this case. > > Hence, v1 attached, now with a commit message. > > regards, tom lane > > --000000000000eebac8063e4efc63 Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable
Not to let perfect be the enemy of better, but we're f= acing a variant of this issue that would not be addressed by the proposed p= atch. If you're interested, the real world use case is described in https://github.com/= medplum/medplum/issues/7310. Rows have an array consisting of one commo= n element present in every row and one unique element. With a statistics ta= rget at the default 100, this pushes the threshold for vastly overestimated= row counts for the unique elements down to 7,937 rows.

= In this case, the effects of the proposed patch are not applied since the m= ost_common_elems array=C2=A0is not empty. I'm not a statistician, so ma= ybe this wouldn't be valid, but it seems like using the highest frequen= cy of an element that did not qualify for the mce list instead of the 0.5% = default frequency could be an elegant, but more invasive solution.=C2=A0

Simple repro of the problem:

CREATE TABLE pub= lic.test(
=C2=A0 =C2=A0 id integer,
=C2=A0 =C2=A0 tags text[]
);

INSERT INTO public.test (id, tags)
=C2=A0 =C2=A0 = =C2=A0 =C2=A0 SELECT n, ARRAY['common', TO_CHAR(n,'fm00000000&#= 39;)] FROM ( SELECT generate_series(1,7_937) AS n );

ANALYZE public.test;

SELECT most_common_elems, most_common_el= em_freqs FROM pg_stats WHERE schemaname =3D 'public' AND tablename = =3D 'test' AND attname =3D 'tags';

EXPLAIN (ANALYZE,= BUFFERS,VERBOSE)
=C2=A0 =C2=A0 =C2=A0 =C2=A0 SELECT * FROM test WHERE ta= gs && ARRAY['common'];

EXPLAIN (ANALYZE,BUFFERS,VERB= OSE)
=C2=A0 =C2=A0 =C2=A0 =C2=A0 SELECT * FROM test WHERE tags &&= ; ARRAY['00000001'];

With 7,936 rows:<= /div>
=C2=A0 =C2=A0 most_common_elems has 1,000 entries and looks like = {00000001,00000003,00000009,...,common}
=C2=A0 =C2=A0 most_common= _elem_freqs has 1,003 entries and looks like {0.00012600806,0.00012600806,0= .00012600806,...,1,0.00012600806,1,0}

With 7,937+ = rows:
=C2=A0 =C2=A0 most_common_elems is {common}
=C2= =A0 =C2=A0 most_common_elem_freqs is {1,1,1,0}

On Mon, Sep 8, 2025 at 11:27=E2=80=AFAM Tom Lane <tgl@sss.pgh.pa.us> wrote:
I wrote:
> Well, we don't have a most common element in this scenario --- the=
> whole point is that the occurrence counts resulting from the lossy
> counting algorithm are too low to be trustworthy.=C2=A0 However, what = we
> do have is the cutoff frequency, and it seems to me that we could use<= br> > that as the estimate of the maximum frequency of the non-MCEs.

Here's a less crude patch for that.=C2=A0 The array_typanalyze.c change= s
are the same as before, but I reconsidered what to do about this
stumbling block:

> Assuming we like this direction, the main thing that makes this a hack=
> not a polished patch is that I had to strongarm the code into storing<= br> > a zero-length values array.=C2=A0 What update_attstats would normally = do
> is leave the values column of the MCELEM stats slot NULL, which then > causes get_attstatsslot to throw a that-shouldn't-be-null error. > An alternative answer is to change get_attstatsslot to allow a null, > but I'm not sure that that's any cleaner.=C2=A0 Either way it = seems like
> there's a hazard of breaking some code that isn't expecting th= e case.

I concluded that letting get_attstatsslot accept nulls is too risky;
there is probably code that assumes that get_attstatsslot would've
rejected that.=C2=A0 Instead, this version changes update_attstats' rul= e
for when to store an array rather than a null.=C2=A0 Now it will do so
if the passed-in stavalues pointer isn't null, even if numvalues
is zero.=C2=A0 I think that this doesn't change the outcome for any
existing analyze routines because they wouldn't pass a data pointer
if they have no values; and even if it does, storing an empty array
not a null seems like it should be pretty harmless.

> An alternative that feels cleaner but a good bit more invasive
> is to get rid of the convention of storing the min/max/nulls
> frequencies as extra entries in the MCELEM numbers entry ---
> which surely is a hack too --- and put them into some new slot
> type.=C2=A0 I'm not sure if that's enough nicer to be worth th= e
> conversion pain.

A year ago I would have seriously considered doing it that way.
But now that we have code to dump-n-restore stats, that code would
have to be adjusted to convert the old representation.=C2=A0 It's not worth it for this case.

Hence, v1 attached, now with a commit message.

=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 regards, tom lane

--000000000000eebac8063e4efc63--