diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml index 26fc3fc9779..40bf4e6d741 100644 --- a/doc/src/sgml/protocol.sgml +++ b/doc/src/sgml/protocol.sgml @@ -3004,7 +3004,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. @@ -3233,6 +3235,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 4ce30afb5ec..1e0e9251780 100644 --- a/src/backend/backup/basebackup.c +++ b/src/backend/backup/basebackup.c @@ -307,10 +307,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; @@ -1173,7 +1173,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 0fc4042693f..d8ac3bba6bb 100644 --- a/src/backend/backup/basebackup_incremental.c +++ b/src/backend/backup/basebackup_incremental.c @@ -729,7 +729,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 8a1c96b4f5c..d07a940423a 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();