【问题标题】:Keep only the last 5 rows per object in an audit table仅保留审计表中每个对象的最后 5 行
【发布时间】:2019-07-17 01:00:28
【问题描述】:

我有一个由 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 来获取此信息,但问题是我拥有一百万多个对象,其中一些对象已更新一千多次,而其他对象多年来几乎没有更新。并且审计日志的读/写非常繁重(正如预期的那样)。

删除每个对象的第 5 次最新更新之前的所有条目的最佳方法是什么(当然,理想情况下,我会将其移到某个辅助存储中)?

【问题讨论】:

  • 请提供您的 Postgres 版本和 CREATE TABLE 语句,仅包含相关列,但显示数据类型和约束。
  • 要求添加详细信息(我猜你想询问审计表数据类型,所以添加相同)。
  • 我要求提供CREATE TABLE 语句,这是一个真正的信息来源(包括数据类型和约束)。例如,您可以在 pgAdmin III 或 4 中看到这些。
  • 再次更新@ErwinBrandstetter
  • audit_log 中没有 PK?是objid 还是id - 我想是integer?

标签: sql postgresql audit audit-trail rowdeleting


【解决方案1】:

解决方案有几个成分:

  • PostgreSQL row_number 函数。不幸的是,这是一个“窗口函数”,不能在 where 子句中使用。
  • 公用表表达式 (CTE):“with T as (...some SQL...) ...do something with T...”
  • PostgreSQL ctid 字段,唯一标识表中的一行。

您使用 CTE 创建一个包含ctid 和row_number 的逻辑表。然后从删除语句中引用它。像这样的:

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 并将其插入到新表中,然后用新表替换旧表.

首先进行 QA 测试! :-)

【讨论】:

    【解决方案2】:

    如果您打算在可能包含数千条记录的组中只保留 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;
    

    不利的一面是,这将使任何在事务进行时尝试访问表的会话都处于等待状态。


    免责声明:无论您做什么,请确保在开始清除之前正确备份整个表!

    【讨论】:

    • 比原地删除快得多。不过,请注意比赛条件。该表“读/写非常重”。在CREATE TABLE 语句的开头和TRUNCATE 之间同时写入的每一行都会丢失。另外,TRUNCATE 需要独占锁。 (唯一)安全的方法是在事务中执行此操作并从表上的排他锁开始 - 这对于读/写重表可能是一个问题。第 22 条。
    • @ErwinBrandstetter :感谢您指出这一点,我用更多细节更新了我的答案!
    • 我现在喜欢你的回答。您甚至可以使用TEMPORARY 表来提高速度。见:stackoverflow.com/a/8290958/939860
    【解决方案3】:

    您可以使用普通的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。见:

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-09-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多