提高 PostgresSQL 聚合查询性能

Posted

技术标签:

【中文标题】提高 PostgresSQL 聚合查询性能【英文标题】:Improve PostgresSQL aggregation query performance 【发布时间】:2020-05-06 04:33:30 【问题描述】:

我正在聚合来自 Postgres 表的数据,查询大约需要 2 秒,我想将其减少到不到一秒。

请在下面找到执行细节:


查询

select
    a.search_keyword,
    hll_cardinality( hll_union_agg(a.users) ):: int as user_count,
    hll_cardinality( hll_union_agg(a.sessions) ):: int as session_count,
    sum(a.total) as keyword_count
from
    rollup_day a
where
    a.created_date between '2018-09-01' and '2019-09-30'
    and a.tenant_id = '62850a62-19ac-477d-9cd7-837f3d716885'
group by
    a.search_keyword
order by
    session_count desc
limit 100;

表元数据

    总行数 - 506527 列上的复合索引:tenant_id 和 created_date


查询计划

Custom Scan (cost=0.00..0.00 rows=0 width=0) (actual time=1722.685..1722.694 rows=100 loops=1)
  Task Count: 1
  Tasks Shown: All
  ->  Task
        Node: host=localhost port=5454 dbname=postgres
        ->  Limit  (cost=64250.24..64250.49 rows=100 width=42) (actual time=1783.087..1783.106 rows=100 loops=1)
              ->  Sort  (cost=64250.24..64558.81 rows=123430 width=42) (actual time=1783.085..1783.093 rows=100 loops=1)
                    Sort Key: ((hll_cardinality(hll_union_agg(sessions)))::integer) DESC
                    Sort Method: top-N heapsort  Memory: 33kB
                    ->  GroupAggregate  (cost=52933.89..59532.83 rows=123430 width=42) (actual time=905.502..1724.363 rows=212633 loops=1)
                          Group Key: search_keyword
                          ->  Sort  (cost=52933.89..53636.53 rows=281055 width=54) (actual time=905.483..1351.212 rows=280981 loops=1)
                                Sort Key: search_keyword
                                Sort Method: external merge  Disk: 18496kB
                                ->  Seq Scan on rollup_day a  (cost=0.00..17890.22 rows=281055 width=54) (actual time=29.720..112.161 rows=280981 loops=1)
                                      Filter: ((created_date >= '2018-09-01'::date) AND (created_date <= '2019-09-30'::date) AND (tenant_id = '62850a62-19ac-477d-9cd7-837f3d716885'::uuid))
                                      Rows Removed by Filter: 225546
            Planning Time: 0.129 ms
            Execution Time: 1786.222 ms
Planning Time: 0.103 ms
Execution Time: 1722.718 ms

我的尝试

    我尝试过使用 tenant_id 和 created_date 上的索引,但由于数据量很大,所以它总是进行序列扫描,而不是过滤器的索引扫描。我已经阅读并发现,如果返回的数据大于总行数的 5-10%,则 Postgres 查询引擎会切换到顺序扫描。请点击链接了解更多reference。 我已将 work_mem 增加到 100MB,但它只提高了一点性能。

任何帮助将不胜感激。


更新

设置work_mem为100MB后的查询计划

Custom Scan (cost=0.00..0.00 rows=0 width=0) (actual time=1375.926..1375.935 rows=100 loops=1)
  Task Count: 1
  Tasks Shown: All
  ->  Task
        Node: host=localhost port=5454 dbname=postgres
        ->  Limit  (cost=48348.85..48349.10 rows=100 width=42) (actual time=1307.072..1307.093 rows=100 loops=1)
              ->  Sort  (cost=48348.85..48633.55 rows=113880 width=42) (actual time=1307.071..1307.080 rows=100 loops=1)
                    Sort Key: (sum(total)) DESC
                    Sort Method: top-N heapsort  Memory: 35kB
                    ->  GroupAggregate  (cost=38285.79..43996.44 rows=113880 width=42) (actual time=941.504..1261.177 rows=172945 loops=1)
                          Group Key: search_keyword
                          ->  Sort  (cost=38285.79..38858.52 rows=229092 width=54) (actual time=941.484..963.061 rows=227261 loops=1)
                                Sort Key: search_keyword
                                Sort Method: quicksort  Memory: 32982kB
                                ->  Seq Scan on rollup_day_104290 a  (cost=0.00..17890.22 rows=229092 width=54) (actual time=38.803..104.350 rows=227261 loops=1)
                                      Filter: ((created_date >= '2019-01-01'::date) AND (created_date <= '2019-12-30'::date) AND (tenant_id = '62850a62-19ac-477d-9cd7-837f3d716885'::uuid))
                                      Rows Removed by Filter: 279266
            Planning Time: 0.131 ms
            Execution Time: 1308.814 ms
Planning Time: 0.112 ms
Execution Time: 1375.961 ms

更新 2

created_date 创建索引并将 work_mem 增加到 120MB

create index date_idx on rollup_day(created_date);

总行数为:12,124,608

查询计划是:

Custom Scan (cost=0.00..0.00 rows=0 width=0) (actual time=2635.530..2635.540 rows=100 loops=1)
  Task Count: 1
  Tasks Shown: All
  ->  Task
        Node: host=localhost port=9702 dbname=postgres
        ->  Limit  (cost=73545.19..73545.44 rows=100 width=51) (actual time=2755.849..2755.873 rows=100 loops=1)
              ->  Sort  (cost=73545.19..73911.25 rows=146424 width=51) (actual time=2755.847..2755.858 rows=100 loops=1)
                    Sort Key: (sum(total)) DESC
                    Sort Method: top-N heapsort  Memory: 35kB
                    ->  GroupAggregate  (cost=59173.97..67948.97 rows=146424 width=51) (actual time=2014.260..2670.732 rows=296537 loops=1)
                          Group Key: search_keyword
                          ->  Sort  (cost=59173.97..60196.85 rows=409152 width=55) (actual time=2013.885..2064.775 rows=410618 loops=1)
                                Sort Key: search_keyword
                                Sort Method: quicksort  Memory: 61381kB
                                ->  Index Scan using date_idx_102913 on rollup_day_102913 a  (cost=0.42..21036.35 rows=409152 width=55) (actual time=0.026..183.370 rows=410618 loops=1)
                                      Index Cond: ((created_date >= '2018-01-01'::date) AND (created_date <= '2018-12-31'::date))
                                      Filter: (tenant_id = '12850a62-19ac-477d-9cd7-837f3d716885'::uuid)
            Planning Time: 0.135 ms
            Execution Time: 2760.667 ms
Planning Time: 0.090 ms
Execution Time: 2635.568 ms

【问题讨论】:

这个“排序方法:外部合并磁盘:18496kB”占用了大部分时间。您可能需要将 work_mem 增加到 100MB 以上,直到它消失。 @a_horse_with_no_name,感谢您的回复。这仅占用 18MB 内存,而我的 work_mem 为 64 MB。为什么它仍然使用磁盘进行排序操作。 磁盘上的大小远小于内存中的大小(磁盘操作针对小尺寸进行了优化,以使其在性能上至少可以接受)。内存中排序所需的内存通常比这大得多。也许hll_union_agg 需要那么多内存。 @a_horse_with_no_name,感谢,我有 4 核 16GB EC2 机器。您能否为这个系统推荐一些基准? 表示 4 核 16GB EC2 机器需要多少 work_mem。 【参考方案1】:

使用表分区并创建一个复合索引,它将降低总成本:

它将为您节省大量扫描费用。 分区将隔离数据,并且在未来的清除操作中也非常有用。

我已经亲自尝试和测试过这种情况下的表分区,并且通过结合使用吞吐量是惊人的 分区和复合索引。

可以在创建日期范围内进行分区,然后在日期和租户上进行复合索引。

请记住,如果您的查询中的条件有非常具体的要求,那么您始终可以拥有一个包含条件的复合索引。这样数据就已经在索引中进行了排序,也可以节省大量的排序操作成本。

希望这会有所帮助。

PS:另外,是否可以共享任何相同的测试样本数据?

【讨论】:

谢谢,Raj,我知道分区,并且仅当该特定范围的数据集受到限制或适合该范围时才有效,但就我而言,数据集确实很大。如果 created_date 适合分区范围,我将查询优化为 900 毫秒。一旦查询超出范围,它就需要对分区进行分组,这是一项繁重的操作。 我确实理解您的担忧,但在这种情况下查询优化器会做的是跳过排序和前 N 堆,试一试,以防万一它不起作用,您可以随时使用调整工作内存。使用这个我已经能够将过去的查询从 84000 毫秒缩短到 10 毫秒!真实的故事。 欣赏,很高兴听到这个消息。 work_mem 只需要在内存而不是磁盘中移动排序或连接计算,我已经实现了,所以我认为增加 work_mem 不会对查询产生更多影响。 如果您需要更详细的信息,请告诉我,如果您可以分享一些样本,也许我可以看看。祝你好运! :) 确定我的linkedin在我的个人资料中,随时联系!【参考方案2】:

我的建议是拆分选择。 现在我也会尝试结合它在桌子上设置 2 个索引。一个在日期上,另一个在 ID 上。奇怪的 ID 的问题之一是,比较需要时间,并且可以在后台将它们视为字符串比较。这就是为什么要在执行 between 命令之前预先过滤数据的原因。现在 between 命令可以使选择变慢。在这里,我建议将其分解为 2 个选择和内部连接(我现在内存消耗是一个问题)。

