DBA:从没有PK的表中清除克隆记录

在某些情况下, 在没有主键或其他唯一索引的表中,已经存在的记录的完整克隆会无意中掉落。



例如,将时间度量标准的值写入PostgreSQL COPY流,然后突然失败,并且部分完全相同的数据再次出现。

如何清除不必要的克隆数据库?

当PK不是助手时


最简单的方法是完全避免这种情况的发生。 例如,滚动相同的PRIMARY KEY。 但这并不总是可能的,而不增加存储的数据量。

例如,如果原始系统的精度高于数据库中字段的精度:

metric | ts | data -------------------------------------------------- cpu.busy | 2019-12-20 00:00:00 | {"value" : 12.34} cpu.busy | 2019-12-20 00:00:01 | {"value" : 10} cpu.busy | 2019-12-20 00:00:01 | {"value" : 11.2} cpu.busy | 2019-12-20 00:00:03 | {"value" : 15.7} 

你注意到了吗? 倒数秒不是从00:00:02倒数秒开始写入ts的,而是从应用程序的角度来看仍然非常有效(毕竟,数据值是不同的!)。

当然,您可以设置PK(公制,ts) ,但是这样我们将获得有效数据的插入冲突。

您可以制作PK(公制,ts,数据) -但这将大大增加其体积,我们将不再使用。

因此,最正确的选择是制作一个常规的非唯一索引(度量标准,ts),并在出现问题后进行处理(如果确实存在)。

“阵营战争已经开始”


发生了某种意外,现在我们必须从表中销毁克隆记录。



让我们模拟源数据:

 CREATE TABLE tbl(k text, v integer); INSERT INTO tbl VALUES ('a', 1) , ('a', 3) , ('b', 2) , ('b', 2) -- oops! , ('c', 3) , ('c', 3) -- oops!! , ('c', 3) -- oops!! , ('d', 4) , ('e', 5) ; 

然后我们的手颤抖了三下,Ctrl + V卡住了,现在...

首先,让我们了解一下我们的表可能非常大,因此,在找到所有克隆之后,建议我们从字面上“戳一下手指”删除特定记录,而不必再次搜索它们

有一种方法- 通过ctid寻址 ,即特定记录的物理标识符。

也就是说,首先,我们需要在表行的全部内容的上下文中收集ctid记录。 最简单的选择是将整行都转换为文本:

 SELECT T::text , array_agg(ctid) ctids FROM tbl T GROUP BY 1; 

 t | ctids --------------------------------- (e,5) | {"(0,9)"} (d,4) | {"(0,8)"} (c,3) | {"(0,5)","(0,6)","(0,7)"} (b,2) | {"(0,3)","(0,4)"} (a,3) | {"(0,2)"} (a,1) | {"(0,1)"} 

是否可以不进行投射?
原则上,在大多数情况下是可能的。 在开始使用此表中没有类型运算符的类型字段之前,请执行以下操作

 CREATE TABLE tbl(k text, v integer, x point); SELECT array_agg(ctid) ctids FROM tbl T GROUP BY T; -- ERROR: could not identify an equality operator for type tbl 

是的,我们立即看到,如果阵列中有多个记录,那么就是所有克隆。 让我们只留下它们:

 SELECT unnest(ctids[2:]) FROM ( SELECT array_agg(ctid) ctids FROM tbl T GROUP BY T::text ) T; 

 unnest ------ (0,6) (0,7) (0,4) 

矮个情人
您可以这样写:
 SELECT unnest((array_agg(ctid))[2:]) FROM tbl T GROUP BY T::text; 

由于序列化字符串本身的值对我们而言并不有趣,因此我们只需将其从子查询的返回列中扔出即可。

剩下的就是让DELETE使用我们收到的集合:

 DELETE FROM tbl WHERE ctid = ANY(ARRAY( SELECT unnest(ctids[2:]) FROM ( SELECT array_agg(ctid) ctids FROM tbl T GROUP BY T::text ) T )::tid[]); 

检查一下自己:


[看explain.tensor.ru]

是的,没错:我们选择了3条记录作为整个表的唯一Seq扫描,并且Delete节点使用一次遍历使用Tid Scan搜索数据:

 -> Tid Scan on tbl (actual time=0.050..0.051 rows=3 loops=1) TID Cond: (ctid = ANY ($0)) 

如果您清除了许多记录, 请不要忘记驱动VACUUM ANALYZE

让我们检查一张更大的桌子,花很多钱:

 TRUNCATE TABLE tbl; INSERT INTO tbl SELECT chr(ascii('a'::text) + (random() * 26)::integer) k -- a..z , (random() * 100)::integer v -- 0..99 FROM generate_series(1, 10000) i; 


[看explain.tensor.ru]

因此,该方法可以成功工作,但是必须谨慎使用。 因为对于每条删除的记录,在“ Tid扫描”中有一个数据页读数,在“删除”中有一个数据页读数。

Source: https://habr.com/ru/post/zh-CN481352/


All Articles