我有一个由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个最新更新更早的所有条目的最佳方法是什么(当然,理想情况下我会将其移到某个辅助存储中)?
解决方案有一些成分:
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
并将其插入到新表中,然后将旧表替换为新表。
首先在QA中测试! :-)
如果您要在可能包含数千个的组中保留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;
缺点是,这会等待任何会在事务进行时尝试访问该表的会话。
免责声明:无论您做什么,请确保在开始清除之前正确备份整个表格!
你可以使用一个简单的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
。看到: