每个对象在审计表中仅保留最后5行

时间:2019-02-22 21:30:05

标签: sql postgresql audit audit-trail rowdeleting

我有一个由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 BYLIMIT来获得此功能,但是问题是我有100万个对象,其中一些已被更新1000多次,而另一些则几乎没有更新多年。而且审核日志的读/写操作非常繁琐(可以预料)。

对于每个对象,删除所有早于第5个最新更新的所有条目的最佳方法是什么(当然,理想情况下,我将其移至某些二级存储中)?

3 个答案:

答案 0 :(得分:1)

解决方案有一些要素:

  • PostgreSQL row_number函数。不幸的是,这是一个“窗口函数”,不能在where子句中使用。
  • 常用表表达式(CTE):“用T作为(...一些SQL ...)...用T做某事...”
  • PostgreSQL ctid字段,用于唯一标识表中的一行。

您使用CTE创建包含ctidrow_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。参见: