PostgreSQL 13 - 改进大表数据聚合

Posted

技术标签:

【中文标题】PostgreSQL 13 - 改进大表数据聚合【英文标题】:PostgreSQL 13 - Improve huge table data aggregation 【发布时间】:2021-04-26 09:36:34 【问题描述】:

我有一个由Year_month 分区和currency 子分区的巨大数据库(当前大小约为900GB,并且仍然有新数据)。 问题是当我尝试从整个分区中获取聚合时,它会变慢。这是一份报告,因此会经常被查询。 我要聚合的当前分区大小:7.829.230 行。每个子分区都是相似的。 表架构(匿名):

CREATE TABLE aggregates_dates
(
    id                    char(1)            DEFAULT '' NOT NULL,
    date                  TIMESTAMP(0)                               NOT NULL,
    currency              CHAR(3)                                    NOT NULL,
    field01               INTEGER                                    NOT NULL,
    field02               INTEGER                                    NOT NULL,
    field03               INTEGER                                    NOT NULL,
    field04               INTEGER                                    NOT NULL,
    field05               INTEGER                                    NOT NULL,
    field06               CHAR(2)                                    NOT NULL,
    field07               INTEGER         DEFAULT 0                  NOT NULL,
    field08               INTEGER         DEFAULT 0                  NOT NULL,
    field09               INTEGER         DEFAULT 0                  NOT NULL,
    field10               INTEGER         DEFAULT 0                  NOT NULL,
    field11               INTEGER         DEFAULT 0                  NOT NULL,
    value01               INTEGER         DEFAULT 0                  NOT NULL,
    value02               INTEGER         DEFAULT 0                  NOT NULL,
    value03               INTEGER         DEFAULT 0                  NOT NULL,
    value04               NUMERIC(24, 12) DEFAULT '0'::NUMERIC       NOT NULL,
    value05               NUMERIC(24, 12) DEFAULT '0'::NUMERIC       NOT NULL,
    value06               INTEGER         DEFAULT 0                  NOT NULL,
    value07               NUMERIC(24, 12) DEFAULT '0'::NUMERIC       NOT NULL,
    value08               NUMERIC(24, 12) DEFAULT '0'::NUMERIC       NOT NULL,
    value09               INTEGER         DEFAULT 0                  NOT NULL,
    value10               NUMERIC(24, 12) DEFAULT '0'::NUMERIC       NOT NULL,
    value11               NUMERIC(24, 12) DEFAULT '0'::NUMERIC       NOT NULL,
    value12               INTEGER         DEFAULT 0                  NOT NULL,
    value13               NUMERIC(24, 12) DEFAULT '0'::NUMERIC       NOT NULL,
    value14               NUMERIC(24, 12) DEFAULT '0'::NUMERIC       NOT NULL,
    value15               INTEGER         DEFAULT 0                  NOT NULL,
    value16               NUMERIC(24, 12) DEFAULT '0'::NUMERIC       NOT NULL,
    value17               NUMERIC(24, 12) DEFAULT '0'::NUMERIC       NOT NULL,
    value18               NUMERIC(24, 12) DEFAULT '0'::NUMERIC       NOT NULL,
    value19               INTEGER         DEFAULT 0,
    value20               INTEGER         DEFAULT 0,
    CONSTRAINT aggregates_dates_pkey
        PRIMARY KEY (id, date, currency)
)
    PARTITION BY RANGE (date);
CREATE TABLE aggregates_dates_2020_01
    PARTITION OF aggregates_dates
        FOR VALUES FROM ('2020-01-01 00:00:00') TO ('2020-01-31 23:59:59')
    PARTITION BY LIST (currency);
CREATE TABLE aggregates_dates_2020_01_eur
    PARTITION OF aggregates_dates_2020_01
        FOR VALUES IN ('EUR');
CREATE INDEX aggregates_dates_2020_01_eur_date_idx ON aggregates_dates_2020_01_eur (date);
CREATE INDEX aggregates_dates_2020_01_eur_field01_idx ON aggregates_dates_2020_01_eur (field01);
CREATE INDEX aggregates_dates_2020_01_eur_field02_idx ON aggregates_dates_2020_01_eur (field02);
CREATE INDEX aggregates_dates_2020_01_eur_field03_idx ON aggregates_dates_2020_01_eur (field03);
CREATE INDEX aggregates_dates_2020_01_eur_field04_idx ON aggregates_dates_2020_01_eur (field04);
CREATE INDEX aggregates_dates_2020_01_eur_field06_idx ON aggregates_dates_2020_01_eur (field06);
CREATE INDEX aggregates_dates_2020_01_eur_currency_idx ON aggregates_dates_2020_01_eur (currency);
CREATE INDEX aggregates_dates_2020_01_eur_field09_idx ON aggregates_dates_2020_01_eur (field09);
CREATE INDEX aggregates_dates_2020_01_eur_field10_idx ON aggregates_dates_2020_01_eur (field10);
CREATE INDEX aggregates_dates_2020_01_eur_field11_idx ON aggregates_dates_2020_01_eur (field11);
CREATE INDEX aggregates_dates_2020_01_eur_field05_idx ON aggregates_dates_2020_01_eur (field05);
CREATE INDEX aggregates_dates_2020_01_eur_field07_idx ON aggregates_dates_2020_01_eur (field07);
CREATE INDEX aggregates_dates_2020_01_eur_field08_idx ON aggregates_dates_2020_01_eur (field08);

