提高 Postgres 性能

Posted

技术标签:

【中文标题】提高 Postgres 性能【英文标题】:Improve Postgres performance 【发布时间】:2021-12-18 01:13:43 【问题描述】:

我是 Postgres 的新手,我确信我做错了什么。 所以我只是想知道是否有人经历过与我在下面的经历类似的事情,或者可以为我指出正确的方向来提高 Postgres 的性能。

我最初的目标是通过从 MS SQL Server 迁移到 Postgres 来加速我在各种仪表板中的数据集市的分析处理。 为了获得一个示例查询来比较速度,我在 MS SQL Server 上运行了查询分析器,同时引用了一个 BI 仪表板,它产生了类似的东西(我知道子查询中有冗余列):

SELECT  COUNT(*) 
FROM    (
        SELECT 
            BM.Key_Date, BM.[Actual Date], BM.[Month]
            ,BM.[Month Number], BM.[Month Year], BM.[No of Working Days]
            ,SDI.Key_Delivery, SDI.[Order Number], SDI.[Quantity SKU]
            ,SDI.[Quantity Sales Unit], SDI.[FactSales - GBP], SDI.[NNSA Capsules]
            ,SFI.[Ship-to], SFI.[Sold-to], SFI.[Sales Force Type], SFI.Region
            ,SFI.[Top Level Account], SFI.[Customer Organisation]
            ,EX.Rate
            ,PDI.[Product Description], PDI.[Product Type Group], PDI.[Product Type],
             PDI.[Main Product Categories], PDI.Section, PDI.Family
        FROM Fact.SalesDataInvoiced                     AS SDI
            JOIN Dimension.SalesforceInvoiced           AS SFI
                ON SDI.[Key_Ship-to]=SFI.[Key_Ship-to]
            JOIN Dimension.BillingMonth                 AS BM
                ON SDI.[Key_Billing Month]=BM.Key_Date
            JOIN Dimension.ProductDataInvoiced          AS PDI
                ON SDI.[Key_Product Code]=PDI.[Key_Product Code]
            CROSS JOIN Dimension.Exchange               AS EX
        WHERE BM.[Actual Date] BETWEEN '20160101' AND '20211001'
    ) AS a 
GROUP BY [Product Type], [Product Type Group],[Main Product Categories]

然后我在不同的相同笔记本电脑上安装了 Postgres 14(在 Centos 8 上)和 MS SQL Server Developer 2017(在 Windows 10 上),并从相同的 csv 数据文件创建了一个数据库和表,以启用上述查询的复制。

使用索引运行 Postgres 查询的速度比不使用索引的 MS SQL 慢很多。 向 MS SQL 添加索引几乎可以立即产生结果。

由于处理时间的差异,我什至安装了带有 Postgres14 的 Citus,并将 Fact.SalesDataInvoiced 创建为柱状表(这使处理时间变得更糟)。 我在 postgresql.conf 中使用过内存设置,但似乎没有任何东西可以实现与 MSSQL 相媲美的速度。

解释分析表明,尽管有索引,但它总是对所有表进行顺序扫描。强制索引扫描对处理时间没有任何影响。

我认为 Postgres 使用集群和分区会表现得更好吗?即使是这种情况,像我试图在单机上运行的简单查询肯定会更快?

表格详情

Dimension.BillingMonth 记录 120, 主键是 KeyDate, KeyDate 上的聚集唯一索引

Dimension.Exchange 记录 1

Dimension.ProductDataInvoiced 记录 275563, 主键是 KeyProduct, KeyProduct 上的聚集唯一索引

Dimension.SalesforceInvoiced 记录 377414, 主键是 KeyShipTo, KeyShipTo 上的聚集唯一索引

Fact.SalesDataInvoiced 记录 43807943, KeyShipTo、KeyProduct、KeyBillingMonth 上的非聚集唯一索引

如前所述,任何帮助将不胜感激,我确定我一定遗漏了一些明显的东西。

非常感谢。

大卫


感谢您的回复。我在下面放置了其他信息。

忘记添加我的 postgres 性能问题是在我执行完全真空和重新索引之后。在导入数据并创建索引后,我执行了这些维护任务。

查询 pg_indexes 后的输出

tablename indexname indexdef
BillingMonth BillingMonth_pkey CREATE UNIQUE INDEX BillingMonth_pkey ON public.BillingMonth USING btree (KeyDate)
ProductDataInvoiced ProductDataInvoiced_pkey CREATE UNIQUE INDEX ProductDataInvoiced_pkey ON public.ProductDataInvoiced USING btree (KeyProductCode)
SalesforceInvoiced SalesforceInvoiced_pkey CREATE UNIQUE INDEX SalesforceInvoiced_pkey ON public.SalesforceInvoiced USING btree (KeyShipTo)
SalesDataInvoiced CI_SalesData CREATE INDEX CI_SalesData ON public.SalesDataInvoiced USING btree (KeyShipTo, KeyProductCode, KeyBillingMonth)

运行 EXPLAIN (ANALYZE, BUFFERS) 后的输出

