【问题标题】:Optimize Postgres deletion of orphaned records优化 Postgres 删除孤立记录
【发布时间】:2017-09-04 14:03:51
【问题描述】:

取以下两张表:

Table "public.contacts"
       Column       |            Type             |                       Modifiers                       | Storage  | Stats target | Description
--------------------+-----------------------------+-------------------------------------------------------+----------+--------------+-------------
 id                 | integer                     | not null default nextval('contacts_id_seq'::regclass) | plain    |              |
 created_at         | timestamp without time zone | not null                                              | plain    |              |
 updated_at         | timestamp without time zone | not null                                              | plain    |              |
 external_id        | integer                     |                                                       | plain    |              |
 email_address      | character varying           |                                                       | extended |              |
 first_name         | character varying           |                                                       | extended |              |
 last_name          | character varying           |                                                       | extended |              |
 company            | character varying           |                                                       | extended |              |
 industry           | character varying           |                                                       | extended |              |
 country            | character varying           |                                                       | extended |              |
 region             | character varying           |                                                       | extended |              |
 ext_instance_id    | integer                     |                                                       | plain    |              |
 title              | character varying           |                                                       | extended |              |
Indexes:
    "contacts_pkey" PRIMARY KEY, btree (id)
    "index_contacts_on_ext_instance_id_and_external_id" UNIQUE, btree (ext_instance_id, external_id)

Table "public.members"
        Column         |            Type             |                             Modifiers                              | Storage  | Stats target | Description
-----------------------+-----------------------------+--------------------------------------------------------------------+----------+--------------+-------------
 id                    | integer                     | not null default nextval('members_id_seq'::regclass)               | plain    |              |
 step_id               | integer                     |                                                                    | plain    |              |
 contact_id            | integer                     |                                                                    | plain    |              |
 rule_id               | integer                     |                                                                    | plain    |              |
 request_id            | integer                     |                                                                    | plain    |              |
 sync_id               | integer                     |                                                                    | plain    |              |
 status                | integer                     | not null default 0                                                 | plain    |              |
 matched_targeted_rule | boolean                     | default false                                                      | plain    |              |
 external_fields       | jsonb                       |                                                                    | extended |              |
 imported_at           | timestamp without time zone |                                                                    | plain    |              |
 campaign_id           | integer                     |                                                                    | plain    |              |
 ext_instance_id       | integer                     |                                                                    | plain    |              |
 created_at            | timestamp without time zone |                                                                    | plain    |              |
Indexes:
    "members_pkey" PRIMARY KEY, btree (id)
    "index_members_on_contact_id_and_step_id" UNIQUE, btree (contact_id, step_id)
    "index_members_on_campaign_id" btree (campaign_id)
    "index_members_on_step_id" btree (step_id)
    "index_members_on_sync_id" btree (sync_id)
    "index_members_on_request_id" btree (request_id)
    "index_members_on_status" btree (status)

主键和members.contact_id都有索引。

我需要删除没有相关members 的任何contact。大约有 3MM contact 和 25MM member 记录。

我正在尝试以下两个查询:

查询 1:

DELETE FROM "contacts"
WHERE  "contacts"."id" IN (SELECT "contacts"."id" 
                           FROM   "contacts" 
                                  LEFT OUTER JOIN members 
                                               ON 
                                  members.contact_id = contacts.id 
                           WHERE  members.id IS NULL);

DELETE 0
Time: 173033.801 ms

-----------------------------------------------------------------------------------------------------------------------------------------------------------------
 Delete on contacts  (cost=2654306.79..2654307.86 rows=1 width=18) (actual time=188717.354..188717.354 rows=0 loops=1)
   ->  Nested Loop  (cost=2654306.79..2654307.86 rows=1 width=18) (actual time=188717.351..188717.351 rows=0 loops=1)
         ->  HashAggregate  (cost=2654306.36..2654306.37 rows=1 width=16) (actual time=188717.349..188717.349 rows=0 loops=1)
               Group Key: contacts_1.id
               ->  Hash Right Join  (cost=161177.46..2654306.36 rows=1 width=16) (actual time=188717.345..188717.345 rows=0 loops=1)
                     Hash Cond: (members.contact_id = contacts_1.id)
                     Filter: (members.id IS NULL)
                     Rows Removed by Filter: 26725870
                     ->  Seq Scan on members  (cost=0.00..1818698.96 rows=25322396 width=14) (actual time=0.043..160226.686 rows=26725870 loops=1)
                     ->  Hash  (cost=105460.65..105460.65 rows=3205265 width=10) (actual time=1962.612..1962.612 rows=3196180 loops=1)
                           Buckets: 262144  Batches: 4  Memory Usage: 34361kB
                           ->  Seq Scan on contacts contacts_1  (cost=0.00..105460.65 rows=3205265 width=10) (actual time=0.011..950.657 rows=3196180 loops=1)
         ->  Index Scan using contacts_pkey on contacts  (cost=0.43..1.48 rows=1 width=10) (never executed)
               Index Cond: (id = contacts_1.id)
 Planning time: 0.488 ms
 Execution time: 188718.862 ms

查询 2:

DELETE FROM contacts 
WHERE  NOT EXISTS (SELECT 1 
                   FROM   members c 
                   WHERE  c.contact_id = contacts.id); 

DELETE 0
Time: 170871.219 ms

-------------------------------------------------------------------------------------------------------------------------------------------------------------
 Delete on contacts  (cost=2258873.91..2954594.50 rows=1895601 width=12) (actual time=177523.034..177523.034 rows=0 loops=1)
   ->  Hash Anti Join  (cost=2258873.91..2954594.50 rows=1895601 width=12) (actual time=177523.029..177523.029 rows=0 loops=1)
         Hash Cond: (contacts.id = c.contact_id)
         ->  Seq Scan on contacts  (cost=0.00..105460.65 rows=3205265 width=10) (actual time=0.018..1068.357 rows=3196180 loops=1)
         ->  Hash  (cost=1818698.96..1818698.96 rows=25322396 width=10) (actual time=169587.802..169587.802 rows=26725870 loops=1)
               Buckets: 262144  Batches: 32  Memory Usage: 36228kB
               ->  Seq Scan on members c  (cost=0.00..1818698.96 rows=25322396 width=10) (actual time=0.052..160081.880 rows=26725870 loops=1)
 Planning time: 0.901 ms
 Execution time: 177524.526 ms

如您所见,即使不删除任何记录,两个查询也显示出相似的性能,大约需要 3 分钟。

服务器磁盘 I/O 达到 100%,因此我假设数据正在溢出到磁盘,因为在 contactsmembers 上都进行了顺序扫描。

服务器是 EC2 r3.large(15GB RAM)。

有什么想法可以优化这个查询吗?

更新 #1:

在为两个表运行vacuum analyze 并确保将enable_mergejoin 设置为on 后,查询时间没有差异:

DELETE FROM contacts 
WHERE  NOT EXISTS (SELECT 1 
                   FROM   members c 
                   WHERE  c.contact_id = contacts.id); 

-------------------------------------------------------------------------------------------------------------------------------------------------------------
 Delete on contacts  (cost=2246088.17..2966677.08 rows=1875003 width=12) (actual time=209406.342..209406.342 rows=0 loops=1)
   ->  Hash Anti Join  (cost=2246088.17..2966677.08 rows=1875003 width=12) (actual time=209406.338..209406.338 rows=0 loops=1)
         Hash Cond: (contacts.id = c.contact_id)
         ->  Seq Scan on contacts  (cost=0.00..105683.28 rows=3227528 width=10) (actual time=0.008..1010.643 rows=3227462 loops=1)
         ->  Hash  (cost=1814029.74..1814029.74 rows=24855474 width=10) (actual time=198054.302..198054.302 rows=27307060 loops=1)
               Buckets: 262144  Batches: 32  Memory Usage: 37006kB
               ->  Seq Scan on members c  (cost=0.00..1814029.74 rows=24855474 width=10) (actual time=1.132..188654.555 rows=27307060 loops=1)
 Planning time: 0.328 ms
 Execution time: 209408.040 ms

更新 2:

PG 版本:

PostgreSQL 9.4.4 on x86_64-pc-linux-gnu, compiled by x86_64-pc-linux-gnu-gcc (Gentoo Hardened 4.5.4 p1.0, pie-0.4.7) 4.5.4, 64-bit

关系大小:

         Table         |  Size   | External Size
-----------------------+---------+---------------
 members               | 23 GB   | 11 GB
 contacts              | 944 MB  | 371 MB

设置:

 work_mem
----------
 64MB

 random_page_cost
------------------
 4

更新 3:

尝试分批执行此操作似乎对 I/O 使用率没有帮助(仍然飙升至 100%),并且尽管使用了基于索引的计划,但似乎也没有按时改善。

DO $do$ 
BEGIN 
  FOR i IN 57..668 
  LOOP 
    DELETE 
    FROM   contacts 
    WHERE  contacts.id IN 
           ( 
                           SELECT          contacts.id 
                           FROM            contacts 
                           left outer join members 
                           ON              members.contact_id = contacts.id 
                           WHERE           members.id IS NULL 
                           AND             contacts.id >= (i    * 10000) 
                           AND             contacts.id < ((i+1) * 10000));
END LOOP;END $do$;

我不得不在Time: 1203492.326 ms 之后终止查询,并且磁盘 I/O 在整个查询运行期间保持在 100%。我还试验了 1,000 和 5,000 个块,但没有发现性能有任何提升。

注意:使用 57..668 范围是因为我知道这些是现有的联系人 ID。 (例如min(id)max(id)

【问题讨论】:

  • 直觉:你的work_mem设置太high,random_page_cost太高,可能统计数据过时或缺失。顺便说一句,表定义应该是:... contact_id integer references contact(id) PLUS:EXPLAIN ANALYZE,请...
  • 问题已更新。至于外键约束,这是在不会自动添加它们的 Rails 应用程序的上下文中。但是,我的印象是外键约束仅用于维护参照完整性。您是否建议使用它们来提高查询性能?
  • members.contact_id 上有索引吗?另外:在查询 2 中使用“*”而不是“1”。这是 postgresql 中的推荐做法。
  • members.contact_id 上有一个索引(它是与另一个字段的复合索引,并且是唯一的。)注意“*” - 但是这对查询性能有影响吗?
  • 如果members.contact_id 是复合索引中的第一个成员,它将可用。在 psql 中顺便说一句,您可以使用 \d members 来显示当前表结构。甚至可以添加+

标签: sql postgresql query-optimization


【解决方案1】:

更新计划器使用的统计信息并将enable_mergejoin设置为on

vacuum analyse members;
vacuum analyse contacts;
set enable_mergejoin to on;

你应该得到一个类似于这个的查询计划:

explain analyse
delete from contacts 
where not exists (
    select 1 
    from members c 
    where c.contact_id = contacts.id); 

                               QUERY PLAN
----------------------------------------------------------------------
 Delete on contacts
   ->  Merge Anti Join
         Merge Cond: (contacts.id = c.contact_id)
         ->  Index Scan using contacts_pkey on contacts
         ->  Index Scan using members_contact_id_idx on members c

【讨论】:

  • 感谢您的建议。我已经用查询计划更新了我的问题,但不幸的是它对查询时间没有帮助。
  • 很奇怪你的服务器不使用索引。明显的原因是如果没有设置参数enable_mergejoin。另一种可能性是将enable_indexscan 设置为关闭,但您问题中的另一个计划表明它已打开。我已经在三台服务器(具有不同的 Postgres 版本)上测试了 1M/2M 数据的查询,它们都给出了答案中的计划。
  • 感谢您的帮助。服务器是9.4。我假设这是导致不同计划的关系大小。
【解决方案2】:

解决此类问题的一种方法是分小块进行。

DELETE FROM "contacts"
WHERE  "contacts"."id" IN (
    SELECT id
    FROM contacts
    LEFT OUTER JOIN members ON members.contact_id = contacts.id 
    WHERE members.id IS NULL
        AND id >= 1 AND id < 1000
);
DELETE FROM "contacts"
WHERE  "contacts"."id" IN (
    SELECT id
    FROM contacts
    LEFT OUTER JOIN members ON members.contact_id = contacts.id 
    WHERE members.id IS NULL
        AND id >= 1001 AND id < 2000
);

冲洗,重复。尝试不同的块大小,为您的数据集找到一个最佳的块,它使用最少的查询,同时将它们全部保存在内存中。

当然,您可能希望使用 plpgsql 或您喜欢的任何脚本语言编写此脚本。

【讨论】:

  • 在找到合适的块大小以在每个块之后循环和提交后,我会将其包装在 pgplsql 块中。为这个想法 +1
  • @JorgeCampos:是的,当然!它应该是自动化的。值得一提的是,在答案中明确指出。感谢您的建议。
  • 感谢您的回复。我希望避免批量处理这些,但如果这会减少资源使用 - 我会试一试。请给我一点时间来编写一个脚本,我可以针对我的生产数据库进行测试,然后我会报告时间。
  • 我已经用批处理查询的结果更新了我的问题。似乎没有改善时间和磁盘 IO。我假设这可以帮助维持恒定的内存配置文件,但对于需要从磁盘读取两个表来说并没有太大帮助。如果我在查询中误解或做错了什么,请告诉我。谢谢! (刚刚意识到,我在我的问题中特别说过我需要更少的内存消耗 - 我想这是相关的,但并不完全准确。我希望在运行它时减少磁盘 IO。很抱歉造成混乱。)
  • @user1032752:如果您的 ID 是连续的,没有间隔,您可以通过包含 LIMIT 10000 来提高性能。这将告诉查询,一旦获取 10000 行,它就可以停止表扫描(针对子查询)。如果你有差距,那么如果不进一步重写你的子查询,你可能永远不会达到这个限制。
【解决方案3】:

这是另一个尝试的变体:

DELETE FROM contacts
USING contacts c
LEFT JOIN members m
ON c.id = m.contact_id
WHERE m.contact_id IS NULL;

它使用一种技术从here 描述的连接查询中删除。

我不能保证这是否肯定会更快,但这可能是因为避免了子查询。会对结果感兴趣...

【讨论】:

  • 感谢您的建议。不知道为什么,但上面的查询最终花费了 30 多分钟,我不得不杀死它。它在我的本地环境中运行良好,但在生产中(考虑到我想象的数量)它似乎运行不佳。
  • 那个查询没有做正确的事情。删除所在的表之间没有连接条件。所以它要么删除所有内容,要么什么都不删除。
【解决方案4】:

有什么想法可以优化这个查询吗?

您的查询很完美。我会使用NOT EXISTS 变体。

你的索引index_members_on_contact_id_and_step_id也有好处:

但请参阅下面有关 BRIN 索引的信息。

您可以调整您的服务器、表和索引配置。

由于您实际上并没有更新或删除许多行(根据您的评论,几乎没有?),您需要优化 读取 性能。

1。升级您的 Postgres 版本

您提供:

服务器是 EC2 r3.large(15GB RAM)。

还有:

PostgreSQL 9.4.4

您的版本严重过时。 至少升级到最新的次要版本。更好的是,升级到当前的主要版本。 Postgres 9.5 和 9.6 为大数据带来了重大改进——这正是您所需要的。

Consider the versioning policy of the project.

Amazon allows you to upgrade!

2。改进表统计

在基本顺序扫描中,预期的行数和实际的行数之间存在意外的 10% 的不匹配:

对成员 c 的 Seq Scan (cost=0.00..1814029.74 rows=24855474 width=10) (实际时间=1.132..188654.555 rows=27307060 loops=1 )

一点也不戏剧化,但仍然不应该出现在这个查询中。表示您可能需要调整您的 autovacuum 设置 - 可能针对非常大的每个表。

更多问题:

Hash Anti Join (cost=2246088.17..2966677.08 rows=1875003 width=12) (实际时间=209406.338..209406.338 rows=0 loops=1)

Postgres 期望找到 1875003 行要删除,而实际上找到了 0 行。这是出乎意料的。也许大幅增加members.contact_idcontacts.id 上的统计目标可以帮助减少差距,这可能允许更好的查询计划。见:

3。避免表和索引膨胀

members 中的 ~ 25MM 行占用 23 GB - 每行几乎 1kb,这对于您提供的表定义来说似乎过多(即使您提供的总大小应该包括索引):

 4 bytes  item identifier

24        tuple header
 8        null bitmap
36        9x integer
16        2x ts
 1        1x bool
??        1x jsonb

见:

每行 89 个字节 - 或更少,一些 NULL 值 - 几乎没有任何对齐填充,所以 最多 96 个字节,加上你的 jsonb 列。 p>

要么 jsonb 列非常大,这让我建议将数据规范化为单独的列或单独的表。考虑:

或者你的桌子很臃肿,这可以用VACUUM FULL ANALYZE解决,或者,在它旁边:

CLUSTER members USING index_members_on_contact_id_and_step_id;
VACUUM members;

但是要么在表上使用排他锁,你说你负担不起。 pg_repack 可以在没有排他锁的情况下做到这一点。见:

即使我们考虑索引大小,您的表似乎也太大了:您有 7 个小索引,每行 36 - 44 个字节,没有膨胀,更少的 NULL 值,所以总共

无论哪种方式,请考虑将more aggressive autovacuum settings 用于您的表members。相关:

和/或停止膨胀表开始。您是否经常更新行?您经常更新的任何特定列?那个jsonb 专栏可能吗?您可以将其移动到单独的 (1:1) 表中,以停止使用死元组使主表膨胀 - 并阻止 autovacuum 完成其工作。

4。试试 BRIN 索引

Block range indexes 需要 Postgres 9.5 或更高版本并且显着减小索引大小。我在初稿中过于乐观。如果每个contact.idmembers 中有很多 行,则BRIN 索引对于您的用例来说是完美 - 在物理聚类 表之后至少一次(参见 ③ 的拟合CLUSTER 命令)。在这种情况下,Postgres 可以快速排除整个数据页。但是您的数字表明每个contact.id 仅包含大约 8 行,因此数据页通常会包含多个值,这会使大部分效果无效。取决于您的数据分布的实际细节...

另一方面,就目前而言,您的元组大小约为 1 kb,因此每个数据页只有约 8 行(通常为 8kb)。如果这不是主要的膨胀,那么 BRIN 索引可能会有所帮助。

但是你需要先升级你的服务器版本。见①。

CREATE INDEX members_contact_id_brin_idx ON members USING BRIN (contact_id);

【讨论】:

  • 感谢您在这里提供的丰富信息。升级在我的清单上,但需要一些时间。同时,我已经使用您建议的索引对表进行了聚类,但这并没有减少时间/磁盘使用量。对于members 表,只有status 和一些_id 列得到更新。其他列(包括 jsonb)仅在插入时填充一次,然后用于查询。如果我将其移动到单独的 1-1 表中 - 我是否也不必遍历该表并删除行时对应的members 被删除了?恐怕这会导致更多开销。
  • 在您看来,我应该先专注于升级还是尝试调整我当前的服务器?你认为最大的好处是什么?非常感谢。
  • @user1032752:如果你的桌子很臃肿,我会先把它清理干净。并为表添加更积极的自动真空设置。然后升级到最新的 Postgres 版本,它有各种好处。然后尝试再优化一些。这取决于你的整体情况。
【解决方案5】:

在 where 子句中使用子查询需要很多时间 你应该使用withusing 这会很多很多很多...更快

with 
    c_not_member as (
     -- here extarct the id of contacts that not in members
     SELECT 
        c.id
     FROM  contacts c LEFT JOIN  members m on c.id = m.contact_id
     WHERE  
        -- to get the contact that don't exist in member just
        -- use condition in a field on member that cannot be null
        -- in this case you have id
        m.id is null
        -- the only case when m.id is null is when c.id does not have m.contact_id maching c.id
        -- in another way c.id doesn't exists in m.contact_id 
                   )
DELETE FROM contacts all_c using c_not_member WHERE all_c.id = not_member.id ;

【讨论】:

  • 感谢您的建议。我已经对此进行了测试,并得到了与上述查询类似的结果。 Time: 249600.967 ms.
猜你喜欢
  • 2012-09-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-10-07
  • 2015-03-13
相关资源
最近更新 更多