有一些情况,当 到没有主键的表 或某些其他唯一索引,由于疏忽,已包含现有记录的完整克隆。
例如,使用 COPY 流将时间顺序度量的值写入 PostgreSQL,然后突然发生故障,部分完全相同的数据再次到达。
当PK不是帮手时
最简单的方法就是从一开始就防止这种情况发生。 例如,滚动主键。 但如果不增加存储数据量,这并不总是可能的。
例如,如果源系统的准确率高于数据库中字段的准确率:
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。 最简单的选择是将整行转换为文本:
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[]);
我们自己检查一下:
是的,一切都正确:我们选择了3条记录进行全表唯一的Seq Scan,并使用Delete节点来搜索数据 使用 Tid Scan 进行单次扫描:
-> Tid Scan on tbl (actual time=0.050..0.051 rows=3 loops=1)
TID Cond: (ctid = ANY ($0))
如果你清除了很多记录,
让我们检查一下是否有更大的表和更多的重复项:
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;
因此,该方法很有效,但必须谨慎使用。 因为每删除一条记录,Tid Scan 都会读取一个数据页,Delete 也会读取一个数据页。
来源: habr.com