Contents
Operations
This page covers correctness, performance and production limitations. For an introduction, read Getting started and Concepts.
Limitations and Portability
These limitations are documented rather than silently ignored.
- Portability is scoped. Logical checksums exclude relation OIDs, heap TIDs and type OIDs. Built-in types, enums, numeric, arrays, composites, domains and ranges use a canonical encoding. Cross-database comparison is intended for the same PostgreSQL major version and compatible schema/type names; custom types whose send/output representation changes between versions may still produce different values.
- Custom types are type-defined. A user-defined type is hashed through its binary send function, or its output function when no send function exists. If that representation differs between databases, the checksum differs.
- Endianness. Logical checksums use a canonical little-endian encoding for the fixed-width integers in arrays, composites and multiranges, and are therefore identical on little-endian and big-endian systems. Physical checksums remain architecture- and storage-dependent.
- Physical checksums are not replica fingerprints. Tuple/table physical checksums depend on tuple locations, MVCC fields, tuple bytes and the relation’s relfilenode/reltablespace; index physical checksums hash raw index pages, including page LSNs. Use physical checksums for on-disk corruption within one cluster, not for comparing replicas.
- Logical index checksums describe expected content, not stored pages.
They are derived from visible table rows and the index definition, so a
missing or corrupt index entry is not detected by this value. Use
pg_index_physical_checksumfor actual index storage. - Coverage gaps. The database physical checksum does not include TOAST
content by default, and never includes sequences, large objects (unless
include_system = true), FSM/VM forks or relation free space. Foreign table partitions are skipped by partitioned table checksums because they have no local storage in this database. - Non-cryptographic hashes. The default SQL API uses 32-bit FNV-1a; generic overloads support 32-bit and 64-bit FNV-1a. PostgreSQL page checksums are 16-bit. None of these are cryptographic hashes or provide tamper evidence.
- Collision probability. A random accidental change is missed with
probability about
2^-32(2.3e-10) by a 32-bit checksum and2^-64(5.4e-20) by a 64-bit checksum. These are rough estimates, not a cryptographic security claim. - RLS and column privileges. Relations protected by row-level security are
rejected because a direct heap scan cannot apply policies. Table-level
SELECTis required; column-only privileges are not sufficient. - Foreign tables. Foreign partitions/tables have no local storage and are skipped by partitioned checksums.
Parallel Computation
Table, index and database checksums can be computed in parallel using PostgreSQL’s parallel worker infrastructure. Each worker scans a subset of the relation’s blocks (or a subset of the database’s relations) using a shared, self-balancing atomic counter, and the partial results are combined by the leader. Logical index checksums parallelize the heap scan and evaluate the index definition in each worker.
Configuration
The degree of parallelism is controlled by the pg_integrity_checksums.workers GUC:
| Setting | Behavior |
|---|---|
pg_integrity_checksums.workers = 0 |
Disabled (default, single-backend) |
pg_integrity_checksums.workers = N |
Use up to N parallel workers |
The actual number of workers is clamped by max_parallel_workers and by the
number of blocks (or relations) being scanned. Parallelism is also skipped
when the function is already running inside a parallel worker, since
parallel workers cannot be nested.
-- Compute a table checksum with 4 parallel workers
SET pg_integrity_checksums.workers = 4;
SELECT pg_table_physical_checksum('large_table'::regclass, false);
-- Disable parallelism (restore the default)
RESET pg_integrity_checksums.workers;
Strict mode
Database checksum functions are strict by default
(pg_integrity_checksums.strict = on): if a relation in scope cannot be
checksummed, the whole call fails. This is deliberate. A partial result is
worse than no result for monitoring, because a skipped corrupt table would
otherwise look like “nothing changed”.
| Situation | strict = on (default) |
strict = off |
|---|---|---|
| Invalid index, logical checksum | error: relation "..." cannot be checksummed |
skipped, counted in the summary WARNING |
| Invalid index, physical checksum | included (its pages can still be read) | included |
| Index with an access method that has no physical checksum (for example a third-party AM) | error: relation "..." cannot be checksummed |
skipped, counted in the summary WARNING |
| Corrupt page, unreadable file | the original error, with a CONTEXT line naming the relation |
skipped, WARNING per relation plus the summary WARNING |
| Relation dropped while the scan runs | error | skipped with a WARNING |
| Unlogged table on a standby (it has no data there) | error naming the table | skipped with a WARNING |
Query cancellation, statement_timeout, out of memory, shutdown |
always propagated | always propagated |
An error looks like this:
SELECT pg_database_logical_checksum(false, true);
-- ERROR: relation "orders_old_idx" cannot be checksummed
-- DETAIL: The index is invalid and has no content to checksum.
-- HINT: Set pg_integrity_checksums.strict = false to skip relations without
-- a usable checksum.
or, for a failure while reading the relation:
ERROR: invalid page in block 0 of relation base/5/16414
CONTEXT: while computing the database checksum of relation "orders" (OID 16414)
Tables without a primary key are supported, and logical index checksums work for every access method. Physical index checksums cover B-tree, Hash, GiST, GIN, SP-GiST and BRIN.
For ad-hoc use you can allow skipping, per session or persistently:
SET pg_integrity_checksums.strict = off;
SELECT pg_database_logical_checksum(false, true);
-- WARNING: skipping relation 16438 while computing database checksum
-- WARNING: database checksum skipped 1 relation(s)
-- HINT: Set pg_integrity_checksums.strict = on to fail with the reason ...
ALTER DATABASE appdb SET pg_integrity_checksums.strict = off; -- persistent
With strict mode off, a skipped relation is excluded from the result and is
only reported as a warning (in parallel workers only in the server log), so do
not use that value for integrity monitoring. Before 2.5.0 the default was
off. The CLI’s --strict flag is separate from this setting; it controls
the CLI’s own preflight, not the server GUC.
Investigating a mismatch
A database or table checksum differs from the baseline. Work from the outside in:
- Physical or logical? A physical checksum also changes after
VACUUM FULL,CLUSTER,REINDEXor moving a table to another tablespace; that is expected. A logical difference means the data (or a column or type name) changed. - Freeze the view of the data. Run the comparison in one transaction, so that every call sees the same snapshot:
BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY;
- Find the table. Compare per-table logical checksums on both sides (or against the baseline) and diff the two lists:
SELECT c.oid::regclass AS tbl, pg_table_logical_checksum(c.oid) AS checksum
FROM pg_class c
WHERE c.relkind IN ('r', 'm') AND c.relnamespace = 'public'::regnamespace
ORDER BY 1;
The CLI does this for you: pg_integrity_checksums verify reports each
object that differs.
4. Find the rows. Within a table, compare row checksums by key:
SELECT id, pg_tuple_logical_checksum('orders'::regclass, ctid)
FROM orders ORDER BY id;
- If the data is identical but the storage is suspect, verify the pages
against their stored checksums (needs
data_checksums = on). The check reads the on-disk copy of each page, so runCHECKPOINTfirst; pages that have not been written yet are reported asnew:
SELECT blk, pg_page_checksum_status('orders'::regclass, blk) AS status
FROM generate_series(0,
(pg_relation_size('orders') / current_setting('block_size')::int)::int - 1) AS blk
WHERE pg_page_checksum_status('orders'::regclass, blk) = 'mismatch';
- If a database call fails instead of returning a value, see
relation "..." cannot be checksummed.
Correctness
Because aggregation is order- and partition-independent, the parallel result is always identical to the serial result for the same data state, regardless of the number of workers. The same consistent snapshot is propagated to all workers, so every tuple is checksummed as of the same point in time.
Snapshot consistency across calls
One SQL statement sees one snapshot. A single pg_table_logical_checksum or
pg_database_logical_checksum call is therefore internally consistent, but two
separate calls under the default READ COMMITTED isolation can see different
points in time. A table and its index, or several tables, should be checked
inside one transaction when the comparison matters:
BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY;
SELECT pg_table_logical_checksum('orders'::regclass);
SELECT pg_table_logical_checksum('customers'::regclass);
COMMIT;
The CLI already does this for capture and verify. A long repeatable-read
transaction keeps an xmin horizon and can delay VACUUM; keep checksum
transactions as short as the workload allows and monitor long-running sessions.
Practical Applications
1. Data Integrity Monitoring
-- Create checksum monitoring system
CREATE TABLE checksum_audit_log (
audit_time timestamptz DEFAULT now(),
table_name text,
checksum_type text, -- 'physical' or 'logical'
checksum_value bigint,
PRIMARY KEY (table_name, checksum_type, audit_time)
);
-- Periodic monitoring of critical tables
INSERT INTO checksum_audit_log (table_name, checksum_type, checksum_value)
SELECT
'transactions',
'physical',
pg_table_physical_checksum('transactions'::regclass, false)
UNION ALL
SELECT
'transactions',
'logical',
pg_table_logical_checksum('transactions'::regclass);
2. Replication Consistency Validation
-- Compare source and replica logical content.
-- Logical checksums exclude relation OIDs, heap TIDs and type OIDs, so they
-- are the appropriate values to compare when schema/type names and row
-- content match. Physical checksums include page LSNs, tuple locations and
-- other storage details, so they will normally differ on a standby.
-- Run on source and replica and compare logical_cs only:
SELECT
c.relname,
pg_table_logical_checksum(c.oid) AS logical_cs,
pg_table_physical_checksum(c.oid, false) AS physical_cs
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public'
AND c.relkind IN ('r', 'p', 'm')
ORDER BY c.relname;
3. Data Migration Verification
-- Verify migration integrity. Logical checksums omit relation OIDs and heap
-- TIDs, so values are comparable between source and target databases.
-- Run this query on both databases and compare the two result sets.
WITH table_checksums AS (
SELECT
c.relname,
pg_table_logical_checksum(c.oid) AS checksum
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public'
AND c.relkind IN ('r', 'p', 'm')
)
SELECT * FROM table_checksums ORDER BY relname;
4. Compare Against a Known-Good Value
A checksum is useful only when compared with a value recorded at a known-good point. The audit table from the first example provides that history:
WITH expected AS (
SELECT checksum_value
FROM checksum_audit_log
WHERE table_name = 'transactions'
AND checksum_type = 'logical'
ORDER BY audit_time DESC
LIMIT 1
)
SELECT
pg_table_logical_checksum('transactions'::regclass) AS current_value,
expected.checksum_value AS expected_value,
pg_table_logical_checksum('transactions'::regclass) = expected.checksum_value AS matches
FROM expected;
pg_page_checksum only calculates PostgreSQL’s 16-bit page checksum. It does
not compare the value with the stored pd_checksum, and 0 is ambiguous
(new page or a genuine zero). Use pg_page_checksum_status when you want a
verification result. data_checksums = on is required for stored page
checksums to be meaningful; without it the status is checksums_disabled.
5. Batch Change Auditing
Record checksums after a batch, ETL run or maintenance window. Do not compute a whole-table checksum in a per-row trigger: that makes every write O(table) and would flag every legitimate change as suspicious.
-- Record the state once, after the batch has finished.
INSERT INTO checksum_audit_log (table_name, checksum_type, checksum_value)
SELECT 'transactions', 'logical',
pg_table_logical_checksum('transactions'::regclass)
UNION ALL
SELECT 'transactions', 'physical',
pg_table_physical_checksum('transactions'::regclass, false);
Compare later runs with the most recent known-good row, for example with the query in section 4.
6. Restore Validation
Record a checksum before the backup, then compare the restored table with that recorded value:
-- Before backup: record a known-good logical checksum.
INSERT INTO checksum_audit_log (table_name, checksum_type, checksum_value)
VALUES ('transactions', 'logical',
pg_table_logical_checksum('transactions'::regclass));
-- After restore: compare with the latest recorded value.
WITH expected AS (
SELECT checksum_value
FROM checksum_audit_log
WHERE table_name = 'transactions'
AND checksum_type = 'logical'
ORDER BY audit_time DESC
LIMIT 1
)
SELECT
pg_table_logical_checksum('transactions'::regclass) = expected.checksum_value AS restored_ok
FROM expected;
A database-level checksum is also available when the recorded value was
produced by pg_database_logical_checksum; do not compare a table checksum
with a database checksum.
Platform Support
CI runs regression and TAP tests for PostgreSQL 12–19 on:
- Linux x86_64 and arm64
- macOS arm64
- Windows x86_64
- FreeBSD amd64 and arm64
- Linux s390x (big-endian, manually triggered Big-endian workflow)
Logical checksums use a canonical little-endian encoding for the few fixed-width integers in the canonical form (array shape, composite field count and multirange count). They are therefore identical on little-endian and big-endian systems. Physical checksums remain storage- and architecture-dependent by design; use logical checksums for cross-platform comparisons.