diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml index e3839518b13..84af9372ebe 100644 --- a/doc/src/sgml/protocol.sgml +++ b/doc/src/sgml/protocol.sgml @@ -3200,7 +3200,9 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" before the transfer is even started, and might as such have a negative impact on the performance. In particular, it might take longer before the first data - is streamed. Since the database files can change during the backup, + is streamed. For an incremental backup, each file is counted at the + size it will occupy in the archive rather than its size on disk. + Since the database files can change during the backup, the size is only approximate and might both grow and shrink between the time of approximation and the sending of the actual files. The default is false. @@ -3429,6 +3431,11 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" tablespace, in kilobytes (1024 bytes), if progress report has been requested; otherwise it's null. This size is measured before any server-side compression. + For an incremental backup this can be much smaller than the + size of the tablespace, since a relation file that has not changed + since the prior backup, or has changed only in part, can be replaced + by a smaller incremental file containing just the blocks that have + changed. diff --git a/src/backend/backup/basebackup.c b/src/backend/backup/basebackup.c index 8e26c4f8118..cbde4c41a0c 100644 --- a/src/backend/backup/basebackup.c +++ b/src/backend/backup/basebackup.c @@ -313,10 +313,10 @@ perform_base_backup(basebackup_options *opt, bbsink *sink, if (tmp->path == NULL) tmp->size = sendDir(sink, ".", 1, true, state.tablespaces, - true, NULL, InvalidOid, NULL); + true, NULL, InvalidOid, ib); else tmp->size = sendTablespace(sink, tmp->path, tmp->oid, true, - NULL, NULL); + NULL, ib); state.bytes_total += tmp->size; } state.bytes_total_is_valid = true; @@ -1175,7 +1175,8 @@ sendTablespace(bbsink *sink, char *path, Oid spcoid, bool sizeonly, /* * Include all files from the given directory in the output tar stream. If * 'sizeonly' is true, we just calculate a total length and return it, without - * actually sending anything. + * actually sending anything. A file that will be sent incrementally is + * counted at the size of the incremental file. * * Omit any directory in the tablespaces list, to avoid backing up * tablespaces twice when they were created inside PGDATA. diff --git a/src/backend/backup/basebackup_incremental.c b/src/backend/backup/basebackup_incremental.c index 9235f6b268c..6955bd3088d 100644 --- a/src/backend/backup/basebackup_incremental.c +++ b/src/backend/backup/basebackup_incremental.c @@ -727,7 +727,11 @@ GetFileBackupMethod(IncrementalBackupInfo *ib, const char *path, ipath = GetIncrementalFilePath(dboid, spcoid, relfilenumber, forknum, segno); if (backup_file_lookup(ib->manifest_files, ipath) == NULL) + { + pfree(ipath); return BACK_UP_FILE_FULLY; + } + pfree(ipath); } /* diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build index d70ce5786a2..2d3b22ff06e 100644 --- a/src/bin/pg_basebackup/meson.build +++ b/src/bin/pg_basebackup/meson.build @@ -100,6 +100,7 @@ tests += { 'tests': [ 't/010_pg_basebackup.pl', 't/011_in_place_tablespace.pl', + 't/012_incremental_progress.pl', 't/020_pg_receivewal.pl', 't/030_pg_recvlogical.pl', 't/040_pg_createsubscriber.pl', diff --git a/src/bin/pg_basebackup/t/012_incremental_progress.pl b/src/bin/pg_basebackup/t/012_incremental_progress.pl new file mode 100644 index 00000000000..fdc0729cf58 --- /dev/null +++ b/src/bin/pg_basebackup/t/012_incremental_progress.pl @@ -0,0 +1,85 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Verify that incremental backup size estimates account for files sent in +# incremental format, rather than counting every file at its full size. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('primary'); +$node->init(allows_streaming => 1); +$node->append_conf('postgresql.conf', 'summarize_wal = on'); +$node->append_conf('postgresql.conf', 'autovacuum = off'); +$node->start; + +# Put unchanged data in a separate tablespace to exercise size estimation +# for both PGDATA and user-defined tablespaces. +$node->safe_psql('postgres', < $node->host, + '--port' => $node->port, + '--pgdata' => $path, + '--format' => 'tar', + '--wal-method' => 'none', + '--no-sync', + '--progress', + '--checkpoint' => 'fast', + @extra + ], + '>' => \$stdout, + '2>' => \$stderr); + ok($result, "backup into $path succeeded") + or die "pg_basebackup into $path failed: $stderr"; + + # Use the last progress line, allowing either newline or carriage return. + my @lines = grep { /kB/ } split(/[\r\n]+/, $stderr); + my $line = $lines[-1] // ''; + my ($streamed, $total) = $line =~ m{^\s*(\d+)/(\d+) kB\b}; + ok(defined($total), "progress was reported for $path") + or die "could not parse progress output for $path: $stderr"; + note "backup into $path streamed $streamed of $total kB"; + return ($streamed, $total); +} + +my $backup_dir = $node->backup_dir; +my (undef, $full_total) = backup_with_progress("$backup_dir/full"); + +# Make a small change before taking the incremental backup. +$node->safe_psql('postgres', + 'INSERT INTO changed SELECT g FROM generate_series(1, 1000) g'); + +my ($incr_streamed, $incr_total) = backup_with_progress("$backup_dir/incr", + '--incremental' => "$backup_dir/full/backup_manifest"); + +# The incremental backup estimate should be much smaller than the full one. +cmp_ok($incr_total, '<', $full_total / 2, + 'incremental backup estimate is much smaller than a full backup estimate' +); + +# Check for a substantial overestimate. pg_basebackup raises the displayed +# total when the amount streamed exceeds the original estimate. +cmp_ok( + $incr_total, '<', + $incr_streamed * 2, + 'incremental backup estimate is not substantially overestimated'); + +done_testing();