PGCOPYDB CLONE(1)
๐ธ NAME ๐ธ
pgcopydb clone โ pgcopydb clone
okay so this is the main character of the whole pgcopydb manual, bestie, and honestly it ate ๐
The main pgcopydb operation is the clone operation, and for historical and user friendliness reasons (iykyk) there are two aliases that do the exact same thing:
pgcopydb
clone Clone an entire database from source to target
fork Clone an entire database from source to target
Yeah. Same vibe, two names, no cap. ๐
๐ฆ PGCOPYDB CLONE ๐ฆ
The command pgcopydb clone copies a database from the given source Postgres instance to the target Postgres instance. It's giving "copy that, but make it a whole database". โจ
pgcopydb clone: Clone an entire database from source to target
usage: pgcopydb clone --source ... --target ... [ --table-jobs ... --index-jobs ... ]
๐ the flags, all of them, periodt ๐
--sourceโ Postgres URI to the source database--targetโ Postgres URI to the target database--dirโ work directory to use--table-jobsโ number of concurrentCOPYjobs to run--index-jobsโ number of concurrentCREATE INDEXjobs to run--restore-jobsโ number of concurrent jobs forpg_restore--large-objects-jobsโ number of concurrent Large Objects jobs to run--split-tables-larger-thanโ same-table concurrency size threshold--split-max-partsโ maximum number of jobs for Same-table concurrency--estimate-table-sizesโ allow using estimates for relation sizes--drop-if-existsโ on the target database, clean-up from a previous run first--rolesโ also copy roles found on source to target--no-role-passwordsโ do not dump passwords for roles--no-ownerโ do not set ownership of objects to match the original database--no-aclโ prevent restoration of access privileges (grant/revoke commands).--no-commentsโ do not output commands to restore comments--no-tablespacesโ do not output commands to select tablespaces--skip-large-objectsโ skip copying large objects (blobs)--skip-extensionsโ skip restoring extensions--skip-ext-commentsโ skip restoringCOMMENT ON EXTENSION--skip-collationsโ skip restoring collations--skip-vacuumโ skip runningVACUUM ANALYZE--skip-analyzeโ skip runningvacuumdb --analyze-only--skip-db-propertiesโ skip copyingALTER DATABASE SETproperties--skip-split-by-ctidโ skip spliting tables byctid--requirements <filename>โ list extensions requirements--filters <filename>โ use the filters defined in <filename>--fail-fastโ abort early in case of error--restartโ allow restarting when temp files exist already--resumeโ allow resuming operations after a failure--not-consistentโ allow taking a new snapshot on the source database--snapshotโ use snapshot obtained withpg_export_snapshot--followโ implement logical decoding to replay changes--pluginโ output plugin to use (pgoutput,test_decoding,wal2json)--wal2json-numeric-as-stringโ print numeric data type as string when usingwal2jsonoutput plugin--slot-nameโ use this Postgres replication slot name--create-slotโ create the replication slot--originโ use this Postgres replication origin node name--endposโ stop replaying changes when reaching this LSN--use-copy-binaryโ use theCOPY BINARYformat forCOPYoperations--all-databasesโ clone all databases found on the source instance
๐ PGCOPYDB FORK ๐
The command pgcopydb fork copies a database from the given source Postgres instance to the target Postgres instance. This command is an alias to the command pgcopydb clone seen above โ literally the same thing wearing a different outfit. Slay. ๐ซถ
โจ DESCRIPTION โจ
The pgcopydb clone command implements both a base copy of a source database into a target database and a full Logical Decoding client (https://www.postgresql.org/docs/current/logicaldecoding.html). The default plugin is pgoutput (https://www.postgresql.org/docs/current/protocol-logical-replication.html), built into PostgreSQL core since version 10. The wal2json (https://github.com/eulerto/wal2json/) and test_decoding (https://www.postgresql.org/docs/current/test-decoding.html) plugins are also supported, so pick your fighter. ๐ฅ
๐ Base copy, or the clone operation ๐
The pgcopydb clone command implements the following steps:
pgcopydbgets the list of ordinary and partitioned tables from a catalog query on the source database, and also the list of indexes, and the list of sequences with their current values.
When filtering is used, the list of objects OIDs that are meant to be filtered out is built during this step.
pgcopydbcalls intopg_dumpto produce the pre-data section and the post-data sections of the dump using Postgres custom format.- The pre-data section of the dump is restored on the target database using the
pg_restorecommand, creating all the Postgres objects from the source database into the target database.
When filtering is used, the pg_restore --use-list feature is used to filter the list of objects to restore in this step.
This step uses as many as --restore-jobs jobs for pg_restore to share the workload and restore the objects in parallel.
- Then as many as
--table-jobsCOPYsub-processes are started to share the workload andCOPYthe data from the source to the target database one table at a time, in a loop. ๐
A Postgres connection and a SQL query to the Postgres catalog table pg_class is used to get the list of tables with data to copy around, and the reltuples statistic is used to start with the tables with the greatest number of rows first, as an attempt to minimize the copy time.
- An auxiliary process loops through all the Large Objects found on the source database and copies its data parts over to the target database, much like
pg_dumpitself would.
This step is much like pg_dump | pg_restore for large objects data parts, except that there isn't a good way to do just that with the tooling. ๐ญ
- As many as
--index-jobsCREATE INDEXsub-processes are started to share the workload and build indexes. In order to make sure to start theCREATE INDEXcommands only after theCOPYoperation has completed, a queue mechanism is used. As soon as a table dataCOPYhas completed, all the indexes for the table are queued for processing by theCREATE INDEXsub-processes.
The primary indexes are created as UNIQUE indexes at this stage.
- Then the
PRIMARY KEYconstraints are createdUSINGthe just built indexes. This two-steps approach allows the primary key index itself to be created in parallel with other indexes on the same table, avoiding anEXCLUSIVE LOCKwhile creating the index. - As many as
--table-jobsVACUUM ANALYZEsub-processes are started to share the workload. As soon as a table dataCOPYhas completed, the table is queued for processing by theVACUUM ANALYZEsub-processes. - An auxilliary process loops over the sequences on the source database and for each of them runs a separate query on the source to fetch the
last_valueand theis_calledmetadata the same way thatpg_dumpdoes.
For each sequence, pgcopydb then calls pg_catalog.setval() on the target database with the information obtained on the source database.
- The final stage consists now of running the
pg_restorecommand for the post-data section script for the whole database, and that's where the foreign key constraints and other elements are created.
The post-data script is filtered out using the pg_restore --use-list option so that indexes and primary key constraints already created in steps 6 and 7 are properly skipped now.
This step uses as many as --restore-jobs jobs for pg_restore to share the workload and restore the objects in parallel.
๐ Postgres privileges, superuser, and dump and restore ๐
Postgres has a notion of a superuser status that can be assigned to any role in the system, and the default role postgres has this status. From the Role Attributes (https://www.postgresql.org/docs/current/role-attributes.html) documentation page we see that:
superuser status: A database superuser bypasses all permission checks, except the right to log in. This is a dangerous privilege and should not be used carelessly; it is best to do most of your work as a role that is not a superuser. To create a new database superuser, use CREATE ROLE name SUPERUSER. You must do this as a role that is already a superuser.
Some Postgres objects can only be created by superusers, and some read and write operations are only allowed to superuser roles, such as the following non-exclusive list:
- Reading the
pg_authid(https://www.postgresql.org/docs/current/catalog-pg-authid.html) role password (even when encrypted) is restricted to roles with the superuser status. Reading this catalog table is done when callingpg_dumpall --roles-onlyso that the dump file can then be used to restore roles including their passwords.
It is possible to implement a pgcopydb migration that skips the passwords entirely when using the option --no-role-passwords. In that case though authentication might fail until passwords have been setup again correctly. โ ๏ธ
- Most of the available Postgres extensions, at least when being written in C, are then only allowed to be created by roles with superuser status.
When such an extension contains Extension Configuration Tables (https://www.postgresql.org/docs/current/extend-extensions.html#EXTEND-EXTENSIONS-CONFIG-TABLES) and has been created with a role having superuser status, then the same superuser status is needed again to pg_dump and pg_restore that extension and its current configuration.
When using pgcopydb it is possible to split your migration in privileged and non-privileged parts, like in the following examples:
$ coproc ( pgcopydb snapshot )
# first two commands would use a superuser role to connect
$ pgcopydb copy roles --source ... --target ...
$ pgcopydb copy extensions --source ... --target ...
# now it's possible to use a non-superuser role to connect
$ pgcopydb clone --skip-extensions --source ... --target ...
$ kill -TERM ${COPROC_PID}
$ wait ${COPROC_PID}
In that kind of script, bestie, the calls to pgcopydb copy roles and pgcopydb copy extensions get made with connection strings that come in as a role with superuser status ๐ฆ โ and then the pgcopydb clone call gets made with a non-privileged role, usually the role that owns the source and the target databases.
Warning ๐ญ โ okay so, real talk: there's currently a limitation in pg_dump that impacts pgcopydb. When an extension with a configuration table has been installed as superuser, even the mainpgcopydb cloneoperation has to be done with superuser status. Why? Because pg_dump filtering (--exclude-table, that one) does not apply to extension members, and pg_dump does not provide a mechanism to exclude extensions. It's giving "no exit door". ๐
๐ง Extension-aware migration
pgcopydb detects certain extensions in the source catalog and automatically runs extension-specific steps at exactly the right point in the pipeline โจ. No configuration needed โ the behaviour is triggered by the extension simply being there in the source database. And heads up: if an extension is listed under [exclude-extension] in the pgcopydb configuration filter file, it is never fetched into the catalog, so its hook is skipped. Clean. ๐
TimescaleDB
When timescaledb is detected in the source database, pgcopydb calls two functions on the target database:
- Before
pg_restore --pre-data:timescaledb_pre_restore()puts TimescaleDB into restore mode so it does not interfere with schema restore. โจ - After
pg_restore --post-data:timescaledb_post_restore()re-enables normal TimescaleDB operation. ๐ธ
Both the source and the target databases must have the TimescaleDB extension installed. No cap.
Citus
When citus is detected in the source database, pgcopydb queries the citus_tables view on the source coordinator and, after pg_restore --pre-data (at which point tables exist on the target as empty plain PostgreSQL tables), calls create_distributed_table() and create_reference_table() on the target coordinator. Main character behaviour, honestly. ๐
Co-location groups are preserved: within each group the alphabetically first table is created with shard_count := N, colocate_with := 'none', and the remaining tables reference it via colocate_with. Reference tables are always created after all distributed tables.
Data is then copied through the Citus coordinator, which routes rows to the correct shards transparently โ shard tables are never visible to pgcopydb. ๐ Both the source and the target must be Citus coordinators (multi-worker clusters are fully supported).
Cloning all databases with --all-databases
When you use the --all-databases option, pgcopydb clone enumerates all non-template user databases on the source Postgres instance and clones each one of them to the target instance in a single invocation. One command, whole instance, slay. ๐
The source and target URIs must point to the instance level (e.g. postgres://host/postgres) rather than to a specific application database. pgcopydb derives per-database connection strings by substituting the database name component.
The operation is structured in three sequential phases:
- Phase I โ snapshot export and pre-data: Roles are copied once at the instance level. A snapshot holder subprocess exports one consistent REPEATABLE READ snapshot per source database and holds those transactions open. A pool of
--table-jobspre-data workers then processes the databases in parallel, running schema fetch,pg_dump --pre-data, andpg_restore --pre-datafor each. - Phase II โ global COPY, indexes, and VACUUM: All tables across all databases are sorted by size (largest first) and processed by a single shared pool of workers โ
--table-jobsCOPY workers,--index-jobsindex/constraint workers, and--table-jobsVACUUM workers. The pools are not allocated per-database; they serve the entire workload globally. ๐ซถ - Phase III โ post-data: Databases are finalized one at a time:
pg_restore --post-dataapplies remaining objects (triggers, rules, comments, etc.) and sequences are reset to match the source.
A consolidated summary is printed at the end aggregating timings across all databases. ๐
For details about the process tree and the cross-database COPY queue, see Cloning all databases: the --all-databases option.
๐ Change Data Capture using Postgres Logical Decoding
When you use the --follow option, the steps from the pgcopydb follow command are also run concurrently to the main copy. The Change Data Capture is then automatically driven from a prefetch-only phase to the prefetch-and-catchup phase, which is enabled as soon as the base copy is done. It literally promotes itself. โจ
See the command pgcopydb stream sentinel set endpos to remote control the follow parts of the command even while the command is already running.
The command pgcopydb stream cleanup must be used to free resources created to support the change data capture process.
Important ๐จ โ Make sure to read the documentation for pgcopydb follow and the specifics about Logical Replication Restrictions as documented by Postgres. Seriously bestie, read it.
๐ธ Change Data Capture Example 1
A simple approach to applying changes after the initial base copy has been done follows:
$ pgcopydb clone --follow &
# later when the application is ready to make the switch
$ pgcopydb stream sentinel set endpos --current
# later when the migration is finished, clean-up both source and target
$ pgcopydb stream cleanup
๐ฆ Change Data Capture Example 2
In some cases, it might be necessary to have more control over some of the steps taken here. Given pgcopydb flexibility, it's possible to implement the following steps:
- Grab a snapshot from the source database and hold an open Postgres connection for the duration of the base copy.
In case of crash or other problems with the main operations, it's then possible to resume processing of the base copy and the applying of the changes with the same snapshot again.
This step is also implemented when using pgcopydb clone --follow. That said, if the command was interrupted (or crashed), then the snapshot would be lost. ๐ญ
- Setup the logical decoding within the snapshot obtained in the previous step, and the replication tracking on the target database.
The following SQL objects are then created:
- a replication slot on the source database,
- a replication origin on the target database.
This step is also implemented when using pgcopydb clone --follow. There is no way to implement Change Data Capture with pgcopydb and skip creating those SQL objects. Periodt.
- Start the base copy of the source database, and prefetch logical decoding changes to ensure that we consume from the replication slot and allow the source database server to recycle its WAL files.
- Remote control the apply process to stop consuming changes and applying them on the target database.
- Re-sync the sequences to their now-current values.
Sequences are not handled by Postgres logical decoding, so extra care needs to be implemented manually here. ๐
Important โ The next version of pgcopydb will include that step in the pgcopydb clone --snapshot command automatically, after it stops consuming changes and before the process terminates.
- Clean-up the specific resources created for supporting resumability of the whole process (replication slot on the source database, replication origin on the target database).
- Stop holding a snaphot on the source database by stopping the
pgcopydb snapshotprocess left running in the background.
If the command pgcopydb clone --follow fails it's then possible to start it again. It will automatically discover what was done successfully and what needs to be done again because it failed or was interrupted (table copy, index creation, resuming replication slot consuming, resuming applying changes at the right LSN position, etc). Resilient queen. ๐
Here is an example implement the previous steps:
$ pgcopydb snapshot &
$ pgcopydb stream setup
$ pgcopydb clone --follow &
# later when the application is ready to make the switch
$ pgcopydb stream sentinel set endpos --current
# when the follow process has terminated, re-sync the sequences
$ pgcopydb copy sequences
# later when the migration is finished, clean-up both source and target
$ pgcopydb stream cleanup
# now stop holding the snapshot transaction (adjust PID to your environment)
$ kill %1
โจ OPTIONS โจ
The following options are available to pgcopydb clone:
--sourceโ Connection string to the source Postgres instance. See the Postgres documentation for connection strings for the details. In short both the quoted form"host=... dbname=..."and the URI formpostgres://user@host:5432/dbnameare supported. ๐--targetโ Connection string to the target Postgres instance.--dirโ During its normal operations pgcopydb creates a lot of temporary files to track sub-processes progress ๐. Temporary files are created in the directory specified by this option, or defaults to${TMPDIR}/pgcopydbwhen the environment variable is set, or otherwise to/tmp/pgcopydb.--table-jobsโ How many tables can be processed in parallel.
This limit only applies to the COPY operations, more sub-processes will be running at the same time that this limit while the CREATE INDEX operations are in progress, though then the processes are only waiting for the target Postgres instance to do all the work. ๐
--index-jobsโ How many indexes can be built in parallel, globally. A good option is to set this option to the count of CPU cores that are available on the Postgres target system, minus some cores that are going to be used for handling the COPY operations. ๐--restore-jobsโ How many threads or processes can be used duringpg_restore. A good option is to set this option to the count of CPU cores that are available on the Postgres target system.
Okay quick catch-up before we move on ๐
โ if this value isn't set, we reuse the --index-jobs value. And if that value isn't set either? We just fall back to the default value for --index-jobs. easy โจ
--large-object-jobsโ how many worker processes to start so Large Objects get copied concurrently ๐ฆ like, parallel besties doing blobs at the same time.--split-tables-larger-thanโ turns on Same-table Concurrency when we're processing the source database. ๐ธ The value here (this environment variable value) is expected to be a byte size, and the bytes units that are known areB,kB,MB,GB,TB,PB, andEB. so like,--split-tables-larger-than 1GBvibes, iykyk.--estimate-table-sizesโ uses estimates of table sizes to decide how to split tables when Same-table Concurrency is in play ๐ซ
When this option is used, we run the vacuumdb --analyze-only --jobs=<table-jobs> command on the source database, and that updates the statistics for the number of pages for each relation. Then later we take the number of pages, plus the size for each page, and estimate the actual size of the tables. math but make it slay ๐งฎโจ
And if you'd rather run the ANALYZE command yourself, manually, before running pgcopydb โ you can use --skip-analyze. That way you can decrease the time spent on the migration. main character optimization energy ๐
--drop-if-existsโ when we restore the schema on the target Postgres instance, pgcopydb actually usespg_restoreunder the hood. And when this option is specified, thesepg_restoreoptions get used too:--clean --if-exists. ๐
This one is useful when you run the same command several times in a row โ whether you're fixing a previous mistake, or for instance when it's used in a continuous integration system. reliable queen ๐
โ ๏ธ bestie READ THIS TWICE: this option causesDROP TABLEandDROP INDEXand otherDROPcommands to be used. Make sure you understand what you're doing here! ๐
--rolesโ adds a preliminary step that copies the roles found on the source instance over to the target instance. Since Postgres roles are a global object, they don't exist only within the context of a specific database โ so all the roles get copied over when you use this option. ๐
The pg_dumpall --roles-only command is what fetches the list of roles from the source database, and heads up: that command includes support for passwords. Which means this operation requires superuser privileges. ๐
See also pgcopydb copy roles.
--no-role-passwordsโ don't dump passwords for roles. When they get restored, roles will have anullpassword, and password authentication will always fail until the password is set. ๐ญ
Since password values aren't needed when this option is specified, the role information is read from the catalog view pg_roles instead of pg_authid. So bonus: this option also helps if access to pg_authid is restricted by some security policy. protective and efficient, we love ๐ซถ
--no-ownerโ don't output commands to set ownership of objects to match the original database. By default,pg_restoreissuesALTER OWNERorSET SESSION AUTHORIZATIONstatements to set ownership of created schema elements. Those statements will fail unless the initial connection to the database is made by a superuser (or the same user that owns all of the objects in the script). With--no-owner, any user name can be used for the initial connection, and this user will own all the created objects. no cap ๐--skip-large-objectsโ skip copying large objects (also known as blobs) when copying the data from the source database to the target database. bye blobs ๐๐ฆ--skip-extensionsโ skip copying extensions from the source database to the target database. ๐ง
When used, schema that extensions depend-on are also skipped: it is expected that creating needed extensions on the target system is then the responsibility of another command (such as pgcopydb copy extensions), and schemas that extensions depend-on are part of that responsibility. team effort fr fr.
And because creating extensions requires superuser, this allows a multi-steps approach where extensions are dealt with superuser privileges, and then the rest of the pgcopydb operations are done without superuser privileges. ๐
--skip-ext-commentsโ skip copyingCOMMENT ON EXTENSIONcommands. ๐ This is implicit when using--skip-extensions.--requirements <filename>โ this option lets you specify which version of an extension to install on the target database. The given filename is expected to be a JSON file, and the JSON contents must be an array of objects with the keys"name"and"version". ๐
The command pgcopydb list extensions --requirements --json produces such a JSON file, and can be used on the target database instance to get you started. iconic little pipeline ngl โจ
See also the command pgcopydb list extensions --available-versions.
See also pgcopydb list extensions.
--skip-collationsโ skip copying collations from the source database to the target database. ๐ธ
In some scenarios the list of collations provided by the Operating System on the source and target system might be different, and a mapping then needs to be manually installed before calling pgcopydb.
So this option lets pgcopydb skip over collations and just assume all the needed collations have been deployed on the target database already. ๐
See also pgcopydb list collations.
--skip-vacuumโ skip runningVACUUM ANALYZEon the target database once a table has been copied, its indexes have been created, and constraints installed. we're just not doing that today ๐--skip-analyzeโ skip runningvacuumdb --analyze-onlyon the source database to update statistics that are required when estimating table sizes. ๐
This option is useful only when using --estimate-table-sizes and the user runs the relevant ANALYZE command manually before running pgcopydb. you got this yourself, and that's valid ๐
--skip-db-propertiesโ skip fetching database properties and copying them using the SQL commandALTER DATABASE ... SET name = value. ๐ This is useful when the source and target database have a different set of properties, or when the target database is hosted in a way that disabled setting some of the properties that have been set on the source database, or also when copying these settings is not wanted. sometimes we just don't wanna ๐ซถ--skip-split-by-ctidโ skip splitting tables based on CTID during the copy operation. By default, pgcopydb splits large tables into smaller chunks based on the CTID column if there isn't a unique integer column in the table. ๐ However, in some cases you may want to skip this splitting process if the CTID range scan is slow in the underlying system. not everything has to be a speedrun ๐--filters <filename>โ this option lets you exclude table and indexes from the copy operations. ๐ธ See Filtering for details about the expected file format and the filtering options available.--fail-fastโ abort early in case of error by sending theTERMsignal to all the processes in the pgcopydb process group. no waiting around, we're out ๐โโ๏ธ๐จ--restartโ when you run the pgcopydb command again, if the work directory already contains information from a previous run, then the command refuses to proceed and delete information that might be used for diagnostics and forensics. ๐
In that case, the --restart option can be used to allow pgcopydb to delete traces from a previous run. fresh start era โจ
--resumeโ when the pgcopydb command was terminated before completion โ either by an interrupt signal (such as C-c orSIGTERM) or because it crashed โ it is possible to resume the database migration. ๐ญโก๏ธ๐
When resuming activity from a previous run, table data that was fully copied over to the target server is not sent again. But table data that was interrupted during the COPY has to be started from scratch even when using --resume: the COPY command in Postgres is transactional and was rolled back. so like, no half-finished copies here, that's not a thing ๐
Same reasonning applies to the CREATE INDEX commands and ALTER TABLE commands that pgcopydb issues โ those commands are skipped on a --resume run only if known to have run through to completion on the previous one. ๐
Finally, using --resume requires the use of --not-consistent.
--not-consistentโ okay so, in order to be consistent, pgcopydb exports a Postgres snapshot by calling thepg_export_snapshot()function on the source database server. That snapshot is then re-used in all the connections to the source database server by using theSET TRANSACTION SNAPSHOTcommand. ๐
Per the Postgres documentation about pg_export_snapshot:
Saves the transaction's current snapshot and returns a text string identifying the snapshot. This string must be passed (outside the database) to clients that want to import the snapshot. The snapshot is available for import only until the end of the transaction that exported it.
Now, when the pgcopydb process was interrupted (or crashed) on a previous run, it is possible to resume operations โ but the snapshot that was exported does not exist anymore. The pgcopydb command can only resume operations with a new snapshot, and thus can not ensure consistency of the whole data set, because each run is now using their own snapshot. two different timelines, bestie ๐
--snapshotโ instead of exporting its own snapshot by calling the PostgreSQL functionpg_export_snapshot(), it is possible for pgcopydb to re-use an already exported snapshot. you bring the snapshot, we'll bring the copy ๐ฆ--followโ when the--followoption is used, then pgcopydb implements Change Data Capture as detailed in the manual page forpgcopydb follow, in parallel to the main copy database steps. โญ
The replication slot is created using the same snapshot as the main database copy operation, and the changes to the source database are prefetched only during the initial copy, then prefetched and applied in a catchup process. ๐ซ
It is possible to give pgcopydb clone --follow a termination point (the LSN endpos) while the command is running, with the command pgcopydb stream sentinel set endpos. iconic control fr ๐
--pluginโ which logical decoding output plugin we're pulling up with ๐ the default ispgoutput, which is literally built into PostgreSQL core (since Postgres 10) so you do not need to install any extension on the source server at all. Usingpgoutputmeans no superuser needed just to install an extension, and it plays nice with any PostgreSQL publication-based setup. No drama, no extra installs, we love that for us ๐ซถ
You can also roll with test_decoding or wal2json โ both are still supported for backwards compatibility. See: https://www.postgresql.org/docs/current/protocol-logical-replication.html, https://www.postgresql.org/docs/current/test-decoding.html, https://github.com/eulerto/wal2json/
--wal2json-numeric-as-stringโ if you're using the wal2json output plugin, you can pass--wal2json-numeric-as-stringto tell wal2json to output numeric values as strings, which lowkey saves you from some precision loss ๐ญ no more numbers getting quietly mangled, we stan.
Heads up bestie: your source database needs a wal2json plugin version that supports the --numeric-data-types-as-string option, otherwise this does nothing for you.
Also peep the wal2json docs about this option for the details: https://github.com/eulerto/wal2json/pull/255
--slot-nameโ the logical decoding slot name to use โจ it defaults topgcopydb, which is genuinely unfortunate if your use-case involves migrating more than one database from the source server (like, now they're fighting over the same name ๐).--create-slotโ tellspgcopydbto go ahead and create the logical replication slot to use ๐--endposโ the logical replication target LSN to use ๐ฏ it automatically stops replication and exits with normal exit status0once receiving reaches the specified LSN. And if there's a record with an LSN exactly equal to lsn, that record will be output.
Okay so important one: --endpos is not aware of transaction boundaries and may truncate output partway through a transaction ๐จ any partially output transaction will not be consumed and will be replayed again when the slot is next read from. Individual messages, though, are never truncated. So likeโฆ it's giving messy breakup with a transaction mid-sentence.
See also the docs for pg_recvlogical: https://www.postgresql.org/docs/current/app-pgrecvlogical.html
--use-copy-binaryโ use theCOPY WITH (FORMAT BINARY)instead of the plainCOPYcommand ๐
See also the docs for COPY: https://www.postgresql.org/docs/current/sql-copy.html
--originโ the logical replication target system needs to track the transactions that have already been applied, so that if we get disconnected or need to resume operations, we can skip the already-replayed transaction โ no re-doing work, periodt.
Postgres uses a notion of an origin node name, as documented in Replication Progress Tracking. This option lets you pick your own node name, and it defaults to "pgcopydb". Picking a different name is useful in some advanced scenarios โ like migrating several sources into the same target, where each source should have its own unique origin node name ๐ง
See: https://www.postgresql.org/docs/current/replication-origins.html
--verbose,--noticeโ increase the current verbosity ๐ข the default level of verbosity isINFO. In ascending order,pgcopydbknows about these verbosity levels:FATAL,ERROR,WARN,INFO,NOTICE,SQL,DEBUG,TRACE.--debugโ set the current verbosity toDEBUGlevel ๐--traceโ set the current verbosity toTRACElevel ๐ต๏ธโโ๏ธ--quietโ set the current verbosity toERRORlevel ๐คซ
๐ฑ ENVIRONMENT ๐ฑ
no cap, this whole section is just the ~vibe config~ layer, bestie โ a bunch of environment variables that stand in for command-line flags when you'd rather not type them out every single time ๐ . Every one of these is basically a "hey if you didn't pass the flag, I gotchu" backup plan. let's go through them one by one, mwah ๐ซถ
PGCOPYDB_SOURCE_PGURIโ the connection string to your source Postgres instance. when--sourceis left off the command line, this env var is the one that gets used, periodt.PGCOPYDB_TARGET_PGURIโ connection string to the target Postgres instance. when--targetis omitted from the command line, this environment variable steps in. ๐PGCOPYDB_TABLE_JOBSโ how many concurrent jobs are allowed to run COPY operations in parallel. when--table-jobsis omitted from the command line, this env var is used.PGCOPYDB_INDEX_JOBSโ how many concurrent jobs are allowed to run CREATE INDEX operations in parallel. when--index-jobsis omitted from the command line, this environment variable is used. โจPGCOPYDB_RESTORE_JOBSโ how many concurrent jobs are allowed to run pg_restore operations in parallel. when--restore-jobsis omitted from the command line, this environment variable is used.PGCOPYDB_LARGE_OBJECTS_JOBSโ how many concurrent jobs are allowed to copy Large Objects data in parallel. when--large-objects-jobsis omitted from the command line, then this environment variable is used. ๐PGCOPYDB_SPLIT_TABLES_LARGER_THANโ lets you use Same-table Concurrency when processing the source database. this one wants a byte size, and the byte units it knows about areB,kB,MB,GB,TB,PB, andEB. when--split-tables-larger-thanis omitted from the command line, then this environment variable is used. ๐ฆPGCOPYDB_SPLIT_MAX_PARTSโ caps the maximum number of parts when Same-table Concurrency is in play. when--split-max-partsis omitted from the command line, then this environment variable is used.PGCOPYDB_ESTIMATE_TABLE_SIZESโ when it's true (oryes, oron, or1โ same input as a Postgres boolean, iykyk) then pgcopydb estimates the size of tables to figure out whether or not to split tables. this option is only useful when querying the relation sizes on the source database is costly, ngl. when--estimate-table-sizesis omitted from the command line, then this environment variable is used.
when this option is used, we run vacuumdb --analyze-only --jobs=<table-jobs> on the source database, and that updates the statistics for the number of pages for each relation. later, we use the number of pages, and the size for each page, to estimate the actual size of the tables. ๐ง
if you'd rather run the ANALYZE command manually before running pgcopydb, you can use the --skip-analyze option or the PGCOPYDB_SKIP_ANALYZE environment variable. this way, you can decrease the time spent on the migration. โญ
PGCOPYDB_OUTPUT_PLUGINโ the logical decoding output plugin to use. when--pluginis omitted from the command line, then this environment variable is used.PGCOPYDB_WAL2JSON_NUMERIC_AS_STRINGโ when it's true (oryes, oron, or1, same input as a Postgres boolean) then pgcopydb uses the wal2json option--numeric-data-types-as-stringwhen using the wal2json output plugin. when--wal2json-numeric-as-stringis omitted from the command line then this environment variable is used. ๐PGCOPYDB_DROP_IF_EXISTSโ when it's true (oryes, oron, or1, same input as a Postgres boolean) then pgcopydb uses the pg_restore options--clean --if-existswhen creating the schema on the target Postgres instance. when--drop-if-existsis omitted from the command line then this environment variable is used.PGCOPYDB_FAIL_FASTโ when it's true (oryes, oron, or1, same input as a Postgres boolean) then pgcopydb sends theTERMsignal to all the processes in its process group as soon as one process terminates with a non-zero return code. when--fail-fastis omitted from the command line then this environment variable is used. ๐ญPGCOPYDB_SKIP_VACUUMโ when it's true (oryes, oron, or1, same input as a Postgres boolean) then pgcopydb skips the VACUUM ANALYZE jobs entirely, same as when using the--skip-vacuumoption.PGCOPYDB_SKIP_ANALYZEโ when it's true (oryes, oron, or1, same input as a Postgres boolean) then pgcopydb skips thevacuumdb --analyze-onlycommands entirely, same as when using the--skip-analyzeoption.PGCOPYDB_SKIP_DB_PROPERTIESโ when it's true (oryes, oron, or1, same input as a Postgres boolean) then pgcopydb skips theALTER DATABASET SETproperties commands that copy the setting from the source to the target database, same as when using the--skip-db-propertiesoption. ๐PGCOPYDB_SKIP_CTID_SPLITโ when it's true (oryes, oron, or1, same input as a Postgres boolean) then pgcopydb skips the CTID split operation during the clone process, same as when using the--skip-split-by-ctidoption.PGCOPYDB_USE_COPY_BINARYโ when it's true (oryes, oron, or1, same input as a Postgres boolean) then pgcopydb uses theCOPY WITH (FORMAT BINARY)instead of the COPY command, same as when using the--use-copy-binaryoption. โจPGCOPYDB_SNAPSHOTโ the Postgres snapshot identifier to re-use, see also--snapshot.TMPDIRโ the pgcopydb command creates all its work files and directories in${TMPDIR}/pgcopydb, and it defaults to/tmp/pgcopydb. ๐PGCOPYDB_LOG_TIME_FORMATโ the logs time format defaults to%H:%M:%Swhen pgcopydb is used on an interactive terminal, and to%Y-%m-%d %H:%M:%Sotherwise. this environment variable can be set to any format string other than the defaults.
see documentation for strftime(3) for details about the format string. see documentation for isatty(3) for details about detecting if pgcopydb is run in an interactive terminal. ๐ฅ
PGCOPYDB_LOG_JSONโ when it's true (oryes, oron, or1, same input as a Postgres boolean) then pgcopydb formats its logs using JSON. it's giving structured logging, bestie:
{
"timestamp": "2023-04-13 16:53:14",
"pid": 87956,
"error_level": 4,
"error_severity": "INFO",
"file_name": "main.c",
"file_line_num": 165,
"message": "Running pgcopydb version 0.11.19.g2290494.dirty from \"/Users/dim/dev/PostgreSQL/pgcopydb/src/bin/pgcopydb/pgcopydb\""
}
PGCOPYDB_LOG_FILENAMEโ when it's set to a filename (in a directory that must exist already) then pgcopydb writes its logs output to that filename in addition to the logs on the standard error output stream. ๐
if the file already exists, its content is overwritten. in other words, the previous content would be lost when running the same command twice. ๐
PGCOPYDB_LOG_JSON_FILEโ when it's true (oryes, oron, or1, same input as a Postgres boolean) then pgcopydb formats its logs using JSON when writing toPGCOPYDB_LOG_FILENAME. ๐XDG_DATA_HOMEโ the standard XDG Base Directory Specification defines several environment variables that allow controlling where programs should store their files.
XDG_DATA_HOMEdefines the base directory relative to which user-specific data files should be stored. If$XDG_DATA_HOMEis either not set or empty, a default equal to$HOME/.local/shareshould be used.
when using Change Data Capture (through the --follow option and Postgres logical decoding with wal2json) then pgcopydb pre-fetches changes in JSON files and transforms them into SQL files to apply to the target database. ๐ฆ
these files are stored at the following location, tried in this order:
- when
--diris used, then pgcopydb uses thecdcsubdirectory of the--dirlocation, - when
XDG_DATA_HOMEis set in the environment, then pgcopydb uses that location, - when neither of the previous settings have been used then pgcopydb defaults to using
${HOME}/.local/share.
๐ฌ EXAMPLES ๐ฌ
okay bestie this is the moment, this is the whole plot, let's go ๐ธ
first you set your env vars so you don't have to retype URIs like a peasant, then you just send it:
$ export PGCOPYDB_SOURCE_PGURI=postgres://pagila:0wn3d@source/pagila
$ export PGCOPYDB_TARGET_PGURI=postgres://pagila:0wn3d@target/pagila
$ export PGCOPYDB_DROP_IF_EXISTS=on
$ pgcopydb clone --table-jobs 8 --index-jobs 12
08:13:13.961 42893 INFO [SOURCE] Copying database from "postgres://pagila:0wn3d@source/pagila?keepalives=1&keepalives_idle=10&keepalives_interval=10&keepalives_count=60"
08:13:13.961 42893 INFO [TARGET] Copying database into "postgres://pagila:0wn3d@target/pagila?keepalives=1&keepalives_idle=10&keepalives_interval=10&keepalives_count=60"
08:13:14.009 42893 INFO Using work dir "/tmp/pgcopydb"
08:13:14.017 42893 INFO Exported snapshot "00000003-000000EB-1" from the source database
08:13:14.019 42904 INFO STEP 1: fetch source database tables, indexes, and sequences
08:13:14.339 42904 INFO Fetched information for 5 tables (including 0 tables split in 0 partitions total), with an estimated total of 1000 thousands tuples and 128 MB on-disk
08:13:14.342 42904 INFO Fetched information for 4 indexes (supporting 4 constraints)
08:13:14.343 42904 INFO Fetching information for 1 sequences
08:13:14.353 42904 INFO Fetched information for 1 extensions
08:13:14.436 42904 INFO Found 1 indexes (supporting 1 constraints) in the target database
08:13:14.443 42904 INFO STEP 2: dump the source database schema (pre/post data)
08:13:14.448 42904 INFO /usr/bin/pg_dump -Fc --snapshot 00000003-000000EB-1 --section=pre-data --section=post-data --file /tmp/pgcopydb/schema/schema.dump 'postgres://pagila:0wn3d@source/pagila?keepalives=1&keepalives_idle=10&keepalives_interval=10&keepalives_count=60'
08:13:14.513 42904 INFO STEP 3: restore the pre-data section to the target database
08:13:14.524 42904 INFO /usr/bin/pg_restore --dbname 'postgres://pagila:0wn3d@target/pagila?keepalives=1&keepalives_idle=10&keepalives_interval=10&keepalives_count=60' --section pre-data --jobs 2 --use-list /tmp/pgcopydb/schema/pre-filtered.list /tmp/pgcopydb/schema/schema.dump
08:13:14.608 42919 INFO STEP 4: starting 8 table-data COPY processes
08:13:14.678 42921 INFO STEP 8: starting 8 VACUUM processes
08:13:14.678 42904 INFO Skipping large objects: none found.
08:13:14.693 42920 INFO STEP 6: starting 2 CREATE INDEX processes
08:13:14.693 42920 INFO STEP 7: constraints are built by the CREATE INDEX processes
08:13:14.699 42904 INFO STEP 9: reset sequences values
08:13:14.700 42959 INFO Set sequences values on the target database
08:13:16.716 42904 INFO STEP 10: restore the post-data section to the target database
08:13:16.726 42904 INFO /usr/bin/pg_restore --dbname 'postgres://pagila:0wn3d@target/pagila?keepalives=1&keepalives_idle=10&keepalives_interval=10&keepalives_count=60' --section post-data --jobs 2 --use-list /tmp/pgcopydb/schema/post-filtered.list /tmp/pgcopydb/schema/schema.dump
08:13:16.751 42904 INFO All step are now done, 2s728 elapsed
08:13:16.752 42904 INFO Printing summary for 5 tables and 4 indexes
likeโฆ look at her go ๐ฆ she printed out every pg_dump and pg_restore invocation at full length (yes, with the whole URI in quotes, keepalives and all), told you the work dir, the snapshot, the work dir for the schema, all ten steps, and she did it in 2s728.
and yeah the log hands you STEP 8 before STEP 6 and STEP 7 โ that's just the order the lines land in the terminal, the steps themselves all get done by the time she says All step are now done. not a bug, just chaotic main character energy ๐ซถ
then comes the receipts โ table per table ๐
OID | Schema | Name | Parts | copy duration | transmitted bytes | indexes | create index duration
------+--------+------------------+-------+---------------+-------------------+---------+----------------------
16398 | public | pgbench_accounts | 1 | 1s496 | 91 MB | 1 | 302ms
16395 | public | pgbench_tellers | 1 | 37ms | 1002 B | 1 | 15ms
16401 | public | pgbench_branches | 1 | 45ms | 71 B | 1 | 18ms
16386 | public | table1 | 1 | 36ms | 984 B | 1 | 21ms
16392 | public | pgbench_history | 1 | 41ms | 0 B | 0 | 0ms
pgbench_accounts carried the run with 91 MB and 1s496 of copy duration โ that's the diva, that's the one doing all the work ๐
โ while pgbench_history shipped 0 B and got 0 indexes and 0ms of create index time. every table shows its OID, its Schema, its Name, its Parts, its copy duration, its transmitted bytes, its indexes and its create index duration. no secrets here, bestie.
and THEN the grand finale, the timing breakdown โจ
Step Connection Duration Transfer Concurrency
-------------------------------------------------- ---------- ---------- ---------- ------------
Catalog Queries (table ordering, filtering, etc) source 119ms 1
Dump Schema source 66ms 1
Prepare Schema target 59ms 1
COPY, INDEX, CONSTRAINTS, VACUUM (wall clock) both 2s125 18
COPY (cumulative) both 1s655 128 MB 8
CREATE INDEX (cumulative) target 343ms 2
CONSTRAINTS (cumulative) target 13ms 2
VACUUM (cumulative) target 144ms 8
Reset Sequences both 15ms 1
Large Objects (cumulative) (null) 0ms 0
Finalize Schema both 27ms 2
-------------------------------------------------- ---------- ---------- ---------- ------------
Total Wall Clock Duration both 2s728 24
each row gives you the Step, its Connection (source, target, both, or (null) when there's nothing to talk to), the Duration, the Transfer amount if there is one, and the Concurrency. you can see the COPY, INDEX, CONSTRAINTS, VACUUM (wall clock) line running 18 wide across both in 2s125, while the cumulative lines stack up underneath it: COPY 1s655 / 128 MB / 8, CREATE INDEX 343ms / 2, CONSTRAINTS 13ms / 2, VACUUM 144ms / 8. large objects sat this one out at 0ms with 0 concurrency because there were none (Skipping large objects: none found. โ she told you already ๐).
and the bottom line? Total Wall Clock Duration โ both, 2s728, concurrency 24. she ate and left absolutely no crumbs. periodt ๐ฅ
โ๏ธ AUTHOR โ๏ธ
Dimitri Fontaine โ the icon, the legend, the moment ๐
๐ COPYRIGHT ๐
2022-2024, Dimitri Fontaine โ all rights reserved, respect the queen ๐
okay bestie that's the whole page, hope you had fun and that your database is now a perfect little clone of the original ๐ง
this was pgcopydb clone from release 0.18, dated Jun 28, 2026. go copy something, i'm obsessed with you ๐๐
๐ฆ where this page comes from
- ubuntu stonking: pgcopydb 0.18-1 ยท PostgreSQL
- ๐ girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms