【问题标题】:PostgreSQL performance tuning with table partitions使用表分区进行 PostgreSQL 性能调优
【发布时间】:2018-02-25 22:26:45
【问题描述】:

我正在解决基于 PostgreSQL 9.6 dbo 的系统的性能问题。简介:

12yo系统,类似于银行系统,查询最多的主表叫transactions。

CREATE TABLE jrn.transactions (
     ID BIGSERIAL,
     type_id VARCHAR(200),
     account_id INT NOT NULL,
     date_issued DATE,
     date_accounted DATE,
     amount NUMERIC,
     ..
)

在transactions 表中,我们将所有交易存储在一个银行账户中。字段type_id 确定事务的类型。服务器也作为 C# EntityFramework Discriminator 列。值如下:

card_payment, cash_withdrawl, cash_in, ...

已知 14 种交易类型。

一般来说,有 4 种类型的查询(3 和 .4 是迄今为止最常见的):

  1. 选择单个事务,例如:SELECT * FROM jrn.transactions WHERE id = 3748734

  2. 选择单个事务并加入其他事务,例如:SELECT * FROM jrn.transactions AS m INNER JOIN jrn.transactions AS r ON m.refund_id = r.id WHERE m.id = 3748734

  3. 选择 0-100、100-200、.. 给定类型的事务,例如:SELECT * FROM jrn.transactions WHERE account_id = 43784 AND type_id = 'card_payment' LIMIT 100

  4. 几个聚合查询,例如:SELECT SUM(amount), MIN(date_issued), MAX(date_issued) FROM jrn.transactions WHERE account_id = 3748734 AND date_issued >= '2017-01-01'

在过去的几个月里,我们的行数出现了意想不到的增长,现在达到了 1.2 亿。

我们正在考虑表分区,遵循 PostgreSQL 文档:https://www.postgresql.org/docs/10/static/ddl-partitioning.html

选项:

  1. type_id 将表分区为 14 个分区
  2. 将year 列和year(或year_month)分区表添加到12(或144)个分区中。

我现在正在将数据恢复到测试环境中,我将测试这两个选项。

对于这种情况,您认为最合适的分区规则是什么?还有其他选择吗?

感谢任何反馈/建议等。

【问题讨论】:

  • 如果查询 3 和 4 是最频繁的,我们应该尝试优化它们的分区。他们的WHERE 条件包括三列account_id、type_id 和date_issued。 account_id 的选择性非常高,因此它很可能应该更好地用于索引。 date_issued 与不相等的运算符一起使用,因此分区不会有太大帮助。 type_id 没有选择性,所以可以,但没有在查询 4 ​​中使用。==> 没有明显的解决方案(至少对我来说)。
  • 请注意,您指的是 Postgresql 10 的文档,而您的服务器是 9.6。
  • 在选项 2 上:由于查询 4 ​​不包含 year(或 year_mongth),这也无济于事。无论如何,查询 3 不受 w.r.t 限制。 date_issued,所以这也不是最佳解决方案。
  • 我目前正在考虑物化视图的方向(例如,用于处理 4 号聚合查询)。 @Luke1988:jrn.transactions 的更新/插入频率是多少?查询 4 ​​中 sum(amount) 的准确性有多重要?
  • 是的,因为它们是最重要的外键。结合其中一个日期字段,它们几乎构造了表格的自然键。

标签: postgresql


【解决方案1】:

分区对这些查询没有多大帮助,因为它们不会执行顺序扫描,除非您忘记了索引。

我认为分区的唯一理由是您想有效地删除旧行;那么按日期分区是最好的。

根据您的查询,您应该有这些索引(除了主键索引):

CREATE INDEX ON jrn.transactions (account_id, date_issued);
CREATE INDEX ON jrn.transactions (refund_id);

如果您可以牺牲一些插入性能以使第三个查询尽可能快(您可能想要测试),则以下索引可能是个好主意:

CREATE INDEX ON jrn.transactions (account_id, type_id);

【讨论】:

  • 谢谢!据我了解,由于account_id,分区不会有帮助吗?如果没有 account_id 列(现在只是理论),那么按 type_id 分区会有意义吗?我可以直接将查询 3. 重定向到这个分区 - SELECT * FROM jrn.transactions_cash_withdrawl LIMIT 100
  • 是的,如果您按type_id 分区,它只会扫描那个分区。但是如果你有合适的索引并使用索引扫描,那不会比在大表上便宜多少。分区仅有助于使用顺序扫描和批量删除的计划。
【解决方案2】:

您在这里所拥有的几乎是基于列的存储的完美案例,因为您可以使用SAP HANA Database 获得它。但是,由于您明确要求 Postgres 的答案,而且我怀疑 HANA 数据库是否在预算限制之内,我们将不得不坚持使用 Postgres。

您的两个查询没有。 3 和 4 走向完全不同的方向,因此您的问题不会有“单一答案”——您总是需要在这两个用例之间取得某种平衡。然而,我会尝试使用两种不同的技术来分别处理它们。

在我看来,最大的问题是查询号。 4,这会在您的 postgres 服务器上产生相当高的负载,因为它正在汇总值。此外,您只是一遍又一遍地总结价值,这很可能不会经常改变(甚至根本不会改变),正如您所说,UPDATEs 几乎根本不会发生。我还假设另外两件事:

  • transactions 是 INSERT-only,即 DELETE 语句几乎从不发生(可能在某些特殊行政干预的情况下除外)。
  • 当INSERTing 时,date_issued 列的值通常“接近今天” - 因此您通常不会将 INSERT 填在过去。

除此之外,为了防止不必要地反复聚合值,我将介绍另一个表:我们称之为transactions_aggr,它是这样构建的:

create table transactions_aggr (
   account_id INT NOT NULL,
   date_issued DATE,
   sumamount NUMERIC,
   primary key (account_id, date_issued)
)

这将为您提供每天预先汇总的值的表格。 为了确定哪些值已经预先聚合,我将向transactions 添加另一个布尔类型的列,这向我表明,哪些行包含在transactions_aggr 中,哪些行不包含(还)。查询号然后必须以这样的方式更改 4,使其仅从 transactions 读取非预聚合行,而其余行可能来自 transactions_aggr。为方便起见,您可以定义这样的视图:

select account_id, date_issued, sum(amount) as sumamount from
    (
    select account_id, date_issued, sumamount as amount from transactions_aggr as aggr
    union all
    select account_id, date_issued, sum(amount) as amount from transactions as t where t.aggregated = false
    )
group by account_id, date_issued

不用说,在transactions.aggregated 上添加索引(可能与account_id 一起使用)可以极大地帮助提高这里的性能。

可以使用多种方法更新transactions_aggr:

  1. 您可以将其用作一次性活动,并且只预先聚合当前约 120m 行的集合一次。这至少会显着减少机器上进行聚合的负载。但是,随着时间的推移,您将再次遇到同样的问题。然后您可以重新执行整个过程,只需将transactions_aggr 作为一个整体删除并从头开始重新创建它(所有原始数据仍然存在于transactions 中)。

  2. 你在一周/一个月/晚上的某个地方度过了一段美好的时光,在那里你几乎没有或没有任何查询。然后你可以打开一个交易,阅读所有transactions WHERE aggregated = false并用@添加它们987654342@s 至transactions_aggr。请记住,然后将aggregated 切换到true(应该在同一个事务中完成)。然而,棘手的部分是您必须注意读取查询将“看到”此事务的哪些内容:根据您在此“更新作业”的时间范围内对准确性的要求,您可能必须考虑切换事务隔离级别为“READ_COMMITED”以防止重读。

关于您的问题。 3 然后您可以尝试真正采用基于type_id 的分区方法。但是,我认为您的查询有点奇怪,因为您正在执行 LIMIT/OFFSET 而没有指定(例如,没有指定的 ORDER BY 语句)(注意:您并不是说您会使用数据库游标)。如果对表启用分区,这可能会导致当前使用的隐式顺序发生更改。所以要小心这可能对你的程序造成的副作用。 还有一件事:在真正进行分区拆分之前,我会先通过发出检查type_id的数据分布

select type_id, count(*) from transactions group by type_id

事实并非如此,例如,您 90% 的数据都使用 card_payment - 这样您的分区之间的分布就会非常不均匀,并且最大的性能占用查询是那些仍然会进入这个问题的查询单个“大分区”。

希望这会有所帮助 - 祝你好运!

【讨论】:

  • 感谢您的努力!我运行了那个查询。数据分布几乎相等,大约。每种类型 5% 到 10%。一种用得很少,两种用得比较频繁,但不超过20%。对 transaction_aggregations 的不好想是查询是由应用程序层生成的,目前,对应用程序的任何修改都是有问题的,而不是在我们手中。关于查询顺序:我们使用隐式顺序,因为我们在事务进入系统时列出它们,因此自然主键顺序可以正常工作。
猜你喜欢
  • 2012-09-24
  • 1970-01-01
  • 2010-12-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-02-07
  • 2018-01-29
相关资源
最近更新 更多