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 1x6PZU-000DWw-34 for pgsql-bugs@arkaria.postgresql.org; Tue, 15 Sep 2026 09:35:25 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1x6PZT-000wUw-1L for pgsql-bugs@arkaria.postgresql.org; Tue, 15 Sep 2026 09:35:23 +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 1x6Dcv-00FYls-2E for pgsql-bugs@lists.postgresql.org; Mon, 14 Sep 2026 20:50:09 +0000 Received: from mahout.postgresql.org ([2001:4800:3e1:1::227]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1x6Dcr-00000000SIX-1ib9 for pgsql-bugs@lists.postgresql.org; Mon, 14 Sep 2026 20:50:09 +0000 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=postgresql.org; s=20171124; h=Message-ID:Date:Reply-To:Cc:From:To:Subject: Content-Transfer-Encoding:MIME-Version:Content-Type:Sender:Content-ID: Content-Description:In-Reply-To:References; bh=j6n5eiu6C2MWvCP1uKueQI1JqUyhWMGWMm92YT4UkeE=; b=BJeE0Eq3On7ZBzLYVtwVRY3WIT h5oqTYHJz8eA1PPwEkRTW3ejpIR43wyz4SoU1qqhMxtc2Gwzh2SjnGpKX1AjXy3xTlYcMJVYtMyEK d1bHygxur//WXCcw0K3RvB/zHIIq8sIYZViLdhBekxbD95yvgpCBWYead40VlO6F5LhMrFuB5BEsr ePaMbsWGM4ifUAyRCF5xubH6nASt+4zm6VX7TGhA294vnrgmmQeGwc078yYLdFN2BoBvRA5Jrn6Eo +Lshuv4SyYJldb6BMtbEH7PS1h+sWPVGMwt09g4B4o/Qt+4psKykwO99WnEuIXMFyvY60vKL+Q3E8 clQfFZtw==; Received: from wrigleys.postgresql.org ([2a02:16a8:dc51::60]) by mahout.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1x6Dcq-000DPO-2r for pgsql-bugs@lists.postgresql.org; Mon, 14 Sep 2026 20:50:05 +0000 Received: from localhost ([127.0.0.1] helo=wrigleys.postgresql.org) by wrigleys.postgresql.org with esmtp (Exim 4.98.2) (envelope-from ) id 1x6Dcp-000000037uF-1eY6 for pgsql-bugs@lists.postgresql.org; Mon, 14 Sep 2026 20:50:03 +0000 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Subject: BUG #19688: pg_dump --schema scans all sequences in PostgreSQL 18, causing severe performance regression To: pgsql-bugs@lists.postgresql.org From: PG Bug reporting form Cc: cesarg9@gmail.com Reply-To: cesarg9@gmail.com, pgsql-bugs@lists.postgresql.org Date: Mon, 14 Sep 2026 20:49:46 +0000 Message-ID: <19688-e90025dc375a22a3@postgresql.org> X-Auto-Response-Suppress: All Auto-Submitted: auto-generated List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk The following bug has been logged on the website: Bug reference: 19688 Logged by: C=C3=A9sar Garc=C3=ADa Naranjo Email address: cesarg9@gmail.com PostgreSQL version: 18.6 Operating system: Ubuntu 24.04 Description: =20 I am seeing a severe performance regression in pg_dump --schema after upgrading from PostgreSQL 16 to PostgreSQL 18.6. Environment: * PostgreSQL server: 18.6 * pg_dump: 18.6 * Database uses a schema-per-tenant design * Total sequences in the database: 142237 * Sequences in the schema being dumped: 329 The command is approximately: --------------- pg_dump \ -h 127.0.0.1 \ -p 5432 \ -U postgres \ -F c \ -n myschema \ -b \ mydatabase \ -f output.dump --------------- With PostgreSQL 16, dumping this schema normally took aprox. ~50 seconds. After upgrading to PostgreSQL 18.6, the same dump takes approximately 5 minutes and half. Using pg_stat_activity while the dump is running, almost all of the additional time is spent in this query executed by pg_dump: --------------- SELECT seqrelid, format_type(seqtypid, NULL), seqstart, seqincrement, seqmax, seqmin, seqcache, seqcycle, last_value, is_called FROM pg_catalog.pg_sequence, pg_get_sequence_data(seqrelid) ORDER BY seqrelid; --------------- During this query, the backend is typically waiting on: wait_event_type =3D IO wait_event =3D DataFileRead I reproduced the sequence query independently. Running it for all sequences in the database: --------------- SELECT count(*) FROM ( SELECT seqrelid, format_type(seqtypid, NULL), seqstart, seqincrement, seqmax, seqmin, seqcache, seqcycle, last_value, is_called FROM pg_catalog.pg_sequence, pg_get_sequence_data(seqrelid) ORDER BY seqrelid ) s; Result: count: 142237 Time: 298715.267 ms (04:58.715) --------------- The complete pg_dump --schema=3Dmyschema takes approximately: --------------- 326.7 seconds --------------- so this sequence collection query accounts for almost all of the dump time, however the selected schema only contains 329 sequences. Running an equivalent query restricted to that schema: --------------- SELECT count(*) FROM ( SELECT s.seqrelid, d.last_value, d.is_called FROM pg_catalog.pg_sequence s JOIN pg_catalog.pg_class c ON c.oid =3D s.seqrelid JOIN pg_catalog.pg_namespace n ON n.oid =3D c.relnamespace CROSS JOIN LATERAL pg_get_sequence_data(s.seqrelid) d WHERE n.nspname =3D 'myschema' ) x; Result: count: 329 Time: 4984.888 ms (00:04.985) --------------- The important part appears to be that PostgreSQL 18 pg_dump calls pg_get_sequence_data() for every sequence in the database, even when pg_dump is restricted to a single schema. This is particularly expensive for schema-per-tenant databases. In this database there are hundreds of schemas, each with approximately 329 sequences. I prepared an experimental patch to pg_dump, with AI assistance. I am not familiar with the PostgreSQL codebase, so please treat this patch as a proof of concept rather than a proposed final fix. The patch changes collectSequences() so that, for partial dumps, the sequence query is restricted to the sequence OIDs that pg_dump has already selected internally. It keeps the existing PostgreSQL 18 behavior for full database dumps, while for partial dumps it adds a condition equivalent to: --------------- WHERE seqrelid =3D ANY ('{selected sequence OIDs}'::oid[]) --------------- With this patched pg_dump, dumping the same schema takes approximately ~50 seconds instead of ~5 minutes. I also compared the output produced by the official and patched versions. The archive TOCs were identical except for the archive creation timestamp. After converting both archives to SQL using pg_restore, the generated SQL was identical except for the random \restrict / \unrestrict token generated by pg_dump. I also restored both dumps and compared the sequence state. This suggests that the performance regression is caused specifically by collectSequences() reading sequence data for sequences that are not part of the requested partial dump. This behavior is especially problematic when backups are performed separately per schema, because the cost of scanning all 142,237 sequences is paid again for every pg_dump --schema invocation. In my case i take daily backups for every schema, and adding ~5 minutes for every one would add 36 additional hours, thus making daily backups impossible. Would it make sense for collectSequences() to restrict pg_get_sequence_data() to the sequence objects already selected by pg_dump for partial dumps, while retaining the current bulk query for full database dumps? Here is the patch that i am using right now as i cannot rollback the database upgrade. I used the following to configure, compile and run the current tests: ./configure --without-readline --enable-tap-tests make -j4 make -C src/bin/pg_dump check All the tests passed. --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -311,7 +311,7 @@ static void dumpTableSchema(Archive *fout, const TableInfo *tbinfo); static void dumpTableAttach(Archive *fout, const TableAttachInfo *attachinfo); static void dumpAttrDef(Archive *fout, const AttrDefInfo *adinfo); -static void collectSequences(Archive *fout); +static void collectSequences(Archive *fout, TableInfo tblinfo[], int numTables); static void dumpSequence(Archive *fout, const TableInfo *tbinfo); static void dumpSequenceData(Archive *fout, const TableDataInfo *tdinfo); static void dumpIndex(Archive *fout, const IndxInfo *indxinfo); @@ -1141,7 +1141,7 @@ collectBinaryUpgradeClassOids(fout); =20 /* Collect sequence information. */ - collectSequences(fout); + collectSequences(fout, tblinfo, numTables); =20 /* Lastly, create dummy objects to represent the section boundaries */ boundaryObjs =3D createBoundaryObjects(); @@ -18728,10 +18728,15 @@ * speed in lookup. */ static void -collectSequences(Archive *fout) +collectSequences(Archive *fout, TableInfo tblinfo[], int numTables) { PGresult *res; - const char *query; + PQExpBuffer query; + bool partial_dump =3D !fout->dopt->include_everything || + schema_exclude_oids.head !=3D NULL || + table_exclude_oids.head !=3D NULL || + tabledata_exclude_oids.head !=3D NULL || + extension_exclude_oids.head !=3D NULL; =20 /* * Before Postgres 10, sequence metadata is in the sequence itself. With @@ -18743,26 +18748,49 @@ */ if (fout->remoteVersion < 100000) return; - else if (fout->remoteVersion < 180000 || + query =3D createPQExpBuffer(); + if (fout->remoteVersion < 180000 || (!fout->dopt->dumpData && !fout->dopt->sequence_data)) - query =3D "SELECT seqrelid, format_type(seqtypid, NULL), " + appendPQExpBufferStr(query, "SELECT seqrelid, format_type(seqtypid, NULL), " "seqstart, seqincrement, " "seqmax, seqmin, " "seqcache, seqcycle, " "NULL, 'f' " - "FROM pg_catalog.pg_sequence " - "ORDER BY seqrelid"; + "FROM pg_catalog.pg_sequence "); else - query =3D "SELECT seqrelid, format_type(seqtypid, NULL), " + appendPQExpBufferStr(query, "SELECT seqrelid, format_type(seqtypid, NULL), " "seqstart, seqincrement, " "seqmax, seqmin, " "seqcache, seqcycle, " "last_value, is_called " "FROM pg_catalog.pg_sequence, " - "pg_get_sequence_data(seqrelid) " - "ORDER BY seqrelid;"; + "pg_get_sequence_data(seqrelid) "); =20 - res =3D ExecuteSqlQuery(fout, query, PGRES_TUPLES_OK); + /* For PostgreSQL 18 and newer, restrict partial dumps to sequences whose + * definition or data is emitted. Keep the upstream query for older servers. + * getSchemaData() has already accounted for filters, ownership, and + * extension membership; getTableData() has created the data objects. + */ + if (fout->remoteVersion >=3D 180000 && partial_dump) + { + appendPQExpBufferStr(query, "WHERE seqrelid =3D ANY ('{"); + for (int i =3D 0, n =3D 0; i < numTables; i++) + { + TableInfo *tbinfo =3D &tblinfo[i]; + + if (tbinfo->relkind !=3D RELKIND_SEQUENCE || + (!(tbinfo->dobj.dump & DUMP_COMPONENT_DEFINITION) && + tbinfo->dataObj =3D=3D NULL)) + continue; + appendPQExpBuffer(query, "%s%u", n++ ? "," : "", + tbinfo->dobj.catId.oid); + } + appendPQExpBufferStr(query, "}'::pg_catalog.oid[]) "); + } + appendPQExpBufferStr(query, "ORDER BY seqrelid"); + + res =3D ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + destroyPQExpBuffer(query); =20 nsequences =3D PQntuples(res); sequences =3D (SequenceItem *) pg_malloc(nsequences * sizeof(SequenceItem));