【问题标题】:Very slow query performance in aws postgresql for a table with 4 billion rows对于具有 40 亿行的表,aws postgresql 中的查询性能非常慢
【发布时间】:2023-02-24 01:45:51
【问题描述】:

问题

我们有一个关系表,用于存储用户活动。像下面这样的查询77秒!

FROM "site_activity"
WHERE
    (
        NOT "site_activity"."is_deleted"
        AND "site_activity"."user_id" = 68812389
        AND NOT (
            "site_activity"."kind" IN (
                'updated',
                'duplicated',
                'reapplied'
            )
        )
        AND NOT (
            "site_activity"."content_type_id" = 14
            AND "site_activity"."kind" = 'created'
        )
    )
ORDER BY
    "site_activity"."created_at" DESC,
    "site_activity"."id" DESC
LIMIT  9;

查询计划看起来像这样

                                     QUERY PLAN
--------------------------------------------------------------------------------------------
Limit
    (cost=17750.72..27225.75 rows=9 width=16)
    (actual time=199501.336..199501.338 rows=9 loops=1)
  Output: id, created_at
  Buffers: shared hit=4502362 read=693523 written=37273
  I/O Timings: read=190288.205 write=446.870
  ->  Incremental Sort
      (cost=17750.72..2003433582.97 rows=1902974 width=16)
      (actual time=199501.335..199501.336 rows=9 loops=1)
        Output: id, created_at
        Sort Key: site_activity.created_at DESC, site_activity.id DESC
        Presorted Key: site_activity.created_at
        Full-sort Groups: 1  Sort Method: quicksort  Average Memory: 25kB  Peak Memory: 25kB
        Buffers: shared hit=4502362 read=693523 written=37273
        I/O Timings: read=190288.205 write=446.870
        ->  Index Scan Backward using site_activity_created_at_company_id_idx on public.site_activity
            (cost=0.58..2003345645.30 rows=1902974 width=16)
            (actual time=198971.283..199501.285 rows=10 loops=1)
              Output: id, created_at
              Filter: (
                (NOT site_activity.is_deleted) AND (site_activity.user_id = 68812389)
                AND ((site_activity.kind)::text <> ALL ('{updated,duplicated,reapplied}'::text[]))
                AND ((site_activity.content_type_id <> 14) OR ((site_activity.kind)::text <> 'created'::text))
              )
              Rows Removed by Filter: 14735308
              Buffers: shared hit=4502353 read=693523 written=37273
              I/O Timings: read=190288.205 write=446.870
Settings: effective_cache_size = '261200880kB',
          effective_io_concurrency = '400',
          jit = 'off',
          max_parallel_workers = '24',
          random_page_cost = '1.5',
          work_mem = '64MB'
Planning:
  Buffers: shared hit=344
Planning Time: 6.429 ms
Execution Time: 199501.365 ms
(22 rows)

Time: 199691.997 ms (03:19.692)

表格事实

  1. 它包含的内容略多于40 亿行.

  2. 表结构是

                                                Table "public.site_activity"
        Column      |           Type           | Collation | Nullable |                   Default
    ----------------+--------------------------+-----------+----------+----------------------------------------------
    id              | bigint                   |           | not null | nextval('site_activity_id_seq'::regclass)
    created_at      | timestamp with time zone |           | not null |
    modified_at     | timestamp with time zone |           | not null |
    is_deleted      | boolean                  |           | not null |
    object_id       | bigint                   |           | not null |
    kind            | character varying(32)    |           | not null |
    context         | text                     |           | not null |
    company_id      | integer                  |           | not null |
    content_type_id | integer                  |           | not null |
    user_id         | integer                  |           |          |
    Indexes:
        "site_activity_pkey" PRIMARY KEY, btree (id)
        "site_activity_modified_at_idx" btree (modified_at)
        "site_activity_company_id_idx" btree (company_id)
        "site_activity_created_at_company_id_idx" btree (created_at, company_id)
        "site_activity_object_id_idx" btree (object_id)
        "site_activity_content_type_id_idx" btree (content_type_id)
        "site_activity_kind_idx" btree (kind)
        "site_activity_kind_idx1" btree (kind varchar_pattern_ops)
        "site_activity_user_id_idx" btree (user_id)
    Foreign-key constraints:
        "site_activity_company_id_fk_site_company_id" FOREIGN KEY (company_id)
            REFERENCES site_company(id) DEFERRABLE INITIALLY DEFERRED
        "site_activity_content_type_id_fk_django_co" FOREIGN KEY (content_type_id)
            REFERENCES django_content_type(id) DEFERRABLE INITIALLY DEFERRED
        "site_activity_user_id_fk_site_user_id" FOREIGN KEY (user_id)
            REFERENCES site_user(id) DEFERRABLE INITIALLY DEFERRED
    

    A。 kind 实际上是一个enum。其中大约有 100 个值。

    b.content_type_id 有大约 80 个值。

  3. 这是值的分布,

    A。 context 实际上是最大 8Mb 大小的 JSON。

    A。 3 content_type_id 值保持92%的行

    A。 3 kind消费75%行。

    A。 kindcontent_type_id 的组合创建了 460 个值。其中,2 组合包含 65% 的行,我们始终在查询中排除它们。

  4. 副本实例的类型为 db.r5.12xlarge24核心,48岁vCPU,384GB内存,存储类型IO1.

    问题

    1. 如果表增长到1000亿?在目前的预测中,这可能会在未来 3-5 年内发生。
    2. NoSQL 是一个好的解决方案吗?请注意,我们不是仅使用 id 或 kind 访问文档。

      笔记

      1. 我提供的事实可能会使解决方案偏向于在同一主机中进行复制,然后在多个主机上进行分片。但如果有其他解决方案可以保持到 1000 亿大关,我们应该很好。
      2. 我们不必使用 AWS。但首选.

【问题讨论】:

  • 性能将直接关系到硬件规格/cpus、并行查询的能力以及您如何调整查询/索引表/分区数据
  • 您可以考虑像 clickhouse 这样的内存数据库。虽然不是关系数据库,但它与 Postgres 兼容
  • 发布解释计划将在调整该查询方面获得更直接的响应。
  • 您能否分享您的 SQL 语句的 EXPLAIN(ANALYZE, VERBOSE, BUFFERS, SETTINGS) 的结果? (以纯文本形式,作为您问题的更新)
  • @FrankHeikens 我已经添加了您要求的解释!

标签: sql database postgresql amazon-web-services amazon-rds


【解决方案1】:

当前的计划是扫描已按“created_at”(使用索引)排序的行,然后在找到 10 行(加上可能有几行以说明关系)通过其余条件时停止。它认为它会很快执行此操作,仅在表的大约 1/73,000 (27225.75 / 2003433582.97) 之后。但实际上它必须扫描的远不止这些(14735308 / 4000000000,或表的 1/270)。所以它严重错误地估计了那部分。我不知道它是否估计错了因为满足条件的行数估计不正确(它认为会有 1902974,我们不知道实际有多少,因为它提前停止所以停止计算它们)或因为它假定匹配的行会均匀地分布在索引中,而实际上并非如此。

最适合您的索引可能是(user_id, created_at)。这样你就可以跳转到索引中具有正确 user_id 的部分(我假设这是你的绝大多数选择性的来源),然后仍然按照“created_at”的顺序遍历该部分。您可以删除 (user_id) 上的原始索引,因为新索引适用于旧索引适用的所有内容。您还可以在该索引的其他两列之间添加“is_deleted”,因为它不会破坏排序属性并且会提供一些额外的选择性(但可能不多)。但是,在那里添加的任何其他列都会破坏排序属性。

【讨论】:

    【解决方案2】:

    试试这样的索引,它应该覆盖你的 where 条件中的所有列:

    CREATE INDEX idx_stackoverflow ON site_activity (created_at DESC, user_id, is_deleted, kind, content_type_id);
    

    当前使用的索引过滤掉了近 1500 万条记录,只得到 9 条记录。这是对时间的巨大浪费。

    也许您想同时创建此索引,这可能需要一些额外的时间,但不会阻止其他进程。

    【讨论】:

    • 这不是一个巨大的索引吗?如果数字或行数接近 100B,您将如何处理?
    • @ShipluMokaddim:你对“巨大”的定义是什么?对于如此多的记录,我会看一下表分区,以保持分区更小,索引更小。
    • 巨大的意思是索引中的行数会很多,不是吗?虽然他们仍然可以访问O(logN)。但对于 100B 大关,这可能是可观的。如果我错了,请纠正我。
    • 你为什么在索引中选择 company_id?
    • @FrankHeikens 我不同意这个索引。如我所见,主要访问谓词是"site_activity"."user_id" = 68812389。然后列is_deletedkindcontent_type_id可用于过滤。最后,可以通过具体化结果来应用排序。我假设没有多少行匹配访问谓词。
    猜你喜欢
    • 2015-07-10
    • 2021-04-21
    • 1970-01-01
    • 1970-01-01
    • 2015-02-06
    • 1970-01-01
    • 2021-12-23
    • 1970-01-01
    相关资源
    最近更新 更多