{"id":13466005,"url":"https://github.com/arkhipov/temporal_tables","last_synced_at":"2026-02-19T07:02:06.150Z","repository":{"id":6995686,"uuid":"8259812","full_name":"arkhipov/temporal_tables","owner":"arkhipov","description":"Temporal Tables PostgreSQL Extension","archived":false,"fork":false,"pushed_at":"2024-11-06T16:34:53.000Z","size":106,"stargazers_count":1002,"open_issues_count":27,"forks_count":51,"subscribers_count":27,"default_branch":"master","last_synced_at":"2025-12-10T23:26:46.032Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-2-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/arkhipov.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2013-02-18T01:47:12.000Z","updated_at":"2025-12-05T07:43:35.000Z","dependencies_parsed_at":"2023-09-24T10:13:28.892Z","dependency_job_id":"8bf392a4-6888-4779-b1ca-eaaef5c67c68","html_url":"https://github.com/arkhipov/temporal_tables","commit_stats":{"total_commits":64,"total_committers":10,"mean_commits":6.4,"dds":0.390625,"last_synced_commit":"585224fb945476f3df9baf8b69294b57c3dcf96c"},"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/arkhipov/temporal_tables","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arkhipov%2Ftemporal_tables","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arkhipov%2Ftemporal_tables/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arkhipov%2Ftemporal_tables/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arkhipov%2Ftemporal_tables/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/arkhipov","download_url":"https://codeload.github.com/arkhipov/temporal_tables/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arkhipov%2Ftemporal_tables/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29605799,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-19T06:47:36.664Z","status":"ssl_error","status_checked_at":"2026-02-19T06:45:47.551Z","response_time":117,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-31T15:00:37.935Z","updated_at":"2026-02-19T07:02:06.133Z","avatar_url":"https://github.com/arkhipov.png","language":"C","funding_links":[],"categories":["C"],"sub_categories":[],"readme":"Temporal Tables Extension\n=========================\n\n[![PGXN version](https://badge.fury.io/pg/temporal_tables.svg)](http://badge.fury.io/pg/temporal_tables) [![Linux](https://github.com/arkhipov/temporal_tables/actions/workflows/build-nix.yml/badge.svg)](https://github.com/arkhipov/temporal_tables/actions/workflows/build-nix.yml) [![Windows](https://github.com/arkhipov/temporal_tables/actions/workflows/build-win.yml/badge.svg)](https://github.com/arkhipov/temporal_tables/actions/workflows/build-win.yml)\n\nIntroduction\n===============\n\nA temporal table is a table that records the period of time when a row is valid.\nThere are two types of periods: the application period (also known as valid-time\nor business-time) and the system period (also known as transaction-time).\n\nThe system period is a column (or a pair of columns) with a system-maintained\nvalue that contains the period of time when a row is valid from a database\nperspective.  When you insert a row into such table, the system automatically\ngenerates the values for the start and end of the period.  When you update or\ndelete a row from a system-period temporal table, the old row is archived into\nanother table, which is called the history table.\n\nThe application period is a column (or a pair of columns) with an\napplication-maintained value that contains the period of time when a row is\nvalid from an application perspective.  This column is populated by an\napplication.\n\nNote that these two time periods do not have to be the same for a single fact.\nFor example, you may have a temporal table storing data about historical or even\nfuture facts. The application period of these facts differs from the system\nperiod which is set when we add or modify the facts into the table.\n\nCurrently, Temporal Tables Extension supports the system-period temporal tables\nonly.\n\nAdditional information on temporal databases can be found at the following\nsites:\n\n1. [Wikipedia: Temporal Database](http://en.wikipedia.org/wiki/Temporal_database)\n2. [Developing Time-Oriented Database Applications in SQL, Richard T. Snodgrass, Morgan Kaufmann Publishers, Inc., San Francisco, July, 1999, 504+xxiii pages, ISBN 1-55860-436-7.](http://www.cs.arizona.edu/~rts/tdbbook.pdf)\n3. [WG2 N1536. WG3: KOA-046. Temporal Features in SQL standard. Krishna Kulkarni,. IBM Corporation](http://metadata-standards.org/Document-library/Documents-by-number/WG2-N1501-N1550/WG2_N1536_koa046-Temporal-features-in-SQL-standard.pdf)\n\nThere is [a fantastic tutorial](http://clarkdave.net/2015/02/historical-records-with-postgresql-and-temporal-tables-and-sql-2011/)\non using and querying temporal tables in PostgreSQL with the Temporal Tables\nExtension written by [Clark Dave](https://github.com/clarkdave).\n\nRequirements\n===============\n\nTemporal Tables Extension requires PostgreSQL 9.2 or higher.\n\nInstallation\n===============\n\nIf you are running Linux, the easiest way to install the extension is to to use\nthe [PGXN client](https://pgxn.github.io/pgxnclient/).\n\n    $ pgxn install temporal_tables\n\nOr if you prefer to stick with the good old Make, you can set up the extension\nlike this:\n\n    $ make\n    $ make install\n    $ make installcheck\n\nIf you encounter an error such as:\n\n    make: no target to make.\n\nYou need to use GNU make, which may well be installed on your system as `gmake`:\n\n    $ gmake\n    $ gmake install\n    $ gmake installcheck\n\nIf you encounter an error such as:\n\n    make: pg_config: Command not found\n\nBe sure that you have pg_config installed and in your path.  If you used a\npackage management system such as RPM to install PostgreSQL, be sure that the\n-devel package is also installed.  If necessary tell the build process where to\nfind it:\n\n    $ env PG_CONFIG=/path/to/pg_config make \u0026\u0026 make install \u0026\u0026 make installcheck\n\nIf you encounter an error such as:\n\n    ERROR: must be owner of database regression\n\nYou need to run the test suite using a super user, such as the default\n\"postgres\" super user:\n\n    $ make installcheck PGUSER=postgres\n\nIf you are running Windows, you need to run the [MSBuild](https://www.microsoft.com/en-us/download/details.aspx?id=48159)\ncommand in the [Visual Studio command prompt](https://msdn.microsoft.com/en-us/library/f35ctcxw.aspx).\n\n    \u003e msbuild /p:pgversion=9.4 /p:configuration=Release /p:platform=x64\n\nThe platforms available are x64 and x86 and the PostgreSQL versions are 9.2, 9.3\n, 9.4, 9.5, 9.6, 10, 11, 12, 13, 14, 15.\n\nOr you can download the latest released zip [here](https://github.com/arkhipov/temporal_tables/releases/latest).\n\nThen you must copy the DLL from the project into the PostgreSQL's `lib`\ndirectory and the `.sql` and `.control` files into the directory\n`share\\extension`.\n\n    \u003e copy x64\\9.4\\temporal_tables.dll \"C:\\Program Files\\PostgreSQL\\9.4\\lib\"\n    \u003e copy *.control \"C:\\Program Files\\PostgreSQL\\9.4\\share\\extension\"\n    \u003e copy *.sql \"C:\\Program Files\\PostgreSQL\\9.4\\share\\extension\"\n\nOnce the extension is installed, you can add it to a database.  Connect to a\ndatabase as a super user and do this:\n\n    $ CREATE EXTENSION temporal_tables;\n\nUsage\n========\n\nCreating a system-period temporal table\n---------------------------------------\n\nTemporal Tables Extension uses a general trigger function to maintain\nsystem-period temporal tables behaviour:\n\n    versioning(\u003csystem_period_column_name\u003e, \u003chistory_table_name\u003e, \u003cadjust\u003e)\n\nThe function must be fired before INSERT or UPDATE or DELETE on a\nsystem-period temporal table.  You are to specify a system period column name, a\nhistory table name and \"adjust\" parameter (see Updating data section for\ndetails).\n\nLet's have a look at a simple example.\n\nFirst, create a table:\n\n```SQL\nCREATE TABLE employees\n(\n  name text NOT NULL PRIMARY KEY,\n  department text,\n  salary numeric(20, 2)\n);\n```\n\nIn order to make this table system-period temporal table we should first add a\nsystem period column:\n\n```SQL\nALTER TABLE employees ADD COLUMN sys_period tstzrange NOT NULL;\n```\n\nThen we need a history table that contains archived rows of our table.  The\neasiest way to create it is by using LIKE statement:\n\n```SQL\nCREATE TABLE employees_history (LIKE employees);\n```\n\nNote that a history table does not have to have the same structure as the\noriginal one.  For example, you may want to archive some columns of an original\nrow but ignore others, or a history table may contain some useful information\nthat is not necessary in the original table.  The only two requirements for a\nhistory table are:\n\n  1. A history table must contain system period column with the same name and\n     data type as in the original one.\n  2. If the history table and the original one both contain the column then the\n     data type of this column must be the same in these two tables.\n\nFinally we create a trigger on our table to link it with the history table:\n\n```SQL\nCREATE TRIGGER versioning_trigger\nBEFORE INSERT OR UPDATE OR DELETE ON employees\nFOR EACH ROW EXECUTE PROCEDURE versioning('sys_period',\n                                          'employees_history',\n                                          true);\n```\n\nInserting data\n--------------\n\nFor a user inserting data into a system-period temporal table is similar to\ninserting data into a regular table.  For example, the following data was\ninserted on August 8, 2006 to the table employees:\n\n```SQL\nINSERT INTO employees (name, department, salary)\nVALUES ('Bernard Marx', 'Hatchery and Conditioning Centre', 10000);\n\nINSERT INTO employees (name, department, salary)\nVALUES ('Lenina Crowne', 'Hatchery and Conditioning Centre', 7000);\n\nINSERT INTO employees (name, department, salary)\nVALUES ('Helmholtz Watson', 'College of Emotional Engineering', 18500);\n```\n\nThe employees table now contains the following data:\n\n  name             | department                       | salary  | sys_period\n  ---------------- | -------------------------------- | ------- | --------------\n  Bernard Marx     | Hatchery and Conditioning Centre |   10000 | [2006-08-08, )\n  Lenina Crowne    | Hatchery and Conditioning Centre |    7000 | [2006-08-08, )\n  Helmholtz Watson | College of Emotional Engineering |   18500 | [2006-08-08, )\n\nThe history table employees_history is empty:\n\n  name             | department                       | salary  | sys_period\n  ---------------- | -------------------------------- | ------- | --------------\n\nThe start of sys_period column represents the time when the row became current.\nThe trigger generates this value by using a CURRENT_TIMESTAMP value which\ndenotes the time when the first data change statement was executed in the\ncurrent transaction.\n\nUpdating data\n-------------\n\nWhen a user updates the values of columns in rows of system-period temporal\ntable, the trigger inserts a copy of the old row into the associated history\ntable.  If a single transaction makes multiple updates to the same row, only\none history row is generated.  For example, the following data was updated on\nFebruary 27, 2007 in the table employees:\n\n```SQL\nUPDATE employees SET salary = 11200 WHERE name = 'Bernard Marx';\n```\n\nThe employees table now contains the following data:\n\n  name             | department                       | salary  | sys_period\n  ---------------- | -------------------------------- | ------- | --------------\n  Bernard Marx     | Hatchery and Conditioning Centre |  11200  | [2007-02-27, )\n  Lenina Crowne    | Hatchery and Conditioning Centre |   7000  | [2006-08-08, )\n  Helmholtz Watson | College of Emotional Engineering |  18500  | [2006-08-08, )\n\nThe history table employees_history now contains the following data:\n\n  name             | department                       | salary  | sys_period\n  ---------------- | -------------------------------- | ------- | --------------\n  Bernard Marx     | Hatchery and Conditioning Centre |   10000 | [2006-08-08, 2007-02-27)\n\nUpdate conflicts and time adjustment\n------------------------------------\n\nUpdate conflicts can occur when multiple transactions are updating the same row.\nFor example, two transactions A and B are executing statements against the\nemployees table at the same time:\n\n  Time | Transaction A                                                            |  Transaction B\n  ---- | ------------------------------------------------------------------------ | -------------------------------------\n  T1   | INSERT INTO employees (name, salary) VALUES ('Bernard Marx', 10000);     |\n  T2   |                                                                          | INSERT INTO employees (name, salary) VALUES ('Lenina Crowne', 7000);\n  T3   |                                                                          | COMMIT;\n  T4   | UPDATE employees SET salary = 6800 WHERE name = 'Lenina Crowne';         |\n  T5   | INSERT INTO employees (name, salary) VALUES ('Helmholtz Watson', 18500); |\n  T6   | COMMIT;                                                                  |\n\nAfter the inserts at T1 and T2, the employees history contains the following\ndata:\n\n  name             | department                       | salary  | sys_period\n  ---------------- | -------------------------------- | ------- | --------------\n  Bernard Marx     | Hatchery and Conditioning Centre |  10000  | [T1, )\n  Lenina Crowne    | Hatchery and Conditioning Centre |   7000  | [T2, )\n\nThe history table employee_history is empty.\n\nAt time T4 the trigger must set the start of sys_period column of the row to T1\nand insert the following row into the history table:\n\n  name             | department                       | salary  | sys_period\n  ---------------- | -------------------------------- | ------- | --------------\n  Lenina Crowne    | Hatchery and Conditioning Centre |    7000 | [T2, T1)\n\nHowever, T2 \u003e T1 and the row cannot be inserted.  In this situation, the update\nat time T4 would fail with SQLSTATE 22000.  To avoid such failures, you can\nspecify \"adjust\" parameter of the trigger and set it to \"true\".  Then the start\nof sys_period column at time T4 is set to time T2 plus delta (a small interval\nof time, typically equals to 1 microsecond).  After this adjustment and the\ncompletion of transaction A, the employees table looks like this:\n\n  name             | department                       | salary  | sys_period\n  ---------------- | -------------------------------- | ------- | --------------\n  Bernard Marx     | Hatchery and Conditioning Centre |   10000 | [T1, )\n  Lenina Crowne    | Hatchery and Conditioning Centre |    6800 | [T2 + delta, )\n  Helmholtz Watson | College of Emotional Engineering |   18500 | [T1, )\n\nThe history table employees_history contains the following data:\n\n  name             | department                       | salary  | sys_period\n  ---------------- | -------------------------------- | ------- | --------------\n  Lenina Crowne    | Hatchery and Conditioning Centre |    7000 | [T2, T2 + delta)\n\nDeleting data\n-------------\n\nWhen a user deletes data from a system-period temporal table, the trigger adds\nrows to the associated history table.  For example, the following data was\ndeleted on 24 December, 2012 from the table employees:\n\n```SQL\nDELETE FROM employees WHERE name = 'Helmholtz Watson';\n```\n\nThe employees table now contains the following data:\n\n  name             | department                       | salary  | sys_period\n  ---------------- | -------------------------------- | ------- | --------------\n  Bernard Marx     | Hatchery and Conditioning Centre |   10000 | [2007-02-27, )\n  Lenina Crowne    | Hatchery and Conditioning Centre |    7000 | [2006-08-08, )\n\nThe history table employees_history now looks like this:\n\n  name             | department                       | salary  | sys_period\n  ---------------- | -------------------------------- | ------- | --------------\n  Bernard Marx     | Hatchery and Conditioning Centre |   10000 | [2006-08-08, 2007-02-27)\n  Helmholtz Watson | College of Emotional Engineering |   18500 | [2006-08-08, 2012-12-24)\n\nAdvanced usage\n==============\n\nInstead of using CURRENT_TIMESTAMP, you may want to set a custom system time for\nversioning triggers.  It is useful for creating a data warehouse from a system\nthat recorded a system time and you want to use that time instead.\n\n```SQL\nSELECT set_system_time('1985-08-08 06:42:00+08');\n```\n\nTo revert it back to the default behaviour, call the function with `NULL` as its\nargument.\n\n```SQL\nSELECT set_system_time(NULL);\n```\n\nIf the `set_system_time` function is issued within a transaction that is later\naborted, all the changes are undone.  If the transaction is committed, the\nchanges will persist until the end of the session.\n\nExamples and hints\n=====================\n\nUsing inheritance when creating history tables\n----------------------------------------------\n\nIn the example above we used LIKE statement to create the history table,\nsometimes it is better to use inheritance for this task.  For example:\n\n```SQL\nCREATE TABLE employees_history\n(\n  name text NOT NULL,\n  department text,\n  salary numeric(20, 2),\n  sys_period tstzrange NOT NULL\n);\n```\n\nThen create the employees table:\n\n```SQL\nCREATE TABLE employees (PRIMARY KEY(name)) INHERITS (employees_history);\n```\n\nPruning history tables\n----------------------\n\nHistory tables are always growing and so are consuming an increasing amount of\nstorage.  There are several ways you can prune old data from a history table:\n\n  1. Periodically delete old data from a history table.\n  2. Use partitioning and detach old partitions from a history table (for more\n     information on table parititioning see PostgreSQL documentation).\n\nThere are many possible rules for pruning old rows:\n\n  1. Prune rows older than a certain age.\n  2. Retain only the latest N versions of a row.\n  3. Prune rows when a corresponding row is deleted from the system-period\n     temporal table.\n  4. Prune rows that satisfy the specified business rules.\n\nYou can also set another tablespace for a history table to move it on a cheaper\nstorage.\n\nUsing system-period temporal tables for data audit\n--------------------------------------------------\n\nIt is possible to use system-period temporal tables for data audit.  For\nexample, you can add the following triggers to save user that modified or\ndeleted the current row:\n\n```SQL\nCREATE FUNCTION employees_modify()\nRETURNS TRIGGER AS $$\nBEGIN\n  NEW.user_modified = SESSION_USER;\n  RETURN NEW;\nEND;\n$$ LANGUAGE plpgsql;\n\nCREATE TRIGGER employees_modify\nBEFORE INSERT OR UPDATE ON employees\nFOR EACH ROW EXECUTE PROCEDURE employees_modify();\n\nCREATE FUNCTION employees_delete()\nRETURNS TRIGGER AS $$\nBEGIN\n  NEW.user_deleted = SESSION_USER;\n  RETURN NEW;\nEND;\n$$ LANGUAGE plpgsql;\n\nCREATE TRIGGER employees_delete\nBEFORE INSERT ON employees_history\nFOR EACH ROW EXECUTE PROCEDURE employees_delete();\n```\n\nNotes\n========\n\nTemporal Tables Extension is distributed under the terms of BSD 2-clause\nlicense. See LICENSE or http://www.opensource.org/licenses/bsd-license.php for\nmore details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farkhipov%2Ftemporal_tables","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Farkhipov%2Ftemporal_tables","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farkhipov%2Ftemporal_tables/lists"}