按ID删除数百万行的最佳方法


77

我需要从PG数据库中删除大约200万行。我有一个需要删除的ID列表。但是,我尝试执行此操作的任何方法都需要花费几天的时间。

我尝试将它们放在一个表中并按100个批次进行处理。4天后,它仍在运行,仅删除了297268行。(我必须从ID表中选择100个ID,删除该列表中的位置,从ID表中删除我选择的100个ID)。

我试过了:

DELETE FROM tbl WHERE id IN (select * from ids)

这也是永远的。难以估计需要多长时间,因为直到完成我才能看到它的进度,但是查询在2天后仍在运行。

当我知道要删除的特定ID且有数百万个ID时,这只是一种寻找从表中删除的最有效方法。


2
剩下多少行?一种替代方法是选择工作表中的其余行,然后重命名表。
Thilo

Answers:


96

一切取决于...

  • 删除所有索引(删除ID上需要删除的索引除外)
    ,然后重新创建它们(比对索引进行增量更新要快得多)

  • 检查是否有可以安全地暂时删除/禁用的触发器

  • 外键是否引用您的表?可以删除它们吗?暂时删除?

  • 根据您的自动真空设置,可能有助于VACUUM ANALYZE在操作之前运行。

  • 假设没有对相关表的并发写访问权,或者您可能必须专门锁定表,或者此路由可能根本不适合您。

  • 根据您的设置,《填充数据库》手册相关章节中列出的某些要点也可能有用。

  • 如果删除表的大部分,其余部分放入RAM,最快和最简单的方法是:

SET temp_buffers = '1000MB'; -- or whatever you can spare temporarily

CREATE TEMP TABLE tmp AS
SELECT t.*
FROM   tbl t
LEFT   JOIN del_list d USING (id)
WHERE  d.id IS NULL;      -- copy surviving rows into temporary table

TRUNCATE tbl;             -- empty table - truncate is very fast for big tables

INSERT INTO tbl
SELECT * FROM tmp;        -- insert back surviving rows.

这样,您不必重新创建视图,外键或其他依赖对象。阅读temp_buffers手册中设置。只要表适合内存,或者至少适合大多数内存,此方法就会很快。请注意,如果服务器在此操作过程中崩溃,则可能会丢失数据。您可以将所有内容包装到事务中以使其更安全。

ANALYZE之后运行。或者,VACUUM ANALYZE如果您没有采用截断路线,或者VACUUM FULL ANALYZE您希望将其最小化。对于大表,请考虑备选方案CLUSTER/ pg_repack

对于小型表,通常使用简单DELETE而不是TRUNCATE更快的方法:

DELETE FROM tbl t
USING  del_list d
WHERE  t.id = d.id;

阅读手册中的“注释”部分TRUNCATE。特别是(如Pedro在其评论中也指出的那样):

TRUNCATE不能在具有来自其他表的外键引用的表上使用,除非所有这些表在同一命令中也被截断。[...]

和:

TRUNCATE不会ON DELETE触发表可能存在的任何触发器。


不幸的是,我确实有一些外键,但是我可以通过杀死所有键/删除/重新创建来完成您的建议。不这样做会花费更多的时间,然后再这样做。谢谢!
Anthony Greco,

@AnthonyGreco:您可以删除外键,然后再重新创建。当然,您还必须注意对已删除行的引用。并且在此窗口期间不能保证参照完整性。
Erwin Brandstetter

1
当然不是我想要做的,但是删除索引使我的删除现在飞起来了……现在只需要在所有链接表上执行此操作,以删除已链接的行,但是该死,这比我花了很多时间试图使其正常工作没有
Anthony Greco,

1
@AnthonyGreco:太好了!不要忘记在以后仍然需要重新创建这些索引。
Erwin Brandstetter

1
这是一个很棒的解决方案,如果对某人来说不明显,只需添加它即可删除删除级联。
Pedro Borges

4

我们知道PostgreSQL的更新/删除性能不如Oracle强。当我们需要删除数百万或数以千万计的行时,这确实很困难并且需要很长时间。

但是,我们仍然可以在生产数据库中执行此操作。以下是我的想法:

首先,我们应该创建一个包含两列的日志表- idflagid指您要删除的ID;flag可以是YnullY表示记录已成功删除)。