聚合整个分区的示例查询(未使用所有字段)(此查询可能有更多 WHERE 条件,但这是最坏的情况)

EXPLAIN (ANALYSE, BUFFERS, VERBOSE) SELECT
       COALESCE(SUM(mainTable.value01), 0)            AS                                    "value01",
       COALESCE(SUM(mainTable.value02), 0)       AS                                    "value02",
       COALESCE(SUM(mainTable.value03), 0)       AS                                    "value03",
       COALESCE(SUM(mainTable.value06), 0)       AS                                    "value06",
       COALESCE(SUM(mainTable.value09), 0)    AS                                    "value09",
       COALESCE(SUM(mainTable.value12), 0)      AS                                    "value12",
       COALESCE(SUM(mainTable.value15), 0) AS                                    "value15",
       COALESCE(SUM(mainTable.value03 + mainTable.value06 + mainTable.value09 + mainTable.value12 +
                    mainTable.value15), 0) AS                                    "kpi01",
       COALESCE(SUM(mainTable.value05) * 1, 0)                                         "value05",
       COALESCE(SUM(mainTable.value08) * 1, 0)                                         "value08",
       COALESCE(SUM(mainTable.value11) * 1, 0)                                      "value11",
       COALESCE(SUM(mainTable.value14) * 1, 0)                                        "value14",
       COALESCE(SUM(mainTable.value17) * 1, 0)                                   "value17",
       COALESCE(SUM(mainTable.value05 + mainTable.value08 + mainTable.value11 + mainTable.value14 +
                    mainTable.value17) * 1, 0)                                   "kpi02",
       CASE
           WHEN SUM(mainTable.value02) > 0 THEN (1.0 * SUM(
                       mainTable.value05 + mainTable.value08 + mainTable.value11 +
                       mainTable.value14 + mainTable.value17) / SUM(mainTable.value02) * 1000 * 1)
           ELSE 0 END                                                                      "kpiEpm",
       CASE
           WHEN SUM(mainTable.value01) > 0 THEN (1.0 * SUM(
                       mainTable.value05 + mainTable.value08 + mainTable.value11 +
                       mainTable.value14) / SUM(mainTable.value01) * 1)
           ELSE 0 END
FROM aggregates_dates mainTable
WHERE (mainTable.date BETWEEN '2020-01-01 00:00:00' AND '2020-02-01 00:00:00')
  AND (mainTable.currency = 'EUR')
GROUP BY mainTable.field02;

解释:

+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|QUERY PLAN                                                                                                                                                                          |
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|HashAggregate  (cost=3748444.51..3748502.07 rows=794 width=324) (actual time=10339.771..10340.497 rows=438 loops=1)                                                                 |
|  Group Key: maintable.field02                                                                                                                                                      |
|  Batches: 1  Memory Usage: 1065kB                                                                                                                                                  |
|  Buffers: shared hit=2445343                                                                                                                                                       |
|  ->  Append  (cost=0.00..2706608.65 rows=11575954 width=47) (actual time=212.934..4549.921 rows=7829230 loops=1)                                                                   |
|        Buffers: shared hit=2445343                                                                                                                                                 |
|        ->  Seq Scan on aggregates_2020_01 maintable_1  (cost=0.00..2646928.38 rows=11570479 width=47) (actual time=212.933..4055.104 rows=7823923 loops=1)                        |
|              Filter: ((date >= '2020-01-01 00:00:00'::timestamp without time zone) AND (date <= '2020-02-01 00:00:00'::timestamp without time zone) AND (currency = 'EUR'::bpchar))|
|              Buffers: shared hit=2444445                                                                                                                                           |
|        ->  Index Scan using aggregates_2020_02_date_idx on aggregates_2020_02 maintable_2  (cost=0.56..1800.50 rows=5475 width=47) (actual time=0.036..6.476 rows=5307 loops=1)  |
|              Index Cond: ((date >= '2020-01-01 00:00:00'::timestamp without time zone) AND (date <= '2020-02-01 00:00:00'::timestamp without time zone))                           |
|              Filter: (currency = 'EUR'::bpchar)                                                                                                                                    |
|              Rows Removed by Filter: 31842                                                                                                                                         |
|              Buffers: shared hit=898                                                                                                                                               |
|Planning Time: 0.740 ms                                                                                                                                                             |
|JIT:                                                                                                                                                                                |
|  Functions: 15                                                                                                                                                                     |
|  Options: Inlining true, Optimization true, Expressions true, Deforming true                                                                                                       |
|  Timing: Generation 4.954 ms, Inlining 14.249 ms, Optimization 121.115 ms, Emission 77.181 ms, Total 217.498 ms                                                                    |
|Execution Time: 10345.662 ms                                                                                                                                                        |
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

服务器规格:

AMD 64 线程 315GB 内存 6xSSD RAID 10 Postgres 配置:
postgresql_autovacuum_vacuum_scale_factor: 0.4
postgresql_checkpoint_completion_target: 0.9
postgresql_checkpoint_timeout: 10min
postgresql_effective_cache_size: 240GB
postgresql_maintenance_work_mem: 2GB
postgresql_random_page_cost: 1.0
postgresql_shared_buffers: 80GB
postgresql_synchronous_commit: local
postgresql_work_mem: 1GB

[2021-04-27 更新]

我已经更新了服务器配置:

postgresql_max_worker_processes: 64
postgresql_max_parallel_workers_per_gather: 32
postgresql_max_parallel_workers: 64
postgresql_max_parallel_maintenance_workers: 4

对于整个查询,我有一个关于生产数据的示例(更长 - 所有表字段的聚合)不能更快地工作并且不使用并行(到大选择语句?)。但是当我减少 SELECT 上的聚合数量时,它开始使用并行并提高了战利品性能。但是当我将查询恢复为原始查询时,它不使用并行。

【问题讨论】:

数以千万计的行不会使表变得“巨大”……只要有适当的索引,任何 SQL 服务器都可以合理有效地过滤和聚合它们。几亿?这是巨大的。 您的设置代码引发了多个错误。然后,您查询与刚刚创建的表不同的表。然后您显示的执行计划既不匹配设置代码,也不匹配查询本身。如果你想匿名,那很好。但是你必须正确地做,否则我们将不知道你真正想要做什么。 @jjanes 我修复了模式创建和选择。分析来自带有数据的真实数据库。 【参考方案1】:

您需要performance(currency, date, field02) 上的复合 BTREE 索引来帮助有效地满足此特定查询。

您的查询按currency 上的相等性、date 上的范围过滤,然后按field02 分组。所以 postgreSQL 可以随机访问这个索引到第一个符合条件的行,然后按顺序扫描它以获得日期范围并进行分组。

专业提示date BETWEEN '2020-01-01 00:00:00' AND '2020-02-01 00:00:00' 表示

     date >= '2020-01-01 00:00:00'  AND date <= '2020-02-01 00:00:00'

我想你可能想要(注意 &lt; 代替 &lt;=

     date >= '2020-01-01 00:00:00'  AND date < '2020-02-01 00:00:00'

专业提示:避免创建大量单列索引,除非您知道需要它们来满足查询或强制执行唯一性约束(如主键)。相反,创建满足查询所需的复合索引。索引显示了 INSERT 和 UPDATE 操作,所以如果它们不能帮助 SELECT 操作,它们比无用更糟糕。如果你有不同的 WHERE 过滤器,你可能需要不同的索引来满足你的查询。)postgreSQL 对这些所谓的covering indexes 有很好的解释。

(您的服务器很好。一旦您的索引设置正确,您可能会发现它被过度配置,并且您的成本超出了它的需要。)

【讨论】:

关于 Btree。查询只是众多示例中的一个。可能有更多的条件和更多的组,因此为所有情况创建索引有点困难:) ProTip1:是的,我知道它的行为方式。日期只是一个示例,范围可能更宽,例如耳朵的一半、一年等。 ProTip2:我知道索引是如何工作的。我们对插入器没有问题,我们对 Select 有问题。【参考方案2】:

这是一份报告,因此会经常被查询。

这个说法令人困惑。事务处理查询通常运行得非常频繁。另一方面,报告通常很少运行 - 一年一次,一个月一次,一天一次,也许轮班一次。

7,829,230 是要聚合的很多行,尤其是在计算这么多不同的聚合时。并行查询应该能够加快速度。在我手中确实如此。很难看出为什么它在 v13 中对您不起作用,除非您以未显示的方式更改了配置以阻止它工作。并行查询一般都有效吗?

您的表实际上似乎并未遵循您布置的分区方案,因为实际计划中涉及的两个表都没有将货币名称作为表名的一部分。此外,查询和分区之间的时间边界不匹配,因为 2020-02-01 00:00:00 的确切时刻与满足查询的所有其余时间位于不同的分区中。但这没关系,因为不需要太多时间来拉入那些额外的行,而且聚合本身需要大部分时间,并且 seq 扫描显然没有丢弃任何基于过滤器的行,所以有没有什么可以优化的。

如果您真的想在这里大幅提高性能,您可以做的就是更改每日分区,然后预先计算每日聚合。然后,您的查询必须仅聚合最近一天的盘中数据,并将其与前几天的预计算聚合相结合。但是,您必须手动构建此类查询。除非缩放/分片扩展/分叉之一可以为您做到这一点。

【讨论】:

以上是关于PostgreSQL 13 - 改进大表数据聚合的主要内容,如果未能解决你的问题,请参考以下文章

如何管理跨多个表的大型数据集? UNION 与大表?

Debian 10 | Debian 9系统安装PostgreSQL 13详细过程

如何对 hsqldb 中的大表执行高效的 group by / sum 聚合?

PostgreSQL大表的更新时间

带有大表的 Geoserver WFS + PostgreSQL 速度极慢

如何根据多列的顺序对 PostgreSQL 中的聚合进行分组?