前言

今天有个业务的妹子问我“在吗?”

我说什么事?

给我发个截图,我一看!噢,原来是把数据删除了,想让我把数据找回来。

他说,大哥你能不能帮我。

我说可以!

很快啊,我就打开终端,一个指令就开始了。

我大E了,发现数据闪回不了。

然后十分钟后我告诉她,搞不定了。

然后她就投诉到领导哪里去了,说她丢的数据找不回来了,DBA也搞不定。

我找到这个妹子,说:“你不讲规矩,你不懂这个恢复的难度。”

她忙说对不起,我不懂规矩啊!

我说:“不讲规矩,来,告状,诬陷我20多年经验的dba连点数据都恢复不了。这好吗?这不好。我劝这位女同学,耗子尾汁。好好反思。

PostgreSQL如何开闪

说到闪回查询,PostgreSQL一开始是有这个功能的,它叫Time Travel(时间旅行),这名字听上去比闪回查询要高大上,瞬间让我想起了好几部描述这类的电影,如《时间机器》、《时间旅行者的妻子》、《信条》。


我们拥有时间旅行,可以穿梭于任意的时空,然后来阻止一场灾难?大多数电影都是这类的桥段,而对于数据库来说,时间旅行也是我们拯救灾难的一种方法。

时间旅行Time Travel这项技术还要追溯到PostgreSQL 6时代,但是在之后就被打入了冷宫,变成可以使用但是并不推荐。官方文网也说有性能上的影响,推荐使用触发器替代,一直到PostgreSQL 12这个版本,才真正的把这项功能移除了。

那么怎么在PostgreSQL 12上实现呢?有很多种解决方案,其实原生的PG只是一个食材,大家可以根据这个食材,自由发挥做出各种各样美味的料理。你可以灵活选择适合你口味的料理(适配你的系统)。

pg_dirtyread

pg_dirtyread的工作原理比较简单,就是从Dead Tuples中读取数据。这得益于PostgreSQL的MVCC机制。在PostgreSQL的MVCC机制中,当更新或者删除任何一行记录的时候,将在内部创建新行并将旧行标记为Dead Tuples。而Pg_dirtyread就可以助我们从Dead Tuples读出数据。但是缺点也很明显,如果Dead Tuples被autovacuum进程清理了,也就没数据了。

接下来我们来测试一下,下载编译pg_dirtyread插件。

make && make installcreate extension pg_dirtyread;

create table students(    stuno int,    name varchar(50),    age varchar(50),    city varchar(50));

insert into students (stuno, name, age, city)  values (1, 'abhiram', 22, 'allahabad');  insert into students (stuno, name, age, city)  values (2, 'alka', 20, 'ghaziabad');  insert into students (stuno, name, age, city)  values (3, 'disha', 21, 'varanasi');  insert into students (stuno, name, age, city)  values (4, 'esha', 21, 'delhi');  insert into students (stuno, name, age, city)  values (5, 'manmeet', 23, 'jalandhar'); 

postgres=# select * from students; stuno |  name   | age |   city    -------+---------+-----+-----------     1 | abhiram | 22  | allahabad     2 | alka    | 20  | ghaziabad     3 | disha   | 21  | varanasi     4 | esha    | 21  | delhi     5 | manmeet | 23  | jalandhar 

对上述表做多次更新。

update students set city='WuHan' where stuno=5;update students set age=21 where stuno=5;update students set city='Shanghai' where stuno=5;

通过pg_dirtyread读取更新前的数据。

postgres=# SELECT * FROM pg_dirtyread('students') students(stuno int, name varchar(50),age varchar(50),city varchar(50)); stuno |  name   | age |   city    -------+---------+-----+-----------     1 | abhiram | 22  | allahabad     2 | alka    | 20  | ghaziabad     3 | disha   | 21  | varanasi     4 | esha    | 21  | delhi     5 | manmeet | 23  | jalandhar     5 | manmeet | 23  | WuHan     5 | manmeet | 21  | WuHan     5 | manmeet | 21  | Shanghai

可以看到把历史的数据都找回来了,但是很乱,它读出了所有的历史数据,可能你只是想恢复到其中的某一个时间点。