稍后,我们创建一个函数。我们每10,000行执行一次删除任务。您可以在我的博客上看到更多详细信息。尽管它是中文,但是您仍然可以从那里的SQL代码获取所需的信息。

确保id两个表的列都是索引,因为它将更快地运行。


好吧,我基本上是按批处理的逻辑,但是由于索引的原因,这花费了很长时间。我终于删除了所有索引(这是我不想做的事情),并且这些行很快被清除了。现在备份所有索引。不过谢谢!
Anthony Greco,

2

您可以尝试将表中要删除的ID之外的所有数据复制到新表中,然后重命名然后交换表(前提是您有足够的资源来执行此操作)。

这不是专家建议。


根据要保留的行数和其他外键的复杂程度,这可以起作用。也可以将好的行复制到temp。截断当前表。然后从临时复制回来。
nclu

2

两个可能的答案:

  1. 当您尝试删除记录时,您的表可能具有很多约束或触发器。这将导致很多处理器周期和从其他表进行检查。

  2. 您可能需要将此语句放入事务中。


1.我确实有一些约束(外键),这些约束在表中的一行删除时会自动删除
Anthony Greco

尝试explain (analyze,buffers,timing) ...找出丢失的索引。
Mikko Rantalainen

2

首先,请确保您要删除的表以及用于删除ID的表的ID字段都有索引。

一次100个似乎太小。尝试1000或10000。

无需从删除ID表中删除任何内容。为批次号添加一个新列,并为批次1填充1000,为批次2填充1000,以此类推,并确保删除查询中包含批次号。


2
事实证明,我尝试过的所有关键都是杀死我的钥匙。即使只有15分钟也要花一分钟左右的时间,这就是为什么我只做100分钟的原因。一旦我杀死了索引,它就会飞起来。不过谢谢!
Anthony Greco,

1

最简单的方法是删除所有约束,然后删除。


我真的想避免这种情况,因为那样我就只需要对所有外键重做该过程,但是我很可能必须这样做。谢谢
Anthony Greco,

1

我自己碰到了这个问题,到目前为止,最快的方法是将WITH Queries与结合使用

基本上,WITH查询创建一个临时表,该表具有要在其中要删除的表中删除的主键。

WITH to_delete AS (
   SELECT item_id FROM other_table WHERE condition_x = true
)
DELETE FROM table 
USING to_delete 
WHERE table.item_id = to_delete.item_id 
  AND NOT to_delete.item_id IS NULL;

当然,SELECTWITH查询的内部可以与具有多个联接等的任何其他选择一样复杂。它仅需要返回一个或多个列,这些列用于标识目标表中需要删除的项目。

注意AND NOT to_delete.item_id IS NULL很有可能不是必需的,但是我不敢尝试。

其他要考虑的是

  1. 通过外键在引用该表的其他表上创建索引。在某些情况下,这可以将删除工作从几小时减少到几秒钟
  2. 延迟约束检查:目前还不清楚有多少,如果任何改善,这达到,但根据这个可以提高性能。不利的一面是,如果您遇到外键违规,您只会在最后一刻才学会它。
  3. 危险,但可能会大大提高:在删除过程中禁用约束检查和触发器

您甚至可以创建多个相互引用的表,就像在一种情况下,我想删除所有孤立的并且不再被任何其他表引用的行时,我必须这样做。(WITH existing_items AS ( ... ), to_delete AS ( SELECT item_id FROM table LEFT JOIN existing_items e ON table.item_id = e.item_id WHERE e.item_id IS NULL ) DELETE FROM ...
Torge

0

如果您要从中删除的表被引用some_other_table(并且您甚至不想暂时删除外键),请确保在!的引用列上有索引some_other_table

我有一个类似的问题,并auto_explain与一起使用auto_explain.log_nested_statements = true,它显示delete实际在上执行seq_scans some_other_table

    Query Text: SELECT 1 FROM ONLY "public"."some_other_table" x WHERE $1 OPERATOR(pg_catalog.=) "id" FOR KEY SHARE OF x    
    LockRows  (cost=[...])  
      ->  Seq Scan on some_other_table x  (cost=[...])  
            Filter: ($1 = id)

显然,它试图锁定另一个表中的引用行(该行不应该存在,否则删除将失败)。在引用表上创建索引后,删除速度提高了几个数量级。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.