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 1uyt2x-00E6P7-BY for pgsql-performance@arkaria.postgresql.org; Wed, 17 Sep 2025 14:22:11 +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 1uyt2v-007z8D-5N for pgsql-performance@arkaria.postgresql.org; Wed, 17 Sep 2025 14:22:09 +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.94.2) (envelope-from ) id 1uyt2u-007z4n-Lh for pgsql-performance@lists.postgresql.org; Wed, 17 Sep 2025 14:22:09 +0000 Received: from mail-ej1-x62a.google.com ([2a00:1450:4864:20::62a]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.96) (envelope-from ) id 1uyt2t-000uyZ-0M for pgsql-performance@lists.postgresql.org; Wed, 17 Sep 2025 14:22:08 +0000 Received: by mail-ej1-x62a.google.com with SMTP id a640c23a62f3a-b07883a5feeso1167569866b.1 for ; Wed, 17 Sep 2025 07:22:07 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cybertec.at; s=google; t=1758118926; x=1758723726; darn=lists.postgresql.org; h=mime-version:user-agent:content-transfer-encoding:references :in-reply-to:date:cc:to:from:subject:message-id:from:to:cc:subject :date:message-id:reply-to; bh=//kVI/mvDb8Kr6kOx3fJkco9v3MxNraXK3K0BOo9tAA=; b=oFnuAFD2ag3qPd5C+cNUAFMzb9YpBAM8YHByA5JqI+IK8+vO0wo4xd0cfa91jmpOxu l00N5baxjx/XpRb+4ND9vGaAFx+8jSccN2dU+HV6gctTaQ7DjjD110qEmhA7zBNlIyey iNLtA8EophZ+pgSAtVGjr3sYBXYm8ZBtBa2Xeq8N40nKlx4r+QqPkrBrvQDjm+MYOKWN 7+3bs7A18UJBmSwAygV4mEQrwIwVPHgTQcL5rN8v6o8I1Cd+P9uuk6vq7Q4i9ykogohr fAG58isS0z1LpEg44u6hhmA9mx/HnV0iBp1L5ct+vGAsSB//WkGcEsI2kUtXHWx8Jlu/ ABbw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1758118926; x=1758723726; h=mime-version:user-agent:content-transfer-encoding:references :in-reply-to:date:cc:to:from:subject:message-id:x-gm-message-state :from:to:cc:subject:date:message-id:reply-to; bh=//kVI/mvDb8Kr6kOx3fJkco9v3MxNraXK3K0BOo9tAA=; b=bZyiVcU9I0JSSWSa0ZA3uXP0Zokm86noNfOvny7cNDezD7C9edemZzmBD6n/QS+DCM TNEEavhkeJvwnkI0BJZzvjLWbr5m57Tf/bGrjLJ9uUs6mvmRAYOw0W9vDTLGBkDnrQCE C+0XoBE+07yk6QrlrJh1GzL7JPTAgRib0R70wJIC1mYTAdRGB1XOfh+KqzKHyiO4GYUF oMu8MNjYPvFgsUMKdmolVXzf5NpLinA+s+/mtMocVwftmjaxuUga2eTLuXfAaNJnIsNd asSqrobU8xEuTYo8JEg7Hr6RGlIkTwn8ayPlbGDIpBJvYyW6Zu+3WD0cL5PxN/gCsyuz wzKg== X-Forwarded-Encrypted: i=1; AJvYcCXU0WNBbiSLhkpCZ9VDm/ae5y9KhhG/qIAxrfgUD+1U2ZS8Dp5c4M5m6I35BujzHe73OHvAm+cBVMReyPtzum6jbQ==@lists.postgresql.org X-Gm-Message-State: AOJu0YzcGiuYhYzD4L1s6q9RPMpE3melKRb/96gwhMzMN5ZWdgbFlPGG otWOdNRlTF/9J64DBASEqPBc6DpAtyOmWxXjcYIHbNAnSddHPrrt1MyyKbr8ArDG/Eg= X-Gm-Gg: ASbGnctN33UF8cdk1AxBB8D6eO4SzU3+JtxNhxyQnv3V3mfp1dfRuUEweP5rMO09qPd 8AMiJnY7Jh4biH9YZuiuQovNdarFAXpGtlMxrWr/Ffj/wIGUwWrU/laWjIPYSfkKbSe8H1NHV+b kHe79V4GWhUbp7muqlT44+DlieFHUqgPupbSYLgMNhyd0Vx2FOX6mgjpC/EUky+siI8wC28DxGR p61moMPxH6IkMTugGuDrpDHzQg96pzYsj7ONFj6//1X7HJ36TQC1MY3Fm9B0lCewHSlt8m9AgJA ySph9ZdsIr427IuxLxNhqMfYWZiDBN2ajaDBNVdNsfFN33t7PuUOVY2WBL3d/ZEO88JRCY74ABG R2qyvf4dyOBbDgRACoZLl02KuuvN8Z95V7CaX0qlqVEPDraxHJoQs X-Google-Smtp-Source: AGHT+IG0ZBRVxbKt2z/7P6H9eHaI4xtSzLcTFlN6+czyUUzaoUaKbWd0KF7YI1YIoeSA2Co1ndEG+g== X-Received: by 2002:a17:906:f587:b0:b04:2ee1:8e2 with SMTP id a640c23a62f3a-b1bbd49abbbmr273902266b.36.1758118925798; Wed, 17 Sep 2025 07:22:05 -0700 (PDT) Received: from laurenz.albe-K4N0CV00F97414D ([2001:871:5e:737:fd02:eb3b:54e4:4b92]) by smtp.gmail.com with ESMTPSA id a640c23a62f3a-b1f3f8a9ebfsm7002066b.18.2025.09.17.07.22.05 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 17 Sep 2025 07:22:05 -0700 (PDT) Message-ID: <5c3bbe99acd2dfb9935de8b2aeb246b5e1d2f658.camel@cybertec.at> Subject: Re: Indexes on expressions with multiple columns and operators From: Laurenz Albe To: =?ISO-8859-1?Q?Fr=E9d=E9ric?= Yhuel , "pgsql-performance@lists.postgresql.org" Cc: Jehan-Guillaume de Rorthais , Christophe Courtois Date: Wed, 17 Sep 2025 16:22:04 +0200 In-Reply-To: References: Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable User-Agent: Evolution 3.56.2 (3.56.2-2.fc42) MIME-Version: 1.0 List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk On Wed, 2025-09-17 at 15:55 +0200, Fr=C3=A9d=C3=A9ric Yhuel wrote: > Hello, in the following, I don't understand why: >=20 > 1) the expression index isn't used in the first EXPLAIN >=20 > 2) the number of estimated rows is completely off in the second EXPLAIN,= =20 > whereas the planner could easily use the statistics of foo_f_idx. >=20 > (SQL script attached, tested with master and v17) >=20 > DROP TABLE IF EXISTS foo; >=20 > CREATE UNLOGGED TABLE foo (id bigint, ackid int, crit text); >=20 > ALTER TABLE foo ALTER COLUMN crit SET statistics 400; >=20 > INSERT INTO foo SELECT i, NULL, CASE WHEN i%100=3D1 THEN 'WARNING' ELSE= =20 > 'INFO' END FROM generate_series(1,100000) AS T(i); >=20 > UPDATE foo SET ackid =3D random()*10000 WHERE id%100=3D1 AND id > 500 ; >=20 > CREATE INDEX foo_expr_idx ON foo ((ackid IS NULL AND crit =3D 'WARNING'))= ; >=20 > ANALYZE foo ; >=20 > EXPLAIN (ANALYZE, SUMMARY OFF, BUFFERS OFF) SELECT * FROM foo WHERE=20 > ackid IS NULL AND crit =3D 'WARNING'; > =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0= =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 QUERY PLAN=20 >=20 > -------------------------------------------------------------------------= ------------------------------ > =C2=A0 Seq Scan on foo=C2=A0 (cost=3D0.00..1797.00 rows=3D990 width=3D17)= (actual=20 > time=3D0.012..23.932 rows=3D5.00 loops=3D1) > =C2=A0=C2=A0=C2=A0 Filter: ((ackid IS NULL) AND (crit =3D 'WARNING'::text= )) > =C2=A0=C2=A0=C2=A0 Rows Removed by Filter: 99995 > (3 rows) As far as I know, PostgreSQL considers "ackid IS NULL" and "crit =3D 'WARNI= NG'" as two different RestrictInfos. See the comment: /* * Restriction clause info. * * We create one of these for each AND sub-clause of a restriction conditio= n * (WHERE or JOIN/ON clause). Since the restriction clauses are logically * ANDed, we can use any one of them or any subset of them to filter out * tuples, without having to evaluate the rest. The RestrictInfo node itse= lf * stores data used by the optimizer while choosing the best query plan. PostgreSQL doesn't consider the (exotic) case what someone creates an index on an expression that contains an AND. You could do this: EXPLAIN (ANALYZE, SUMMARY OFF, BUFFERS OFF) SELECT * FROM foo WHERE (ackid IS NULL AND crit =3D 'WARNING') IS TRUE; QUERY PLAN = =20 =E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2= =95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95= =90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90= =E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2= =95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95= =90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90= =E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2= =95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95= =90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90= =E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2= =95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95= =90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90= =E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2= =95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95=90=E2=95= =90=E2=95=90 Index Scan using foo_expr_idx on foo (cost=3D0.29..8.39 rows=3D5 width=3D= 17) (actual time=3D0.011..0.018 rows=3D5.00 loops=3D1) Index Cond: (((ackid IS NULL) AND (crit =3D 'WARNING'::text)) =3D true) Index Searches: 1 (3 rows) But really, you should create a better index, perhaps CREATE INDEX ON foo (crit) WHERE ackid IS NULL; Yours, Laurenz Albe