postgres=# SELECT * FROM pg_dirtyread('students')postgres-# AS students(tableoid oid, ctid tid, xmin xid, xmax xid, cmin cid, cmax cid, dead boolean,stuno int, name varchar(50),age varchar(50),city varchar(50));   tableoid | ctid  |   xmin   |   xmax   | cmin | cmax | dead | stuno |  name   | age |   city    ----------+-------+----------+----------+------+------+------+-------+---------+-----+----------- 20023788 | (0,1) | 26678735 |        0 |    0 |    0 | f    |     1 | abhiram | 22  | allahabad 20023788 | (0,2) | 26678736 |        0 |    0 |    0 | f    |     2 | alka    | 20  | ghaziabad 20023788 | (0,3) | 26678737 |        0 |    0 |    0 | f    |     3 | disha   | 21  | varanasi 20023788 | (0,4) | 26678738 |        0 |    0 |    0 | f    |     4 | esha    | 21  | delhi 20023788 | (0,5) | 26678739 | 26678741 |    0 |    0 | t    |     5 | manmeet | 23  | jalandhar 20023788 | (0,6) | 26678741 | 26678742 |    0 |    0 | t    |     5 | manmeet | 23  | WuHan 20023788 | (0,7) | 26678742 | 26678743 |    0 |    0 | f    |     5 | manmeet | 21  | WuHan 20023788 | (0,8) | 26678743 |        0 |    0 |    0 | f    |     5 | manmeet | 21  | Shanghai

不过知道死元组的事务ID就可以通过pg_xact_commit_timestamp函数将xmin转换成时间。

使用pg_xact_commit_timestamp函数,需要将参数track_commit_timestamp设置为on,修改该参数需要重启数据库。

postgres=# select (pg_xact_commit_timestamp(xmin)) from postgres-# (SELECT * FROM pg_dirtyread('students') AS students(tableoid oid, ctid tid, xmin xid, xmax xid, cmin cid, cmax cid, dead boolean,stuno int, name varchar(50),age varchar(50),city varchar(50))) as b;    pg_xact_commit_timestamp    ------------------------------- 2020-11-23 15:23:48.611553+08 2020-11-23 15:23:48.624343+08 2020-11-23 15:23:48.6367+08 2020-11-23 15:23:48.65629+08 2020-11-23 15:23:48.676773+08 2020-11-23 15:23:55.716173+08 2020-11-23 15:23:55.729868+08 2020-11-23 15:23:56.02215+08

这样就可以基于时间点恢复到你想要的位置了。

可以说pg_dirtyread很好的解决了误操作导致的数据修改删除问题。但是它最大的缺点就是受制于autovacuum进程,如果autovacuum进程清理掉了死元组,pg_dirtyread就没办法工作了。所以当出现误删数据之后,我们第一时间就要先关闭autovacuum,然后通过下面查询误操作的表是否已经发生了vacuum。

select relname,last_vacuum, last_autovacuum, last_analyze, last_autoanalyze from pg_stat_user_tables;

最后说一点,这个方法虽好,但是不支持truncate和drop这类的ddl。

temporal_tables

temporal_tables也是一个不错的扩展,但是它的原理和pg_dirtyread完全不同。它的原理是将我们修改或者删除的旧行归档到一个历史表中,这样可以方便进行审计、对比。至于这个插件我觉得他们和IBM DB2的功能很接近,都需要使用一个叫时态表temporal tables技术。

我们来测试一下,下载编译temporal_tables插件。

make && make installcreate extension temporal_tables;

接下来我们来创建表。

create table students(    stuno int,    name varchar(50),    age varchar(50),    city varchar(50));

建完表后,我们要增加一个sys_period列。

ALTER TABLE students ADD COLUMN sys_period tstzrange NOT NULL;

然后我们需要创建一个历史表。

CREATE TABLE students_history (LIKE students);

最后我们要创建一个触发器,把我们的表和历史表关联起来。

CREATE TRIGGER students_hist_trigger BEFORE INSERT OR UPDATE OR DELETE ON students FOR EACH ROWEXECUTE PROCEDURE versioning('sys_period', 'students_history', true);

插入记录。

insert into students (stuno, name, age, city)  values (1, 'abhiram', 22, 'allahabad');  insert into students (stuno, name, age, city)  values (2, 'alka', 20, 'ghaziabad');  insert into students (stuno, name, age, city)  values (3, 'disha', 21, 'varanasi');  insert into students (stuno, name, age, city)  values (4, 'esha', 21, 'delhi');  insert into students (stuno, name, age, city)  values (5, 'manmeet', 23, 'jalandhar');  

此时通过查询,会发现sys_period列上会显示时间。["2020-11-23 17:10:58.702324+08",)。这个格式前面代表着有效期开始时间,后面则代表有效期结束时间,这里,后面是空的,代表了无穷大。

postgres=# select * from students;stuno |  name   | age |   city    |             sys_period             -------+---------+-----+-----------+------------------------------------     1 | abhiram | 22  | allahabad | ["2020-11-23 17:48:13.841549+08",)     2 | alka    | 20  | ghaziabad | ["2020-11-23 17:48:13.862973+08",)     3 | disha   | 21  | varanasi  | ["2020-11-23 17:48:13.874852+08",)     4 | esha    | 21  | delhi     | ["2020-11-23 17:48:13.891388+08",)     5 | manmeet | 21  | Shanghai  | ["2020-11-23 17:51:22.613059+08",)

我们对表进行更新操作,此时查询students_history就会显示我们的历史数据。

update students set city='WuHan' where stuno=5;update students set age=21 where stuno=5;update students set city='Shanghai' where stuno=5;

postgres=# select * from students_history; stuno |  name   | age |   city    |                            sys_period                             -------+---------+-----+-----------+-------------------------------------------------------------------     5 | manmeet | 23  | jalandhar | ["2020-11-23 17:48:13.916846+08","2020-11-23 17:48:31.964398+08")     5 | manmeet | 23  | WuHan     | ["2020-11-23 17:48:31.964398+08","2020-11-23 17:50:30.014874+08")     5 | manmeet | 21  | WuHan     | ["2020-11-23 17:50:30.014874+08","2020-11-23 17:51:22.613059+08")

虽然能显示数据,但是和我们想要的差距还是有点大。因为如上面所示,数据还是很紊乱的。想要查询到指定的时间点还是很困难的。不过根据历史视图,我们知道我们在下面三个时间点更新了数据。分别是17:48:13 17:50:30 17:51:22

我们可以在创建一个视图。

CREATE VIEW students_with_history ASSELECT * FROM studentsUNION ALLSELECT * FROM students_history;

然后执行下面操作,我就想看看在17:49分应该是显示那条数据,这里要显示第一次做update的结果。

postgres=# SELECT * FROM students_with_historypostgres-# WHERE stuno = 5 AND sys_period @> '2020-11-23 17:49:00'::timestamptz;stuno |  name   | age | city  |                            sys_period                             -------+---------+-----+-------+-------------------------------------------------------------------     5 | manmeet | 23  | WuHan | ["2020-11-23 17:48:31.964398+08","2020-11-23 17:50:30.014874+08")(1 row)

可以看到结果完全正确。换成17点51分在看看,这里显示了第二次做update的结果。

postgres=# SELECT * FROM students_with_historypostgres-#   WHERE stuno = 5 AND sys_period @> '2020-11-23 17:51:00'::timestamptz ; stuno |  name   | age | city  |                            sys_period                             -------+---------+-----+-------+-------------------------------------------------------------------     5 | manmeet | 21  | WuHan | ["2020-11-23 17:50:30.014874+08","2020-11-23 17:51:22.613059+08")(1 row)

以上的操作方法就像Oracle中的语法as of timestamp一样。而AS OF SYSTEM TIME语法是SQL 2011的标准。

而wiki上也有一篇文章详细的描述了PostgreSQL实现SQL 2011的方法和建议,实现的原理和temporal_tables差不多,但是它多出来了truncate的恢复。其实temporal_tables要实现truncate也很简单,自己做一个truncate的触发器就行了。

SQL2011Temporal

衍生版PG

当前现在市面上有一些衍生版的PG,比如CockroachDB。提供了SQL 2011中的AS OF SYSTEM TIME语法。

有兴趣的同学可以看看蟑螂数据库是怎么实现SQL:2011标准的。

Time-Travel Queries: SELECT witty_subtitle FROM THE FUTURE

总结

通过验证可以看出PostgreSQL打开闪回功能还是比较复杂的,它不像其他数据库内置了这个功能。需要我们自己找第三方插件来实现。而大多数第三方插件都是基于触发器实现的。而触发器的往往会存在一些开销。同时还要在原表上增加一个时间区间的字段。所以还是推荐使用pg_dirtyread来拯救您误删除的数据啊喂。