这是我的意思的一个例子。我希望优化器足够聪明,可以重构您的查询。

SELECT 
    a.search_keyword,
    hll_cardinality( hll_union_agg(a.users) ):: int as user_count,
    hll_cardinality( hll_union_agg(a.sessions) ):: int as session_count,
    sum(a.total) as keyword_count
FROM
    (SELECT
        *
    FROM
        rollup_day a
    WHERE
        a.tenant_id = '62850a62-19ac-477d-9cd7-837f3d716885') t1 
WHERE
    a.created_date between '2018-09-01' and '2019-09-30'
group by
    a.search_keyword
order by
    session_count desc

现在,如果这不起作用,那么您需要更具体的优化。例如。总数是否可以等于 0,那么您需要对总数 > 0 的数据进行过滤索引。是否有任何其他条件可以轻松地从选择中排除行。

下一个考虑是创建一个有短 ID 的行(而不是 62850a62-19ac-477d-9cd7-837f3d716885 -> 62850 ),它可以是一个数字,这将使预选变得非常容易并消耗内存较少的。

【讨论】:

此查询的性能更差。您的查询耗时超过 3 秒。 我认为您必须重新索引,复合索引不适用于该查询,您能否发布执行计划,它将显示需要索引的内容。只需编辑我的帖子,这会给我一张图片优化器做了什么。 一个小薄,你需要摆脱你的 UUID,最好做一个有整数和 UUID 关系的表,percona.com/blog/2019/11/22/…【参考方案3】:

您是否尝试过Covering indexes,所以优化器将使用索引,而不是进行顺序扫描?

create index covering on rollup_day(tenant_id, created_date, search_keyword, users, sessions, total);

如果 Postgres 11

create index covering on rollup_day(tenant_id, created_date) INCLUDE (search_keyword, users, sessions, total);

但由于您也可能对search_keyword 进行排序/分组:

create index covering on rollup_day(tenant_id, created_date, search_keyword);
create index covering on rollup_day(tenant_id, search_keyword, created_date);

或者:

create index covering on rollup_day(tenant_id, created_date, search_keyword) INCLUDE (users, sessions, total);
create index covering on rollup_day(tenant_id, search_keyword, created_date) INCLUDE (users, sessions, total);

这些索引之一应该使查询更快。您应该只添加这些索引中的一个

即使它使此查询更快,拥有大索引也会/可能会使您的写入操作变慢(尤其是索引列上不可用的 HOT 更新)。而且您将使用更多存储空间。

Idea came from here ,还有一个关于 work_mem 大小的提示 Another example where the index was not used

【讨论】:

感谢您的回答,我一定会尝试您提到的索引。 好的,让我知道是否有什么效果最好,或者您是否有解释。我也想过将search_keyword 放在首位,但我认为这行不通。 在我的情况下只有一个顺序索引在工作'在 rollup_day(search_keyword desc); 上创建索引 s_k_idx;'我试过休息,但没有运气。 您的意思是在添加它们并执行查询计划时没有使用任何其他索引?甚至create index covering on rollup_day(search_keyword, tenant_id, created_date) INCLUDE (users, sessions, total); OR create index covering on rollup_day(search_keyword, tenant_id, created_date, users, sessions, total); OR create index covering on rollup_day(search_keyword, tenant_id, created_date);? 是的,我将与您提到的所有索引共享查询计划。我认为由于 group by 子句,所有索引都不起作用。【参考方案4】:

您应该尝试使用更高的 work_mem 设置,直到您获得内存排序。当然,只有当你的机器有足够的内存时,你才能慷慨地使用内存。

如果您使用物化视图或第二个表和原始表上的触发器来存储预先聚合的数据,从而使您的查询变得更快,则可以使另一个表中的总和保持更新。我不知道您的数据是否可行,因为我不知道 hll_cardinalityhll_union_agg 是什么。

【讨论】:

感谢您的回复。 hll 是 Postgres 对聚合基数的扩展。参考:github.com/citusdata/postgresql-hll 此表已经是一个预聚合表,包含一天的总和,但由于数据集对于预聚合表来说是巨大的。查询效果不佳。 那么除了更多的work_mem进行排序之外,没有任何改进的可能。

以上是关于提高 PostgresSQL 聚合查询性能的主要内容,如果未能解决你的问题,请参考以下文章

聚合查询中的性能更新

提高查询性能mongodb

提高不同的查询性能

PostgresSQL使用Copy命令能大大提高数据导入速度

Flink 流式聚合性能调优指南

Flink 流式聚合性能调优指南