Re: Slower queries on geometry_columns on 3.7.0beta1

Paragon Corporation <[email protected]> Fri, 31 Jul 2026 22:25:44 -0400
Newsgroups gmane.comp.gis.postgis
Message-ID <CAG29aDtbNcqrSWv1csZET0wY8TT7Wp18=0Z6evz5VFOWDMKCUQ@mail.gmail.com>
I have the issue ticketed here -
https://trac.osgeo.org/postgis/ticket/6110  though I think it might
only affect constraint based tables.

Can you try this change, just add a AS NOT MATERIALIZED to the CTE at the t=
op.

As detailed here --
https://trac.osgeo.org/postgis/changeset/9f52dd2f9eeb30f7bc5f774eefe280020f=
320c52/git

On Thu, Jul 30, 2026 at 7:24=E2=80=AFAM Fredrik Widlert
<[email protected]> wrote:
>
> Hi, I tested postgis-3.7.0beta1 today on PostgreSQL 19 beta 2.
>
> It appears that selects on geometry_columns has become much slower in my =
small test database:
>
> select count(*) from geometry_columns;
>
> now takes more than two minutes. The SQL from the 3.6.0 version of the vi=
ew takes a few milliseconds to run.
>
> I notice that the view is much more complicated in 3.7.0beta1 than what i=
t was in earlier versions,
> so I guess the change is related to the new PostGIS rather than the new P=
ostgreSQL.
>
> Query plan for the new version here:
> https://explain.depesz.com/s/rtSg
>
> Is this a known problem? I had Codex build a test case that seems to repr=
oduce something similar
> in an empty database, but I'm not sure if it shows exactly the same probl=
em I'm encountering.
>
> Anyway, this test case is included below in case it is useful.
>
> On my machine, the testcase shows the 3.6 version taking 2 millis and the=
 3.7 version 11 seconds.
