PostgreSQL 跟踪谁动了你的记录 - Use timetravel function trace tuple’s DML

4 minute read

背景

很久以前, PostgreSQL曾有内建函数来跟踪每条tuple的insert, delete时间。

现在使用timetravel也可以来模拟这个功能.

首先需要创建这个extension :

digoal=> \c digoal pg92  
You are now connected to database "digoal" as user "pg92".  
digoal=# create extension timetravel;  
digoal=# \c digoal digoal  

创建测试表 :

digoal=> create table timetravel_test (id int primary key,info text,insert_user text,update_user text, delete_user text,start_time abstime,stop_time abstime);  
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "timetravel_test_pkey" for table "timetravel_test"  
CREATE TABLE  

创建触发器 :

digoal=> create trigger timetravel  
        before insert or delete or update on timetravel_test  
        for each row  
        execute procedure  
        timetravel (start_time, stop_time, insert_user, update_user, delete_user);  
CREATE TRIGGER  

注意创建触发器的时候, insert_user, update_user, delete_user 这几个列都是可选的. 不选择的话则不跟踪是哪个用户做的insert, update, delete操作.

插入测试数据 :

digoal=> insert into timetravel_test (id,info) select generate_series(1,10),'digoal';  
INSERT 0 10  
digoal=> select * from timetravel_test ;  
 id |  info  | insert_user | update_user | delete_user |       start_time       | stop_time   
----+--------+-------------+-------------+-------------+------------------------+-----------  
  1 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  2 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  3 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  4 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  5 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  6 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  7 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  8 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  9 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
 10 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  

从测试数据来看, timetravel函数帮我们记录了这条tuple是哪个用户插入的, 并且记录了tuple的插入时间, infinity在这里表示这条tuple是有效的。

下面来更新一条记录看看 :

digoal=> update timetravel_test set info='new' where id=1;  
ERROR:  duplicate key value violates unique constraint "timetravel_test_pkey"  
DETAIL:  Key (id)=(1) already exists.  
CONTEXT:  SQL statement "INSERT INTO timetravel_test VALUES ( $1,$2,$3,$4,$5,$6,$7)"  

由于timetravel触发器的原因,更新操作实际上是把被更新的记录的stop_time修改为当前时间,再新增一条start_time为当前时间并且stop_time为infinity的记录. 所以就导致违反了ID的PK约束。

下面我们把约束去掉重新测试 :

digoal=> alter table timetravel_test drop CONSTRAINT timetravel_test_pkey;  
ALTER TABLE  
digoal=> update timetravel_test set info='new' where id=1;  
UPDATE 1  
digoal=> select * from timetravel_test ;  
 id |  info  | insert_user | update_user | delete_user |       start_time       |       stop_time          
----+--------+-------------+-------------+-------------+------------------------+------------------------  
  2 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  3 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  4 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  5 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  6 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  7 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  8 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  9 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
 10 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  1 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | 2012-01-13 15:16:42+08  
  1 | new    |             | digoal      |             | 2012-01-13 15:16:42+08 | infinity  

没错,更新操作实际上是把被更新的记录的stop_time修改为当前时间,再新增一条start_time为当前时间并且stop_time为infinity的记录.

再更新一次 :

digoal=> update timetravel_test set info='new2' where id=1;  
UPDATE 1  
digoal=> select * from timetravel_test ;  
 id |  info  | insert_user | update_user | delete_user |       start_time       |       stop_time          
----+--------+-------------+-------------+-------------+------------------------+------------------------  
  2 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  3 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  4 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  5 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  6 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  7 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  8 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  9 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
 10 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  1 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | 2012-01-13 15:16:42+08  
  1 | new    |             | digoal      |             | 2012-01-13 15:16:42+08 | 2012-01-13 15:17:22+08  
  1 | new2   |             | digoal      |             | 2012-01-13 15:17:22+08 | infinity  

接下来测试一下删除操作 :

digoal=> delete from timetravel_test where id=2;  
DELETE 1  
digoal=> select * from timetravel_test ;  
 id |  info  | insert_user | update_user | delete_user |       start_time       |       stop_time          
----+--------+-------------+-------------+-------------+------------------------+------------------------  
  3 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  4 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  5 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  6 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  7 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  8 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  9 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
 10 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | infinity  
  1 | digoal | digoal      |             |             | 2012-01-13 15:08:52+08 | 2012-01-13 15:16:42+08  
  1 | new    |             | digoal      |             | 2012-01-13 15:16:42+08 | 2012-01-13 15:17:22+08  
  1 | new2   |             | digoal      |             | 2012-01-13 15:17:22+08 | infinity  
  2 | digoal | digoal      |             | digoal      | 2012-01-13 15:08:52+08 | 2012-01-13 15:18:03+08  

说明delete操作是把stop_time修改为当前时间了.

因此创建了timetravel触发器后的表,只有stop_time为infinity 的记录对我们来说是有效记录。

接下来看看能不能修改start_time和stop_time :

digoal=> update timetravel_test set stop_time=now() where id=1;  
ERROR:  timetravel (timetravel_test): you cannot change start_time and/or stop_time columns (use set_timetravel)  
digoal=> update timetravel_test set start_time=now() where id=1;  
ERROR:  timetravel (timetravel_test): you cannot change start_time and/or stop_time columns (use set_timetravel)  

没错, timetravel 对这两个字段进行了保护,不允许用户对这两个字段进行修改。

另外2个函数 :

关闭timetravel 对当前session的作用. 其他SESSION不影响. 参数0表示关闭, 1 表示打开.

digoal=> select set_timetravel('timetravel_test',0);  
 set_timetravel   
----------------  
              1  
(1 row)  

查看当前SESSION的timetravel 状态, 是否启用. 结果0表示关闭, 1 表示打开.

digoal=> select get_timetravel('timetravel_test');  
 get_timetravel   
----------------  
              0  
(1 row)  

此时在这个SESSION里面就可以修改start_time和stop_time了.

digoal=> update timetravel_test set start_time=now() where id=1;  
UPDATE 4  

进入另一个SESSION, 是不可以修改的.

digoal=> update timetravel_test set start_time=now() where id=1;  
ERROR:  timetravel (timetravel_test): you cannot change start_time and/or stop_time columns (use set_timetravel)  

小结

1. timetravel可以对平时变更非常少,但是每次变更都需要有变更轨迹记录的情况。当然这部分功能很容易由应用程序来完成。使用timetravel并不是一个很好的方法,但是timetravel的想法是值得借鉴的。

参考

http://www.postgresql.org/docs/9.1/static/contrib-spi.html

Flag Counter

digoal’s 大量PostgreSQL文章入口