postgresql 删除触发器_PostgreSQL:我没有带闪,不讲武德相关推荐

  1. postgresql 删除触发器_postgresql 触发器

    一.创建事件触发器 1.ddl_command_start - 一个DDL开始执行前被触发: 2.ddl_command_end - 一个DLL 执行完成后被触发: 3.sql_drop -- 删除一 ...

  2. postgresql删除索引_PostgreSQL 13 发布,索引和查找有重大改进

    9月24日,PostgreSQL全球开发组宣布PostgreSQL 13正式发布,作为世界上使用最多的开源数据库之一,PostgresSQL 13是目前的最新版本. PostgreSQL 13 在索引 ...

  3. Postgresql添加/删除触发器示例

    -- 定义触发器 CREATE TRIGGER "vss_after_insert_flow_201702" AFTER INSERT ON "public". ...

  4. 【Postgresql】触发器某个字段更新时执行,行插入或更新执行

    [Postgresql]触发器某个字段更新时执行,行插入或更新执行 1. postgresql触发器 2. 触发器的创建及示例 1) 字段更新时,触发 2) 行插入或更新时,触发 3. 触发器的删除 ...

  5. PostgreSQL数据库触发器实验

    PostgreSQL数据库触发器实验 实验目的 (1)掌握触发器的创建.修改和删除操作. (2)掌握触发器的触发执行. (3)掌握触发器与约束的不同. 实验要求 (1)创建触发器. (2)触发器执行触 ...

  6. 解析postgresql 删除重复数据案例

    这篇文章主要介绍了postgresql 删除重复数据案例详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下 1.建表 /*Navicat Premium ...

  7. 可以删除的android系统自带程序—详细列表【转】

    android系统自带的很多程序平时根本用不到或很少用到,但是它们有的却在你开机之后默默地在后台运行着,既占内存又消耗电量,这时我们就需要把他们一一杀掉!但是很多人不明白这些软件都是些什么,哪些能删哪 ...

  8. mysql创建删除触发器的时候_mysql触发器简介及如何创建和删除触发器

    什么是mysql触发器 需要MySQL 5 对触发器的支持是在MySQL 5中增加的.因此,本章内容适用于MySQL 5或之后的版本. MySQL语句在需要时被执行,存储过程也是如此.但是,如果你想要 ...

  9. MySql命令行创建和删除触发器

    创建和删除mysql触发器 举例如下: 现有表tab1和tab2 要求触发器具有下面功能 当对tab1插入一条记录时则tab2也插入相同的记录 1 2 3 4 5 6 7 8 mysql>del ...

最新文章

  1. python如何将列表,字典,元组,集合首字母变成大写 以及其他的大小写转换!
  2. The user specified as a definer ('root'@'%') does not exist
  3. nginx日志模块及日志格式
  4. cocos2d-x初探学习笔记(14)--菜单项
  5. LeetCode 1966. Binary Searchable Numbers in an Unsorted Array
  6. 原来华为是一家做音乐的公司
  7. 大数据下单集群如何做到2万+规模?
  8. Windows注册表相关示例(重装Chrome、思迅条码软件、清除远程痕迹等)
  9. UTF-8字符集成为Java 18默认字符集?发布周期将至,Java 18现身
  10. 为什么博客园安卓端无法登陆,真服了
  11. java 解锁关闭文件占用_程序员:Java文件锁定、解锁和其它NIO操作
  12. idea 初始界面_IDEA 初始配置教程
  13. 三目运算符?:结合性
  14. 常用的C语言学习网站
  15. 微软影子系统EWF软件用法及参数描述
  16. 利用matlab实现卷积实验报告,matlab卷积实验报告(共8篇).doc
  17. WIN7的便签使用快捷键
  18. 概率图模型: Coursera课程资源分享和简介
  19. java coherence_coherence配置说明
  20. 中国宽带无线移动互联网论坛-无线传感器网络

热门文章

  1. boost::mp11::tuple_for_each相关用法的测试程序
  2. boost::math::hyperexponential用法的测试程序
  3. ITK:创建三角形四边形网格
  4. ITK:获取图像大小
  5. VTK:图片之ImageGradientMagnitude
  6. OpenCV边缘之间的距离转换功能的实例(附完整代码)
  7. C++返回一个数的二进制最右边的一位的实现算法(附完整源码)
  8. OpenGL blending sort 混合排序(前后)的实例
  9. C++重载运算符最好声明为友元
  10. QT自定义图表上不同元素的外观