Finalize GroupAggregate  (cost=1435439.30..1435565.71 rows=480 width=53) (actual time=25960.468..25973.326 rows=31 loops=1)
   Group Key: pdi."ProductType", pdi."ProductTypeGroup", pdi."MainProductCategories"
   Buffers: shared hit=71246 read=859119
   ->  Gather Merge  (cost=1435439.30..1435551.31 rows=960 width=53) (actual time=25960.458..25973.282 rows=89 loops=1)
     Workers Planned: 2
     Workers Launched: 2
     Buffers: shared hit=71246 read=859119
     ->  Sort  (cost=1434439.28..1434440.48 rows=480 width=53) (actual time=25956.982..25956.989 rows=30 loops=3)
           Sort Key: pdi."ProductType", pdi."ProductTypeGroup", pdi."MainProductCategories"
           Sort Method: quicksort  Memory: 28kB
           Buffers: shared hit=71246 read=859119
           Worker 0:  Sort Method: quicksort  Memory: 29kB
           Worker 1:  Sort Method: quicksort  Memory: 29kB
           ->  Partial HashAggregate  (cost=1434413.10..1434417.90 rows=480 width=53) (actual time=25956.878..25956.895 rows=30 loops=3)
                 Group Key: pdi."ProductType", pdi."ProductTypeGroup", pdi."MainProductCategories"
                 Batches: 1  Memory Usage: 49kB
                 Buffers: shared hit=71230 read=859119
                 Worker 0:  Batches: 1  Memory Usage: 49kB
                 Worker 1:  Batches: 1  Memory Usage: 49kB
                 ->  Parallel Hash Join  (cost=62124.74..1327935.46 rows=10647764 width=45) (actual time=285.864..19240.004 rows=14602648 loops=3)
                       Hash Cond: (sdi."KeyShipTo" = sfi."KeyShipTo")
                       Buffers: shared hit=71230 read=859119
                       ->  Hash Join  (cost=19648.48..1257508.51 rows=10647764 width=49) (actual time=204.794..12862.063 rows=14602648 loops=3)
                             Hash Cond: (sdi."KeyProductCode" = pdi."KeyProductCode")
                             Buffers: shared hit=32264 read=859119
                             ->  Hash Join  (cost=3.67..1091456.95 rows=10647764 width=8) (actual time=0.143..7076.104 rows=14602648 loops=3)
                                   Hash Cond: (sdi."KeyBillingMonth" = bm."KeyDate")
                                   Buffers: shared hit=197 read=859119
                                   ->  Parallel Seq Scan on "SalesData_Invoiced" sdi  (cost=0.00..1041846.10 rows=18253310 width=12) (actual 
time=0.071..2585.596 rows=14602648 loops=3)
                                         Buffers: shared hit=194 read=859119
                                   ->  Hash  (cost=2.80..2.80 rows=70 width=4) (actual time=0.049..0.050 rows=70 loops=3)
                                   Hash Cond: (sdi."KeyBillingMonth" = bm."KeyDate")
                                   Buffers: shared hit=197 read=859119
                                   ->  Parallel Seq Scan on "SalesData_Invoiced" sdi  (cost=0.00..1041846.10 rows=18253310 width=12) (actual 
time=0.071..2585.596 rows=14602648 loops=3)
                                         Buffers: shared hit=194 read=859119
                                   ->  Hash  (cost=2.80..2.80 rows=70 width=4) (actual time=0.049..0.050 rows=70 loops=3)
                                         Buckets: 1024  Batches: 1  Memory Usage: 11kB
                                         Buffers: shared hit=3
                                         ->  Seq Scan on "BillingMonth" bm  (cost=0.00..2.80 rows=70 width=4) (actual time=0.012..0.028 
rows=70 loops=3)
                                               Filter: (("ActualDate" >= '2016-01-01'::date) AND ("ActualDate" <= '2021-10-01'::date))
                                               Rows Removed by Filter: 50
                                               Buffers: shared hit=3
                             ->  Hash  (cost=16200.27..16200.27 rows=275563 width=49) (actual time=203.237..203.238 rows=275563 loops=3)
                                   Buckets: 524288  Batches: 1  Memory Usage: 26832kB
                                   Buffers: shared hit=32067
                                   ->  Nested Loop  (cost=0.00..16200.27 rows=275563 width=49) (actual time=0.034..104.143 rows=275563 loops=3)
                                         Buffers: shared hit=32067
                                         ->  Seq Scan on "Exchange" ex  (cost=0.00..1.01 rows=1 width=0) (actual time=0.024..0.024 rows=
1 loops=3)
                                               Buffers: shared hit=3
                                         ->  Seq Scan on "ProductData_Invoiced" pdi  (cost=0.00..13443.63 rows=275563 width=49) (actual 
time=0.007..48.176 rows=275563 loops=3)
                                               Buffers: shared hit=32064
                       ->  Parallel Hash  (cost=40510.56..40510.56 rows=157256 width=4) (actual time=79.536..79.536 rows=125805 loops=3)
                             Buckets: 524288  Batches: 1  Memory Usage: 18912kB
                             Buffers: shared hit=38938
                             ->  Parallel Seq Scan on "Salesforce_Invoiced" sfi  (cost=0.00..40510.56 rows=157256 width=4) (actual time=
0.011..42.968 rows=125805 loops=3)
                                   Buffers: shared hit=38938
 Planning:
   Buffers: shared hit=426
 Planning Time: 1.936 ms
 Execution Time: 25973.709 ms
(55 rows)

【问题讨论】:

您是否在运行查询之前运行了VACUUM ANALYZE(在首次数据导入之后或创建新索引之后)?你的 PostgreSQL 索引是什么? 1) 主键 1a) 自然键 2) 外键 2a) 支持 FK 的索引 3) 可能是特定查询的一些附加索引 4) 服务器配置常量。 检查查询的EXPLAIN (ANALYZE, BUFFERS) 输出。 感谢您的回复。我现在在我的帖子中添加了其他信息 【参考方案1】:

首先,请记住在重建索引后运行VACUUM ANALYZE,或者有时在导入大量数据后运行。 (VACUUM FULL主要用于操作系统回收磁盘空间,后期还需要分析,尤其是重建索引后。)

从您的查询看来,您的主表是 SalesDataInvoiced (SDI),并且如果可能的话,您希望在 KeyBillingMonth 上使用索引(因为这是您放置的主要限制)。通常,您还需要索引,至少在用于连接的列上的其他表上。

正如multi-column indexes in PostgreSQL 的文档所说:

多列 B 树索引可用于涉及索引列的任何子集的查询条件,但当前导(最左侧)列存在约束时,索引效率最高。确切的规则是前导列上的等式约束,加上没有等式约束的第一列上的任何不等式约束,将用于限制扫描的索引部分。在索引中检查这些列右侧的列的约束,因此它们可以正确保存对表的访问,但不会减少必须扫描的索引部分。例如,给定 (a, b, c) 上的索引和查询条件 WHERE a = 5 AND b >= 42 AND c = 77 的索引条目将被跳过,但仍然必须扫描它们。 这个索引原则上可以用于对 b 和/或 c 有约束而对 a 没有约束的查询——但是必须扫描整个索引,所以在大多数情况下,规划器更喜欢顺序表扫描使用索引。

在您的示例中,您要在 (KeyBillingMonth) 上使用约束的主列位于第三位,因此不太可能使用。

CREATE INDEX CI_SalesData ON public.SalesDataInvoiced
   USING btree (KeyShipTo, KeyProductCode, KeyBillingMonth)

创建它应该使它更有可能被使用:

CREATE INDEX ON SalesDataInvoiced(KeyBillingMonth);

然后,运行 VACUUM ANALYZE 并再次尝试查询。

您可能还希望在BillingMonth(ActualDate) 上建立索引,但这并不一定有用,因为似乎行数很少(而且大多数行都在您的查询中返回)。

尚不清楚BillingMonth 表的用途。如果它基本上是关于将ActualDate 截断为每月的第一天,例如,您可以摆脱BillingMonth 上的连接并直接使用SalesDataInvoiced.KeyBillingMonth 上的约束。例如... WHERE SDI.KeyBillingMonth BETWEEN '2016-01-01' AND '2021-10-01' ...

作为旁注,据我所知,BETWEEN 包含其上限。我想这样的查询是为了代表一些月度统计数据,因此可能不应该包括 2021 年 10 月 1 日(但不包括该月剩余时间)的内容。

【讨论】:

谢谢布鲁诺,我有一些好的地方可以跟进。我的初始查询只是与仪表板交互时从 Power BI 触发的众多查询之一。 BillingMonth 表是我的 Dates 表的缩减版本,包含一个月中的工作日数等信息。该表的主键是 KeyDate,它是一个整数值。 ActualDate 保存为日期,用于 Power BI 中的日期计算。 嗨 Bruno,我已经尝试了为 BillingMonth 表(将 ActualDate 添加为索引)和 SalesData_Invoiced 表建议的索引(首先尝试仅在 BillingMonth 上建立索引,然后按照 KeyBillingMonth 的顺序创建非聚集索引, KeyProductCode、KeyShipTo)。不幸的是,这些更改都没有提高查询速度。 VACUUM ANALYZE 在为每次更改重新索引后运行。如果您有任何其他想法,我非常愿意尝试一下:) 您似乎在谈论“集群/非集群”索引。 AFAIK,这在 SQL Server 和 PostgreSQL 中没有相同的含义。 (在 PostgreSQL 中,主索引不能确定数据的页面位置。)我不确定您是否真的使用过“CLUSTERED”,但我一开始会避免使用。我会尝试简化查询,首先是摆脱 CROSS JOIN (这是无用的),并且可能一次添加一个连接以查看何时使用索引而不使用索引。也许尝试使用已知的 KeyBillingMonth 值(可能只是一个或“IN”条件),看看它是否会改变行为。

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

提高 RODBC-Postgres 写入性能

结合关系查询提高 Postgres jsonb 查询的性能

postgres 截断很慢

Postgres 8.3 中的位图扫描比索引扫描 Postgres 9.4 快 2 倍?

如何提高我的 Postgres 选择语句的速度?

提高查询速度:大 postgres 表中的简单 SELECT