最佳实践:优化Postgres查询性能(上)

Posted PostgreSQLChina

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了最佳实践:优化Postgres查询性能(上)相关的知识,希望对你有一定的参考价值。

在过去的五年里,我们了解了很多关于如何优化Postgres性能的知识。在这本电子书中,我们写下了如何最大限度地利用你的数据库的关键体会。

你是否曾收到过你的团队提出的问题:为什么你们的产品应用运行缓慢?很可能你收到过。但是,你是否考虑过实际上是你的数据库造成了这个问题?

根据我们的经验:

数据库性能 = 应用性能

本书目标:

在这本电子书中,我们将带领你让你的Postgres数据库获得3倍性能改进以及减少500倍加载磁盘数据的过程。

01  数据库性能 = 应用性能

通常,应用程序的性能是由底层数据库及其配置决定的,因为许多应用程序及其ORMs(对象-关系映射)并不知道运行并隐藏在ORM调用后面的SQL。

例如,在“Ruby on Rails”框架中,你可能会在应用程序代码中看到类似下面的内容:

1 BackendWaitEvent.where(backend_id:
2 user.backends.first).pluck(:wait_event)

但后来才意识到它生成的SQL更像这样

 1 SELECT “backends”.* FROM “backends” INNER JOIN “servers” ON
 2 “backends”.”server_id” = “servers”.”id” INNER JOIN “organizations” ON
 3 “servers”.”organization_id” = “organizations”.”organization_id” INNER
 4 JOIN “organization_memberships” ON “organizations”.”organization_id” =
 5 “organization_memberships”.”organization_id” WHERE
 6 “organization_memberships”.”user_id” = $1 AND
 7 “organization_memberships”.”accepted” = $2 ORDER BY
 8 “backends”.”backend_id” ASC LIMIT $3;
 9 SELECT “backend_wait_events”.”wait_event” FROM “backend_wait_events”
10 WHERE “backend_wait_events”.”backend_id” = $1;

正如我们所看到的,这些SQL语句必须做一些工作才能真正找到我们要寻找的数据。然而,对于使用ORM的应用程序开发人员来说,这看起来就像一个简单的函数调用,有时会有很高的延迟。

我们可以很容易地意识到:数据库性能 = 应用性能

02  缺少索引是数据库性能的首要问题

根据我们的经历,大多数开发团队在推动功能变更时不会验证运行的所有新SQL。

特别是在使用各种ORM的情况下,在评估一个添加新功能的有影响的请求时,很难确切地知道执行了哪些SQL语句。只有在使用分阶段系统上的真实数据测试功能时,或者当你可以看到在生产环境中运行的并发查询的效果时,你才能真正了解数据库端发生了什么变化。

导致数据库性能问题的最常见错误是开发人员忘记添加索引,通常即使一个功能发布到生产环境中,你也不会注意到缺少了索引。

在几周或几个月的时间里,直到该功能得到足够的运用或足够的底层数据,它才成为性能瓶颈。

你是否知道?导致数据库性能问题的最常见错误是开发人员忘记添加索引。

03  搞清楚你的数据库正在做什么

让我们假设我们想要查明4/13 8:42:23你的PostgreSQL数据库中当前是否有任何缓慢的查询和缺失的索引。我们该怎么做呢?

3.1 评估 PostgreSQL 的查询性能

实现这一点的一个重要工具是Postgres中的扩展 pg_stat_statements。它是捆绑在contrib包的一部分,所以你可以很容易地把它安装到你的数据库服务端上一一如果你使用的是一个托管的数据库即服务(DBaaS,database-as-a-service),比如Heroku Postgres,它也可能已经被启用了。 

要检查是否启用“pg_stat_statements”,以及如何安装它,你可以参考我们文档中的这个指南。

DBaaS(DataBase-as-a-Service),数据库即服务:
将数据库以云服务模式交付给用户,就是数据库即服务——DBaaS,也称云数据库。
DBaaS(亦称泛数据库类服务)就是PaaS 层的一个重要分支。
其他内容参考链接:https://www.modb.pro/db/109150

3.2 使用 pg_stat_statements 查找开销大的查询

使用 pg_stat_statements 查找开销大的查询

下面是一个标准的查询,你可以在你的数据库上运行,用来从pg_stat_statements获取查询统计数据:

SELECT queryid, calls, mean_time, substring(query for 100)
FROM pg_stat_statements
ORDER BY total_time DESC
LIMIT 10;

这将给我们一个如下所示的清单,在顶上是开销最大的查询:

  queryid   |  calls |     mean_time    | substring
-------------+--------+------------------+------------------------------------
   823659002 | 100856 |  212.20739523876 | SELECT “backend_wait_events” ...
  1908568318 |    224 | 392311.585268714 | COPY public.queries ...
  2996059654 |  59056 | 718.891097988979 | UPDATE “backends” ...
   107459272 |    223 | 189880.905045996 | COPY public.query_explains ...
  1819695266 |    223 |  119756.64852817 | COPY public.query_samples ...
  1615643520 |    224 |  90714.414896558 | COPY public.backend_wait_events ...
  3088208845 | 134836 | 87.2854475040199 | COPY “backend_wait_events” ...
   411003829 |   7103 |  983.00906357286 | UPDATE “backends” ...
   429818704 |    211 | 28399.7321560284 | COPY public.snapshot_benchmarks ...
  3773426307 |    224 | 19193.0874573839 | COPY public.backend_queries ...
(10 rows)

需要注意的一点是,pg_stat_statements记录了从它安装开始的统计信息,或者从你最后一次重置统计信息开始的统计信息。

重要提示:
当使用pg_stat_statements而不使用pganalyze这样的监控产品时,可以使用函数pg_stat_statements_reset()来重置统计信息。

3.3 特定慢查询的性能分析

让我们看看上面的查询输出。我们如何去理解为什么以SELECT“backend_wait_events” 开头的查询是慢的?

首先,让我们通过字段“queryid”来查询获取存储在pg_stat_statements中的完整查询文本:

  => SELECT query FROM pg_stat_statements WHERE queryid = 823659002;

                                query
--------------------------------------------------------------------
 SELECT “backend_wait_events”.”wait_event” FROM “backend_wait_events”
 WHERE “backend_wait_events”.”backend_id” = $1
(1 row)

在这里,我们可以看到,pg_stat_statements记录的不是查询的特定调用,而是查询的聚合、规范化形式。类似基于queryid一起分组的查询,同时文本得到规范化,因为如果在原始SQL中包含“backend_id = 'something'”,它是以“backend_id = $1”替代存储。

这主要是为了用户的利益,但缺点是我们不能在这些查询文本上运行EXPLAIN:

=> EXPLAIN SELECT “backend_wait_events”.”wait_event” FROM backend_wait_events
WHERE backend_id = $1;
ERROR: there is no parameter $1
LINE 1: ...”wait_event” FROM backend_wait_events WHERE backend_id = $1;

这当然是讲得通的,因为Postgres执行计划依赖于你正在查询的特定值——为了运行EXPLAIN,我们需要知道$1的值。

POSTGRES执行计划:

EXPLAIN通过显示Postgres如何执行查询来确定该查询的执行计划,例如,让你知道它是进行索引扫描(通常是划算的)还是顺序扫描(通常很慢,除非在非常小的表上)。 

现在,我们刚好知道backend_id的值,然后我们自己替换它,让我们运行这个EXPLAIN:  

 => EXPLAIN SELECT “backend_wait_events”.”wait_event” FROM backend_wait_events
WHERE backend_id = ‘d95d627c-bea7-4c7e-bea5-4f69e18fe53a’;

                                 QUERY PLAN
---------------------------------------------------------------------------
 Seq Scan on backend_wait_events (cost=0.00..168374.85 rows=268 width=14)
   Filter: (backend_id = ‘d95d627c-bea7-4c7e-bea5-4f69e18fe53a’::uuid)
 JIT:
   Functions: 4
   Inlining: false
   Optimization: false
(6 rows)

但通常,我们不知道这些参数的值,这就导致了下一个问题:

如何确定pg_stat_statements中查询的绑定参数值。

3.4 为慢查询查找绑定参数值

为了获得完整的查询文本,我们有两个选择:首先,我们可以利用Postgres的表“pg_stat_activity”,它显示了当前运行的查询。如果你在自己的应用程序代码中不使用参数(即:你在查询文本本身中发送所有参数值),这将起作用,但需要一些额外的工作,即频繁对该表采样。

作为一种更通用的方法,它既能处理pg_stat_statements的绑定参数值,也处理应用程序单独发送的那些参数值,我们转向Postgres日志记录系统。

3.5 了解Postgres日志记录系统

Postgres生成大量的日志事件,并且需要花费大量的精力来检查和解析日志文件。然而,对于这个特定的示例,我们只查看单个日志事件,即由log_min_duration_statement控制的慢查询日志输出。

**日志**

log_min_duration_statement vs log_statement:
对于那些熟悉Postgres配置选项的人,您可能想知道为什么我们推荐使用log_min_duration_statement而不是log_statement。虽然你可以使用log_statement = all来获取每个已运行语句的完整查询文本,但这在生产环境中很少有意义,因为它可能会导致生产系统崩溃,因为在非常快的查询中,日志输出的开销很大。因此,我们建议在生产系统上只使用log_min_duration_statement。

我们可以将log_min_duration_statement设置为一个特定的阈值,任何运行时间超过该持续时间的SQL查询都会将完整的查询文本记录到Postgres日志文件中。通常,从1000毫秒这样的阈值开始是有意义的,如果需要,可以稍低一些,因为这里的目的不是记录每个查询,而是为异常值查询找到具体的查询文本。

启用该参数后,输出如下所示:

LOG: duration: 454.746 ms execute a8: SELECT “backend_wait_events”.”wait_event”
FROM “backend_wait_events” WHERE “backend_wait_events”.”backend_id” = $1
DETAIL: parameters: $1 = ‘6d2d2787-6c27-4d81-807f-37989dc6b9b0’

正如你所看到的,我们可以获得客户端发送的参数,并且如果pg_stat_statements将替换任何值,这些值也将在日志事件中准确地体现出来。现在,我们可以在此上运行EXPLAIN,生成正确的查询计划:

=> EXPLAIN SELECT “backend_wait_events”.”wait_event” FROM “backend_wait_events”
WHERE “backend_wait_events”.”backend_id” = ‘6d2d2787-6c27-4d81-807f-37989dc6b9b0’;
                                  QUERY PLAN
-----------------------------------------------------------------------------
 Seq Scan on backend_wait_events (cost=0.00..168374.85 rows=30012 width=14)
   Filter: (backend_id = ‘6d2d2787-6c27-4d81-807f-37989dc6b9b0’::uuid)
 JIT:
   Functions: 4
   Inlining: false
   Optimization: false
(6 rows)

3.6 使用auto_explain自动收集EXPLAIN计划

上面的步骤适用于偶然运行一些EXPLAIN,但要系统地运行就太费劲了。此外,如果你只是在一两天后查看日志文件,那么可能会得到一个与慢查询发生时实际发生的执行计划不同的执行计划。

因此,我们转向另一个非常有用的Postgres扩展:auto_explain。

auto_explain也附带在Postgres的contrib包中,如同pg_stat_statements那样,同时必须在你的数据库中启用它。请参阅文档中的安装指南。启用后,auto_explain.log_min_duration设置将决定记录哪些查询的EXPLAIN计划。

首先,我们建议将此(auto_explain.log_min_duration)设置为1000毫秒,并根据需要减少它。 

当慢查询产生时,你将会在日志文件中获得这样的计划(见下面):

LOG: duration: 454.730 ms plan:
     Query Text: SELECT “backend_wait_events”.”wait_event” FROM “backend_wait_events” WHERE “backend_wait_events”.”backend_id” = $1
     Seq Scan on public.backend_wait_events (cost=0.00..168374.85 rows=30012 width=14) (actual rows=32343 loops=1)
       Output: wait_event
       Filter: (backend_wait_events.backend_id = ‘6d2d2787-6c27-4d81-807f-37989dc6b9b0’::uuid)
       Rows Removed by Filter: 6445165
       Buffers: shared hit=16145 read=71261Share

**PGANALYZE**

不想自己挖掘日志文件?

pganalyze日志洞悉(Log Insights)自动为你提取有用的日志事件和信息(如:查询样本和EXPLAIN计划),同时将它们与查询统计信息一起展示在一个统一的界面里。
点击这里来了解更多关于pganalyze日志洞悉的信息。

3.7 根据EXPLAIN计划确定缺失的索引

现在,让我们回顾之前得到的EXPLAIN计划,并设法理解如何提高性能。为了得到更全面的查询执行细节,我们可以在执行命令EXPLAIN时,搭配选项ANALYZE和BUFFERS:

=> EXPLAIN (ANALYZE, BUFFERS) SELECT “backend_wait_events”.”wait_event” FROM “backend_wait_events” WHERE “backend_wait_events”.”backend_id” = ‘6d2d2787-6c27-4d81-807f-37989dc6b9b0’;
                                     QUERY PLAN
------------------------------------------------------------------------------
 Seq Scan on backend_wait_events (cost=0.00..168374.85 rows=30012 width=14) (actual time=3.004..537.623 rows=32343 loops=1)
   Filter: (backend_id = '6d2d2787-6c27-4d81-807f-37989dc6b9b0'::uuid)
   Rows Removed by Filter: 6445165
   Buffers: shared hit=417 read=86989
 Planning Time: 0.100 ms
 JIT:
   Functions: 4
   Generation Time: 0.361 ms
   Inlining: false
   Inlining Time: 0.000 ms
   Optimization: false
   Optimization Time: 0.262 ms
   Emission Time: 2.210 ms
 Execution Time: 628.484 ms
(14 rows)

首先,你可以在这里看到对“JIT”的引用,它是PostgreSQL最近添加的,可以在Postgres 11或更新版本上使用。之所以在这里激活它,是因为该查询运行时开销较高同时还处理大量行。如果你想了解更多关于JIT的信息,请查看我们的博文(点击这里)。

当我们解读EXPLAIN计划时,最合理的做法是关注计划中开销最大的部分。示例的计划很简单,因为我们只有一个计划节点“Seq Scan”节点。顺序扫描(sequential scans)按顺序读取表数据(因此得名),不使用索引。

可以看到Postgres用它正在寻找的特定字段“backend_id”来过滤扫描结果,所以它必须丢弃大量的行,由“rows Removed by Filter:”指明丢弃的行数。Postgres也加载了大量的数据,由“Buffers:”信息指明——确切的说,它是在从磁盘加载680MB的数据(读取86989个缓冲区,乘以默认的Postgres块大小8KB)。

现在,下一步是理解为什么Postgres要做顺序扫描——可能是因为没有索引?

使用标准工具检查这个疑问的最简单方法是在Postgres客户端“psql”中使用\\d命令查看这个表:

=> \\d backend_wait_events
Table “public.backend_wait_events”
          Column       |    Type   | Nullable | Default
-----------------------+-----------+----------+-------------------
 backend_wait_event_id | uuid      | not null | gen_random_uuid()
 server_id             | uuid      | not null |
 backend_id            | uuid      | not null |
 seen_at               | timestamp | not null |
 wait_event_type       | text      | not null |
 wait_event            | text      | not null |
Indexes:
    “backend_wait_events_pkey” PRIMARY KEY, btree (backend_wait_event_id)

我们可以看到,在表的主键上有一单个索引。在我们查询的字段上没有索引,因此进行顺序扫描是必然的。

