我有一个由Postgres(v11)数据库和主表支持的Web应用程序,该表中的每一行都可以看作是一个对象,而每一列都是该对象的字段。
所以我们有:
| id | name | field1 | field2| .... | field 100|
-----------------------------------------------
| 1 | foo | 12.2 | blue | .... | 13.7 |
| 2 | bar | 22.1 | green | .... | 78.0 |
该表是使用以下方法创建的:
CREATE TABLE records(
id VARCHAR(50) PRIMARY KEY,
name VARCHAR(50),
field1 NUMERIC,
field2 VARCHAR(355),
field100 NUMERIC);
现在我有一个审计表,该表存储对每个对象的每个字段的更新。审核表定义为:
| timestamp | objid | fieldname | oldval | newval |
-----------------------------------------------
| 1234 | 1 | field2 | white | blue |
| 1367 | 1 | field1 | "11.5" | "12.2" |
| 1372 | 2 | field1 | "11.9" | "22.1" |
| 1387 | 1 | name | baz | foo |
该表是使用以下方法创建的:
CREATE TABLE audit_log(
timestamp TIMESTAMP,
objid VARCHAR (50) REFERENCES records(id),
fieldname VARCHAR (50) NOT NULL,
oldval VARCHAR(355),
newval VARCHAR(355));
oldval
/ newval
保留为varchar
,因为它们仅用于审计目的,因此实际数据类型并不重要。
出于明显的原因,该表在过去几年左右变得越来越大,所以我想删除一些旧数据。有人建议只保留每个对象的最后5个更新(即,然后UI可以显示审计表中的最后5个更新)。
我知道您可以使用GROUP BY
和LIMIT
来获得此功能,但是问题是我有100万个对象,其中一些已被更新1000多次,而另一些则几乎没有更新多年。而且审核日志的读/写操作非常繁琐(可以预料)。
对于每个对象,删除所有早于第5个最新更新的所有条目的最佳方法是什么(当然,理想情况下,我将其移至某些二级存储中)?
答案 0 :(得分:1)
解决方案有一些要素:
row_number
函数。不幸的是,这是一个“窗口函数”,不能在where子句中使用。ctid
字段,用于唯一标识表中的一行。您使用CTE创建包含ctid
和row_number
的逻辑表。然后,从delete语句中引用它。像这样:
with t as (
select ctid, row_number() over (partition by objid)
from the_audit_table
)
delete from the_audit_table
where ctid in (select ctid from t where row_number > 5)
如果您担心一次执行所有操作的效果,那么只需在objid
空间的某些子集上运行许多较小的事务。或者(如果您打算删除99%的行)创建一个新表,将row_number > 5
更改为row_number <= 5
并将其插入新表中,然后替换旧表与新的。
首先要进行质量检查! :-)
答案 1 :(得分:1)
如果要在可能包含数千个的组中仅保留5条记录,则更有效的方法是使用临时表。
首先,使用CREATE TABLE AS
syntax选择要保留的记录,以动态方式创建一个新表。分析功能使选择记录变得容易。
CREATE TABLE audit_log_backup AS
SELECT mycol1, mycol2, ...
FROM (
SELECT a.*, ROW_NUMBER() OVER(PARTITION BY objid ORDER BY timestamp DESC) rn
FROM audit_log a
) x WHERE rn <= 5
然后,仅TRUNCATE
原始表并重新插入保存的数据:
TRUNCATE audit_log;
INSERT INTO audit_log SELECT * FROM audit_log_backup;
--- and eventually...
DROP TABLE audit_log_backup;
如the documentation中所述,截断大表比从表中删除要有效得多:
TRUNCATE
快速从一组表中删除所有行。它具有与每个表上不合格的DELETE
相同的效果,但是由于它实际上并未扫描表,因此速度更快。此外,它会立即回收磁盘空间,而不需要随后的VACUUM
操作。这在大型表上最有用。
Erwin Brandsetter指出,要注意的一件事是,此技术会创建一种竞争条件,其中不会考虑在复制开始后添加(或更新)的记录。一种解决方案是在一个事务中执行所有操作,而locking the table:
BEGIN WORK;
LOCK TABLE audit_log IN SHARE ROW EXCLUSIVE MODE;
CREATE TABLE audit_log_backup AS ...;
TRUNCATE audit_log;
INSERT INTO audit_log SELECT * FROM audit_log_backup;
COMMIT WORK;
缺点是,它将在事务进行过程中等待尝试访问表的任何会话。
免责声明:无论您做什么,请确保在开始清除表之前正确备份整个表!
答案 2 :(得分:1)
您可以使用普通的row_number()
,类似于what @Willis suggested,并经过ORDER BY
的改进:
WITH cte AS (
SELECT ctid
, row_number() OVER (PARTITION BY objid ORDER BY timestamp DESC) AS rn
FROM audit_log
)
DELETE FROM audit_log
USING cte
WHERE cte.ctid = tbl.ctid
AND cte.row_number > 5;
那张大桌子要花很长的时间。您可以在audit_log(objid, timestamp DESC)
上使用多列索引并使用以下查询来更快地实现该目标:
WITH del AS (
SELECT x.ctid
FROM records r
CROSS LATERAL (
SELECT a.ctid
FROM audit_log a
WHERE a.objid = r.id
ORDER BY a.timestamp DESC
OFFSET 5 -- excluding the first 5 per object
) x
)
DELETE FROM audit_log
USING del
WHERE del.ctid = tbl.ctid;
或者:
DELETE FROM audit_log
WHERE ctid NOT IN (
SELECT x.ctid
FROM records r
CROSS JOIN LATERAL (
SELECT a.ctid
FROM audit_log a
WHERE a.objid = r.id
ORDER BY a.timestamp DESC
LIMIT 5 -- the inverse selection here
) x
);
使用支持索引,后者可能更快。
相关:
为每个对象编写一个仅前5位的新表会更快。您可以为此使用上一个查询中的子查询。 (请参见GMB's answer。)它会产生一个没有膨胀的原始表。但由于该表为very read/write heavy
,因此我将其排除在外。如果一段时间不能负担必要的排他锁,那就不行了。
您的timestamp
列未定义NOT NULL
。您可能需要NULLS LAST
。参见: