Description


Tool PGHist keeps history of table changes and allows to get a log(audit) of changes by row, list of changes by field indicating user, time of the change, SQL query, transaction, other technical information and table as of date-time in the past (versioning). To display information in user interface, SQL expressions are defined to describe changed table rows and fields. It is possible to override the operation name and username functions.

Design and working principle



PGHist is a schema with procedures and common tables: transactions, SQL expressions. When history is enabled (procedure pghist.hist_enable), for specified table created additional table, triggers for insert,update,delete,truncate, stored procedures and view for obtaining data. When a table is changed, triggers are fired that modify the history table. There are also event triggers that rebuild the history table and recreate the stored procedures.


Main functions and view



pghist.hist_enable([schema],[table]) enable history keeping
[schema].[table]_hist log(audit) of changes by row, optimized for analysis
[schema].[table]_changes list of changes by field, optimized for display to the user
[schema].[table]_at_timestamp table at date-time in the past (versioning)

More details on page Documentation


Simple example


-- Create table
create table example(
  id int primary key,
  name varchar(20),
  number numeric(10,2),
  date date
);

-- Enable keeping history
call pghist.hist_enable('example');

-- Change data
insert into example values (1, 'Example', 10, current_date);
update example set number=20, date=date-1;

-- View change log by row
select * from example_hist;

-- View changes by field
select * from example_changes() order by 1,2,3;

-- View table at timestamp 
select * from example_at_timestamp(now()-interval '10 second');

-- drop table example cascade;


Important qualities



Restrictions



Data schema


Data schema

Performance benchmark


When a single DML operation is executed, a statement-level trigger (for each statement) captures the changes and distributes them across four tables.

Distributing records across tables
TableNumber of recordsData to be written
Transactions
pghist.hist_transaction
1General information about the operation (date-time, user, IP address, etc.)
SQL statements
pghist.hist_statement
1Identifier and SQL statement information
SQL query texts
pghist.hist_query
0 or 1The source code of the SQL query without duplication
History table
pghist.data$[schema]_[table]
number of rows
in an SQL query
Primary key, foreign key to the master table (optional), and only changed data from rows in the trigger transition table (new table/old table)

Consequently, modifying 1 row in the target table will result in 4 additional rows being written, while modifying 1,000 rows will result in 1,003 additional rows.

Expected Test Results: A significant increase in write latency and zero impact on read operations.
It should be noted in particular that this performance testing isolates database metrics only. These results must be evaluated in relation to network latency and business logic execution time (e.g., complex automated field population).

Bash script for performance benchmarking via pgbench


#!/bin/bash
set -e
set -x

# Test configuration
DB=benchmark
OPTIONS="-j $(nproc) -c $(( $(nproc)*4 )) -t 1000" 

# Recreate DB, init pghist extension
psql -q -c "drop database if exists $DB" -c "create database $DB"
psql $DB -q -f pghist_init.sql
pgbench $DB -i

# Run baseline tests
pgbench $DB $OPTIONS
pgbench $DB $OPTIONS -b select-only

# Enable history tracking on target tables
psql $DB -q -c "call pghist.hist_enable('pgbench_accounts')"
psql $DB -q -c "call pghist.hist_enable('pgbench_branches')"
psql $DB -q -c "call pghist.hist_enable('pgbench_tellers')"

# Run post-pghist tests
pgbench $DB $OPTIONS
pgbench $DB $OPTIONS -b select-only

Benchmark output


postgres@db-debian:~$ ./benchmark.sh
+ DB=benchmark
++ nproc
++ nproc
+ OPTIONS='-j 2 -c 8 -t 1000'
+ psql -q -c 'drop database if exists benchmark' -c 'create database benchmark'
+ psql benchmark -q -f pghist_init.sql
+ pgbench benchmark -i
dropping old tables...
creating tables...
generating data (client-side)...
vacuuming...
creating primary keys...
done in 1.00 s (drop tables 0.00 s, create tables 0.02 s, client-side generate 0.57 s, vacuum 0.22 s, primary keys 0.20 s).
+ pgbench benchmark -j 2 -c 8 -t 1000
pgbench (17.10 (Debian 17.10-1.pgdg13+1))
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 8
number of threads: 2
maximum number of tries: 1
number of transactions per client: 1000
number of transactions actually processed: 8000/8000
number of failed transactions: 0 (0.000%)
latency average = 27.442 ms
initial connection time = 34.556 ms
tps = 291.529019 (without initial connection time)
+ pgbench benchmark -j 2 -c 8 -t 1000 -b select-only
pgbench (17.10 (Debian 17.10-1.pgdg13+1))
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: simple
number of clients: 8
number of threads: 2
maximum number of tries: 1
number of transactions per client: 1000
number of transactions actually processed: 8000/8000
number of failed transactions: 0 (0.000%)
latency average = 0.899 ms
initial connection time = 39.618 ms
tps = 8896.985145 (without initial connection time)
+ psql benchmark -q -c 'call pghist.hist_enable('\''pgbench_accounts'\'')'
+ psql benchmark -q -c 'call pghist.hist_enable('\''pgbench_branches'\'')'
+ psql benchmark -q -c 'call pghist.hist_enable('\''pgbench_tellers'\'')'
+ pgbench benchmark -j 2 -c 8 -t 1000
pgbench (17.10 (Debian 17.10-1.pgdg13+1))
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 8
number of threads: 2
maximum number of tries: 1
number of transactions per client: 1000
number of transactions actually processed: 8000/8000
number of failed transactions: 0 (0.000%)
latency average = 51.236 ms
initial connection time = 31.243 ms
tps = 156.139254 (without initial connection time)
+ pgbench benchmark -j 2 -c 8 -t 1000 -b select-only
pgbench (17.10 (Debian 17.10-1.pgdg13+1))
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 1
query mode: simple
number of clients: 8
number of threads: 2
maximum number of tries: 1
number of transactions per client: 1000
number of transactions actually processed: 8000/8000
number of failed transactions: 0 (0.000%)
latency average = 1.025 ms
initial connection time = 32.404 ms
tps = 7801.795388 (without initial connection time)

Write speed dropped by ~2x, read speed remained unchanged, no errors occurred.

Summary:





PGHist vs pgAudit, temporal_tables, Logidze, supa_audit


Architectural Comparison


FeaturePGHistpgAudittemporal_tablesJSONB Triggers
(Logidze, supa_audit)
Storage locationStructured relational tablesStandard PostgreSQL server logSeparate table identical to the target tableShared table or additional column
Data structurePrimary key and changed columnsLog entry: SQL statement with parameters, user, etc.Row copy with versioning periodJSON delta changes
Implementation languagePL/pgSQLCCPL/pgSQL,
Ruby on Rails
Interception methodDML and event triggersCore hooksExtension triggersDML triggers
Security and permissionsChanges are stored in a separate schema and accessible only via protected views (security definer)Log files are protected by operating system permissionsConfigured by the administratorUsers can modify the shared table or the table log column

Feature Comparison


FeaturePGHistpgAudittemporal_tablesJSONB Triggers
(Logidze, supa_audit)
User change log✅ Full
with value descriptions
❌ Missing⚠️ Limited
data and time only
⚠️ Full
requires JSON parsing
Security audit⚠️ Write-only✅ Full❌ Missing⚠️ Write-only
Historical data
(versioning)
⚠️ Long preparation❌ No data✅ Ready immediately⚠️ JSON format
Execution context✅ Full
row→query→transaction
⚠️ Limited❌ Missing⚠️ Limited
Overhead for single
row operations
❌ Significant
4 rows across 4 tables
✅ Minimal✅ Minimal
row copying
⚠️ Moderate
Overhead for
bulk operations
⚠️ Moderate
for each statement triggers
✅ Minimal⚠️ Moderate
data duplication
⚠️ Moderate
Installation and
portability
✅ High
separate SQL schema
⚠️ Medium
standard extension
❌ Low
compilation required
✅ High
universal trigger
DDL support✅ Full
event triggers
⚠️ Partial
logs statement only
❌ Missing⚠️ Partial
Security✅ Isolated
access via views
✅ Absolute
outside the database
⚠️ Conditional
manual configuration
❌ Low
no separation of duties

Summary




Video


Review posted on YouTube