现在,假设我们创建一个这样的索引:
CREATE INDEX CONCURRENTLY ON backend_wait_events(backend_id);
然后重新运行EXPLAIN:

=> EXPLAIN (ANALYZE, BUFFERS) SELECT “backend_wait_events”.”wait_event” FROM
“backend_wait_events” WHERE “backend_wait_events”.”backend_id” = ‘6d2d2787-6c27-4d81-807f-37989dc6b9b0’;
                                                     QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on backend_wait_events (cost=697.03..61932.29 rows=30012 width=14) (actual time=9.044..197.026 rows=32343 loops=1)
   Recheck Cond: (backend_id = ‘6d2d2787-6c27-4d81-807f-37989dc6b9b0’::uuid)
   Heap Blocks: exact=26451
   Buffers: shared hit=126 read=26452 written=6
   -> Bitmap Index Scan on backend_wait_events_backend_id_idx
(cost=0.00..689.52 rows=30012 width=0) (actual time=5.537..5.539 rows=32343 loops=1)
          Index Cond: (backend_id = ‘6d2d2787-6c27-4d81-807f-37989dc6b9b0’::uuid)
          Buffers: shared hit=126 read=1
 Planning Time: 0.154 ms
 Execution Time: 286.110 ms
(9 rows)

我们现在使用位图索引扫描(Bitmap Index Scan)而不是顺序扫描。我们可以看到,基于该索引,性能提高了2倍。很好,但我们能做得更好吗?
事实上我们可以的!正如我们在新执行计划中看到的,为了获得我们正在寻找的列“wait_event”的值,我们仍然会从表本身加载26451个数据块(207 MB)。如果我们简单地将该列包含在索引中会怎么样呢?

在以前的Postgres版本中,你可以像这样创建一个多列索引:

CREATE INDEX CONCURRENTLY ON backend_wait_events(backend_id, wait_event);

但是,由于我们是在Postgres11上测试的,所以我们也可以使用新关键字“INCLUDE”来指定我们想要在索引中出现的非键列:

CREATE INDEX CONCURRENTLY ON backend_wait_events(backend_id) INCLUDE (wait_event);

现在,新执行计划看起来是这样的(见下面):

=> EXPLAIN (ANALYZE, BUFFERS) SELECT “backend_wait_events”.”wait_event” FROM “backend_wait_events” WHERE “backend_wait_events”.”backend_id” = ‘6d2d2787-6c27-4d81-807f-37989dc6b9b0’;
                               QUERY PLAN
------------------------------------------------------------------------------
 Index Only Scan using backend_wait_events_backend_id_wait_event_idx on backend_wait_events (cost=0.43..1496.33 rows=35194 width=14) (actual time=0.017..96.079 rows=32343 loops=1)
   Index Cond: (backend_id = ‘6d2d2787-6c27-4d81-807f-37989dc6b9b0’::uuid)
   Heap Fetches: 0
   Buffers: shared read=168
 Planning Time: 0.059 ms
 Execution Time: 188.495 ms
(6 rows)

这又带来了1.5倍的性能提升。此外,我们将从磁盘加载的数据量减少到1.3MB,这是最初执行计划的500倍!加载数据的减少将降低磁盘上的压力,并允许其他查询使用现在释放出来的I/O带宽。

我们可以看到,优化查询性能是值得的。然而,运行所有这些查询并处理完每个查询的数据可能需要大量的工作。这是我们构建pganalyze的主要原因之一。

通过使用pganalyze,我们可以自动为你处理这个过程,这样你就可以快速找到查询速度慢的根本原因,并立即添加正确的索引。

以上是关于最佳实践:优化Postgres查询性能(上)的主要内容,如果未能解决你的问题,请参考以下文章

最佳实践:优化Postgres查询性能(下)

最佳实践:优化Postgres查询性能(下)

MySQL性能优化的21个最佳实践

在 React 应用中映射数组以优化性能的最佳实践

webpack包教不包会性能优化最佳实践

mysql性能优化-慢查询分析,优化索引最佳实践