>
> /Fredrik Widlert
> [email protected]
>
>
>
> \set ON_ERROR_STOP on
> \timing on
>
> /*
>  * Reproducer for slow geometry_columns queries with the PostGIS 3.7
>  * definition.
>  *
>  * Run this with psql as a superuser in a new, otherwise empty database.
>  *
>  * The test deliberately creates many CHECK constraints which have nothin=
g to
>  * do with PostGIS.  The 3.7 geometry_columns definition builds constrain=
t_defs
>  * from every row in pg_constraint, calls pg_get_constraintdef() for each=
 row,
>  * and scans the result three times with regular expressions.  Consequent=
ly,
>  * unrelated constraints affect the time needed to inspect geometry colum=
ns.
>  *
>  * A partitioned table is used to populate pg_constraint without needing
>  * thousands of CREATE TABLE statements: its 100 CHECK constraints are co=
pied
>  * to each of 200 partitions, producing about 20,000 unrelated
>  * pg_constraint rows.
>  *
>  * The spatial tables use constraint-based geometry columns rather than
>  * typmods.  This is important because it exercises the constraint infere=
nce
>  * added to geometry_columns in 3.7.  Views over those tables also exerci=
se
>  * _postgis_geometry_columns_view_column_origin().
>  *
>  * On slower machines, lower the loop upper bounds below.  The product of=
 the
>  * CHECK-constraint and partition counts controls most of the catalog loa=
d.
>  */
>
> CREATE EXTENSION postgis;
>
> DO $roles$
> BEGIN
> IF NOT EXISTS (
> SELECT
> FROM pg_catalog.pg_roles
> WHERE rolname =3D 'slow_columns_reader'
> ) THEN
> CREATE ROLE slow_columns_reader NOLOGIN;
> END IF;
>
> IF NOT EXISTS (
> SELECT
> FROM pg_catalog.pg_roles
> WHERE rolname =3D 'slow_columns_user'
> ) THEN
> CREATE ROLE slow_columns_user NOLOGIN;
> END IF;
> END
> $roles$;
>
> GRANT slow_columns_reader TO slow_columns_user;
>
> CREATE SCHEMA slow_columns_repro;
>
> /*
>  * Create the unrelated catalog load first.  Constraints are added to the
>  * parent before its partitions are created so PostgreSQL copies them int=
o
>  * every partition.
>  */
> CREATE TABLE slow_columns_repro.constraint_noise (
> id integer NOT NULL
> ) PARTITION BY RANGE (id);
>
> DO $noise$
> BEGIN
> FOR i IN 1..100 LOOP
> EXECUTE format(
> 'ALTER TABLE slow_columns_repro.constraint_noise '
> 'ADD CONSTRAINT %I CHECK (id >=3D %s)',
> 'noise_check_' || i,
> -i
> );
> END LOOP;
>
> FOR i IN 0..199 LOOP
> EXECUTE format(
> 'CREATE TABLE slow_columns_repro.%I '
> 'PARTITION OF slow_columns_repro.constraint_noise '
> 'FOR VALUES FROM (%s) TO (%s)',
> 'constraint_noise_' || i,
> i,
> i + 1
> );
> END LOOP;
> END
> $noise$;
>
> /*
>  * AddGeometryColumn(..., use_typmod =3D> false) creates legacy CHECK
>  * constraints for dimensionality, SRID, and geometry type.  The example =
in
>  * slow_columns.sql used typmods and therefore did not exercise this path=
.
>  */
> DO $spatial_objects$
> DECLARE
> table_name text;
> view_name text;
> BEGIN
> FOR i IN 0..199 LOOP
> table_name :=3D 't' || i;
> view_name :=3D 'v' || i;
>
> EXECUTE format(
> 'CREATE TABLE slow_columns_repro.%I (id bigint)',
> table_name
> );
>
> PERFORM AddGeometryColumn(
> 'slow_columns_repro',
> table_name,
> 'shape',
> 5845,
> 'POINT',
> 3,
> false
> );
>
> EXECUTE format(
> 'CREATE VIEW slow_columns_repro.%I AS '
> 'SELECT id, shape FROM slow_columns_repro.%I',
> view_name,
> table_name
> );
> END LOOP;
> END
> $spatial_objects$;
>
> GRANT USAGE ON SCHEMA slow_columns_repro TO slow_columns_reader;
> GRANT SELECT ON ALL TABLES IN SCHEMA slow_columns_repro
> TO slow_columns_reader;
>
> /*
>  * Preserve the 3.6 definition under another name so both versions see th=
e
>  * exact same catalog and privileges.
>  */
> CREATE VIEW slow_columns_repro.geometry_columns_36 AS
> SELECT
> current_database()::varchar(256) AS f_table_catalog,
> n.nspname AS f_table_schema,
> c.relname AS f_table_name,
> a.attname AS f_geometry_column,
> COALESCE(postgis_typmod_dims(a.atttypmod), 2) AS coord_dimension,
> COALESCE(NULLIF(postgis_typmod_srid(a.atttypmod), 0), 0) AS srid,
> replace(
> replace(
> COALESCE(
> NULLIF(upper(postgis_typmod_type(a.atttypmod)), 'GEOMETRY'),
> 'GEOMETRY'
> ),
> 'ZM',
> ''
> ),
> 'Z',
> ''
> )::varchar(30) AS type
> FROM pg_catalog.pg_class AS c
> JOIN pg_catalog.pg_attribute AS a
> ON a.attrelid =3D c.oid
> AND NOT a.attisdropped
> JOIN pg_catalog.pg_namespace AS n
> ON c.relnamespace =3D n.oid
> JOIN pg_catalog.pg_type AS t
> ON a.atttypid =3D t.oid
> WHERE c.relkind =3D ANY (ARRAY['r'::"char", 'v'::"char", 'm'::"char",
> 'f'::"char", 'p'::"char"])
> AND c.relname <> 'raster_columns'
> AND t.typname =3D 'geometry'
> AND NOT pg_catalog.pg_is_other_temp_schema(c.relnamespace)
> AND pg_catalog.has_table_privilege(c.oid, 'SELECT');
>
> GRANT SELECT ON slow_columns_repro.geometry_columns_36
> TO slow_columns_reader;
>
> /*
>  * Give the planner current catalog statistics.  Without this, results ca=
n
>  * depend on whether autovacuum happened to run during fixture creation.
>  */
> ANALYZE pg_catalog.pg_class;
> ANALYZE pg_catalog.pg_attribute;
> ANALYZE pg_catalog.pg_constraint;
> ANALYZE pg_catalog.pg_namespace;
> ANALYZE pg_catalog.pg_rewrite;
> ANALYZE pg_catalog.pg_type;
>
> SET ROLE slow_columns_user;
> SET jit =3D off;
>
> \echo
> \echo 'Catalog size used by the reproducer:'
> SELECT count(*) AS constraint_count
> FROM pg_catalog.pg_constraint;
>
> \echo
> \echo 'PostGIS 3.6 geometry_columns definition (baseline):'
> EXPLAIN (ANALYZE, BUFFERS, TIMING OFF, SUMMARY ON)
> SELECT count(*)
> FROM slow_columns_repro.geometry_columns_36
> WHERE f_table_schema =3D 'slow_columns_repro';
>
> \echo
> \echo 'Installed PostGIS geometry_columns definition (3.7 regression):'
> EXPLAIN (ANALYZE, BUFFERS, TIMING OFF, SUMMARY ON)
> SELECT count(*)
> FROM public.geometry_columns
> WHERE f_table_schema =3D 'slow_columns_repro';
>
> RESET ROLE;
>
>
>
>
>
>