<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Baymeev Ruslan</title>
    <description>The latest articles on DEV Community by Baymeev Ruslan (@pgsuite).</description>
    <link>https://dev.to/pgsuite</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4177498%2F62cdc070-d1fa-4fd4-aa3b-fef6608d3d6d.png</url>
      <title>DEV Community: Baymeev Ruslan</title>
      <link>https://dev.to/pgsuite</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pgsuite"/>
    <language>en</language>
    <item>
      <title>PGHist | History and audit of table changes in PostgreSQL</title>
      <dc:creator>Baymeev Ruslan</dc:creator>
      <pubDate>Sat, 07 Nov 2026 19:04:47 +0000</pubDate>
      <link>https://dev.to/pgsuite/pghist-history-and-audit-of-table-changes-in-postgresql-356</link>
      <guid>https://dev.to/pgsuite/pghist-history-and-audit-of-table-changes-in-postgresql-356</guid>
      <description>&lt;p&gt;Hello community!&lt;/p&gt;

&lt;p&gt;I have developed &lt;strong&gt;PGHist&lt;/strong&gt; — an open-source utility designed for saving, auditing, and viewing table change history in PostgreSQL.&lt;/p&gt;

&lt;p&gt;🎯 &lt;strong&gt;Target audience:&lt;/strong&gt; PGHist is specifically tailored for &lt;strong&gt;medium-sized projects&lt;/strong&gt; and &lt;strong&gt;low-load tables within larger systems&lt;/strong&gt;.&lt;br&gt;
Fully handles change auditing (log + temporal tables) but focuses primarily on displaying modifications to users.&lt;/p&gt;

&lt;p&gt;Setting up a trigger-based audit involves creating a schema from one SQL file.&lt;/p&gt;

&lt;p&gt;Unlike other trigger-based tools that dump massive JSONB deltas (like Logidze or supa_audit), it stores history in a structured, normalized format (row → query → transaction), making it much more convenient to analyze and query data.&lt;br&gt;
Also, unlike Debezium, it requires no binary components — it installs as a pure SQL schema, so all history is saved within your standard database dumps and backups.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/PGSuite/PGHist/" rel="noopener noreferrer"&gt;https://github.com/PGSuite/PGHist/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://pghist.org/" rel="noopener noreferrer"&gt;https://pghist.org/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Working principle
&lt;/h3&gt;

&lt;p&gt;When history tracking is enabled for a target table, the tool automatically sets up statement-level triggers (for &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, &lt;code&gt;TRUNCATE&lt;/code&gt;) along with a set of stored procedures and views for data retrieval.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data capture:&lt;/strong&gt; When a table is modified, triggers fire and efficiently copy only the modified fields to the history schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DDL autocorrection:&lt;/strong&gt; Special event triggers detect DDL operations (like &lt;code&gt;ALTER TABLE&lt;/code&gt;), automatically rebuilding the history tables and recreating the corresponding stored procedures.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Simple example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Create table&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Enable keeping history&lt;/span&gt;
&lt;span class="k"&gt;call&lt;/span&gt; &lt;span class="n"&gt;pghist&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hist_enable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'example'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Change data&lt;/span&gt;
&lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="n"&gt;example&lt;/span&gt; &lt;span class="k"&gt;values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Example'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;current_date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="n"&gt;example&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- View change log by row&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;example_hist&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- View changes by field&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;example_changes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- View table at timestamp &lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;example_at_timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'10 second'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Key features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storage optimization:&lt;/strong&gt; saves only old values of changed fields and primary key. The Transaction-Expression-Row storage structure matches the operation of the DBMS and minimizes redundancy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Statement-level triggers:&lt;/strong&gt; DML triggers are implemented with the for each statement activation level and use transition tables for batch inserting of changes. This completely removes row-by-row processing and minimizes DBMS overhead during bulk operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versatility:&lt;/strong&gt; possible to get change log, list of only changed data from several tables and table at a point in time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Descriptions:&lt;/strong&gt; for each table and its columns, it is possible to define SQL expressions to describe changed rows and field values. By default, descriptions are created for foreign key fields and table rows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inheritance:&lt;/strong&gt; stored procedures have parameter "cascade" that allows you to get data with or without inheritance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction and SQL statements:&lt;/strong&gt; end-to-end hierarchical tracking (Change→Query→Transaction) that delivers a complete, structured picture of all modifications. You can quickly retrieve every single edit made within a specific transaction or fetch all rows updated by a particular SQL statement&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Condition (optional):&lt;/strong&gt; when getting a list of changes, can specify a condition on the primary key or foreign key referencing the master table&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autocorrection:&lt;/strong&gt; when performing DDL operations on a table (such as alter table, create index, etc.), an event trigger fires to automatically adjust history tracking. When a table is dropped, its history is automatically deleted&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Main restrictions (technical trade-offs)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Update primary key values:&lt;/strong&gt; updating the values of primary key columns is not supported (exception will be raised)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Significant table changes:&lt;/strong&gt; when the table is significantly changed (for example, reassigning the primary key to another field), the history table may be incorrectly rebuilt, recommended to remove and create history again (pghist.hist_disable and pghist.hist_enable)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write overhead:&lt;/strong&gt; write latency will increase (roughly ~2x based on pgbench tasks) because it captures changes using triggers and splits them dynamically across structured tables &lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  💬 Let's discuss!
&lt;/h3&gt;

&lt;p&gt;What crucial features, edge cases, or architectural pitfalls do you see in this trigger-based approach ?&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>opensource</category>
      <category>sql</category>
      <category>database</category>
    </item>
  </channel>
</rss>
