3

我有一个由 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 次更新)。

我知道您可以使用 aGROUP BY和 a来获得它,LIMIT但问题是我有一百万多个对象,其中一些已经更新了一千多次,而另一些多年来几乎没有更新。并且审计日志的读/写非常繁重(正如预期的那样)。

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

4

3 回答 3

1

如果您打算在可能包含数千条的组中仅保留 5 条记录,则更有效的方法是使用临时表。

CREATE TABLE AS首先,通过使用语法选择要保留的记录来动态创建一个新表。分析功能使选择记录变得容易。

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;

文档中所述,截断大表比从中删除更有效:

TRUNCATE从一组表中快速删除所有行。它与每个表上的 unqualified 具有相同的效果DELETE,但由于它实际上并不扫描表,因此速度更快。此外,它会立即回收磁盘空间,而不需要后续VACUUM操作。这在大表上最有用。

正如Erwin Brandsetter所评论的那样,需要注意的一件事是,这种技术会产生一种竞争条件,即在开始复制后添加(或更新)的记录将不会被考虑在内。一种解决方案是在单个事务中执行所有操作,同时锁定表

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;

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


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

于 2019-02-22T22:28:33.370 回答
1

解决方案有一些成分:

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

您使用 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 > 5row_number <= 5并将其插入到新表中,然后用新表替换旧表。

首先在 QA 中测试!:-)

于 2019-02-22T22:07:09.013 回答
1

您可以使用简单的row_number(),类似于@Willis 建议的,改进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;

That's going to take a long time for your big table. You can have that faster with a multicolumn index on audit_log(objid, timestamp DESC) and this query:

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;

Or:

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
   );

The latter may be faster with the supporting index.

Related:

Writing a new table with just the top 5 for each object would be much faster. You can use the subquery from the last query for this. (And see GMB's answer.) It produces a pristine table without bloat. But I ruled that out due to the table being very read/write heavy. If you cannot afford the necessary exclusive lock for some time, that's a no-go.

Your timestamp column is not defined NOT NULL. You may need NULLS LAST. See:

于 2019-02-22T22:29:29.787 回答