PostgreSQL 16 Administration Cookbook 读书笔记:第10章 Performance and Concurrency
查找缓慢的 SQL 语句
有两类慢SQL,一类是单个SQL运行很慢,一类是单个SQL运行只许几个毫秒,但由于运行次数很多,单个SQL变慢后导致数据库运行缓慢。
确保pg_stat_statements 扩展已安装:
postgres=# \dx pg_stat_statementsList of installed extensionsName | Version | Schema | Description
--------------------+---------+--------+------------------------------------------------------------------------pg_stat_statements | 1.10 | public | track planning and execution statistics of all SQL statements executed
(1 row)
列出运行时间前10的SQL,时间单位是毫秒:
SELECTcalls,total_exec_time,query
FROMpg_stat_statements
ORDER BYtotal_exec_time DESC
LIMIT 10;calls | total_exec_time | query
-------+--------------------+-------------------------------------------------------------------------------------------------------------------2 | 273667.89018600003 | update employees set salary=salary*$1 where employee_id = $24 | 120106.29585899999 | SELECT pg_sleep($1)2 | 70311.60668900001 | vacuum full1 | 854.657837 | vacuum1 | 401.729725 | copy pgbench_accounts from stdin with (freeze on)2 | 347.715876 | create extension pgagent56 | 272.46782600000006 | SELECT pg_catalog.lower(name) FROM pg_catalog.pg_settings WHERE pg_catalog.lower(name) LIKE pg_catalog.lower($1)+| | LIMIT $21 | 150.076567 | alter table pgbench_accounts add primary key (aid)5 | 141.65087 | CREATE TABLE IF NOT EXISTS purchase ( +| | id SERIAL PRIMARY KEY, +| | product varchar(50) NOT NULL, +| | price double PRECISION NOT NULL +| | )25 | 119.23256599999999 | select * from pg_settings where name = $1
(10 rows)
pg_stat_statements 通过在内存中累积数据来收集所有正在运行的查询的数据,并且开销较低。
相似的 SQL 语句会被规范化,从而删除执行时使用的常量和参数。这样,您可以在报告中的一行中查看所有相似的 SQL 语句,而不是查看数千行。但有时也意味着很难找出哪些参数值真正导致了问题。
在以上输出中,排名第一的update employees,只运行了2次。应该是我做锁的实验时的2个SQL。
也可以记录运行时间超过指定时间的SQL:
postgres=# show log_min_duration_statement;log_min_duration_statement
-----------------------------1
(1 row)postgres=# SELECT pg_current_logfile();pg_current_logfile
------------------------log/postgresql-Sun.log
(1 row)
和记录日志相关的参数还有log_min_duration_statement和log_statement_sample_rate,此略。
找出导致 SQL 运行缓慢的原因
SQL运行慢的原因,主要是:
- 做了太多的工作。
- 如果有额外的,不应该做的工作。则通过优化可以缓解。
- 如果都是必须做的,那就升级硬件、调整架构、上新技术手段。
- 其他因素阻止其工作,如锁。
从两个方向入手:
- SQL本身
- SQL触及的对象
手段是查看执行计划及运行统计。
先看一个全表扫描的:
sampledb=> EXPLAIN (ANALYZE, BUFFERS) select * from employees where employee_id = 100;QUERY PLAN
----------------------------------------------------------------------------------------------------Seq Scan on employees (cost=0.00..3.34 rows=1 width=71) (actual time=0.082..0.083 rows=1 loops=1)Filter: (employee_id = '100'::numeric)Rows Removed by Filter: 106Buffers: shared hit=2Planning Time: 0.174 msExecution Time: 0.154 ms
(6 rows)
其实这个表是有索引的,但由于表太小,还是选择了全表扫描。
那就禁用全表扫描,这回走了索引。
sampledb=> set enable_seqscan=false;
SET
sampledb=> EXPLAIN (ANALYZE, BUFFERS) select * from employees where employee_id = 100;QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------Index Scan using emp_emp_id_pk on employees (cost=0.14..8.16 rows=1 width=71) (actual time=0.015..0.016 rows=1 loops=1)Index Cond: (employee_id = '100'::numeric)Buffers: shared hit=2Planning Time: 0.076 msExecution Time: 0.032 ms
(5 rows)
在 pg_stat_user_tables 中,seq_tup_read 的快速增长意味着发生了大量的顺序扫描。seq_tup_read 与 seq_scan 的比率显示了每次 seqscan 读取的元组数量。同样,idx_scan 和 idx_tup_fetch 列显示了索引是否被使用以及它们的效率如何。
在 pg_statio_user_tables 中,查看 heap_blks_hit 和 heap_blks_read 字段。了解有多少数据位于 共享缓冲区中(heap_blks_hit),以及有多少数据需要从磁盘读取(heap_blks_read)。然后按序调整shared_buffers参数。
尽管MVCC可以让读写互不阻塞。但DDL和并发写仍需要锁。
要回顾性地诊断锁定问题,请使用 log_lock_waits 参数为长时间持有的锁生成日志输出。log_lock_waits控制当会话等待获取锁的时间超过 deadlock_timeout 时是否生成日志消息。这有助于确定锁等待是否导致性能下降。默认为关闭。
EXPLAIN 的 FORMAT参数可控制输出格式为JSON,XML,YAML。
操作系统的top和iostat命令,可以查询内存,CPU和I/O是否为瓶颈。
减少返回的行数
减少客户端和服务器之间的往返,需要多少数据就取多少数据。而不是取回来后在客户端处理。
如果确实需要浏览大量数据,可以加分页等手段来按需获取。select也支持offset和limit子句。
必要时使用存储过程。
再考虑一种场景:一个应用程序运行大量小型查找查询。这种情况很容易发生在现代对象关系映射器 (ORM) 和其他工具包中,它们为程序员完成了大量工作,但同时也隐藏了很多正在发生的事情。此处不是说不能使用ORM,而是要清楚ORM到底做了什么。必要时,直接写SQL。
PostgreSQL 的 TABLESAMPLE 子句(在 9.5 版本中引入)提供了一种从表中检索行的随机子集的方法。这和Oracle的近似算法是不一样的。
sampledb=> select avg(salary) from employees;avg
-----------------------6461.8317757009345794
(1 row)-- 相对于 select ... where random()<0.50
sampledb=> select avg(salary) from employees tablesample system(50);avg
-----(1 row)-- 相对于 select ... where random()<0.70
sampledb=> select avg(salary) from employees tablesample system(70);avg
-----------------------5682.0606060606060606
(1 row)
TABLESAMPLE的system_time(5000)子句可以按照时间采样。
简化复杂的 SQL 查询
这里的复杂是指不必要的复杂。复杂会导致难以维护和诊断。
通常这个SQL比较长,或者隐藏在复杂的视图中。涉及多表连接。
这里就涉及到SQL的优化,包括重写,语法的变化,步骤的简化,以及合理利用数据库的SQL函数,子句(例如分析函数lead、lag,Common Table Expression (CTE),LIMIT子句等,因为数据库提供了更优,更简化的解决方法)等。
还列举了临时表,物化视图等优化手段。
PG本身还提供 Genetic Query Optimization (GEQO)优化,待研究。
还提到了set-returning function,即返回多行的函数。
在不重写查询的情况下加速查询
指调整参数。
- work_mem:设置在写入临时磁盘文件之前查询操作(例如排序或哈希表)所使用的基本最大内存量。
- recursive_worktable_factor:设置规划器对递归查询工作表平均大小的估计,该估计大小是查询初始非递归项估计大小的倍数。这有助于规划器选择最合适的方法将工作表与查询的其他表连接起来。递归查询通常指CTE。
关于索引的优化:
- Covering Indexes:即创建索引时将非索引列带上,以实现Index-Only Scan。因为PostgreSQL 中的所有索引都是二级索引,这意味着每个索引都与表的主数据区(PostgreSQL 术语中称为表的堆)分开存储。这意味着在普通的索引扫描中,每次检索行都需要同时从索引和堆中获取数据。
- partial indexes:部分索引是基于表的子集构建的索引;该子集由条件表达式(称为部分索引的谓词)定义。索引仅包含满足谓词的表行的条目。
- CLUSTER:CLUSTER 指示 PostgreSQL 根据 index_name 指定的索引对 table_name 指定的表进行聚类。这对某些 范围查询、顺序扫描特别高效,因为磁盘的局部性大幅提高。但是,此命令是一次性的,后面的更新会破坏CLUSTER。
调低fillfactor (默认为100)以实现HOT(Heap-Only Tuples)更新,HOT更新比普通更新效率更高,但条件是不更改索引列,页内有足够的空间。
最后,就是需要修改schema,也就是对数据库进行重构了。和代码类似,这也是完全必要的,而且需经常的,小步骤的进行。
发现查询未使用索引的原因
原因就是优化器觉得这样做是对的。
第一步就是保证统计信息准。ANALYZE命令。
第二步,比较使用或不使用索引的成本(set enable_seqscan=false,生产系统不建议)。例如EXPLAIN (ANALYZE ON, SETTINGS ON, BUFFERS ON, WAL ON)
。
强制查询使用索引
通常,优化器比你更了解数据库。如果优化器做的决策不对,通常是由于统计信息不准,因为数据是动态变化的。
通常,索引用来返回少量值。如果返回很多值,则使用索引的成本会增加。为了有效地使用索引,请确保使用 LIMIT 子句来减少返回的行数。
在19.7. Query Planning中有对plan_cache_mode的解释:
准备好的语句(例如由 PL/pgSQL 显式准备或隐式生成)可以使用自定义计划或通用计划执行。自定义计划会使用其特定的参数值集为每次执行重新生成,而通用计划不依赖于参数值,并且可以在多次执行之间重复使用。因此,使用通用计划可以节省规划时间,但如果理想计划高度依赖于参数值,则通用计划可能效率低下。这些选项之间的选择通常是自动进行的,但可以使用 plan_cache_mode 覆盖。允许的值为 auto(默认值)、force_custom_plan 和 force_generic_plan。此设置在执行缓存计划时考虑,而不是在准备缓存计划时考虑。
其中也说明了何时使用force_generic_plan 还是 force_custom_plan。
另一个技巧是使用partial indexes(部分索引),例如可用来排除 NULL 或其他不需要的数据。
用CREATE STATISTICS也可以生成类似于Oracle的复合列统计数据,例如:
CREATE STATISTICS cust_stat1 ON state, area_code FROM cust;
另一个鼓励使用索引的方法是将 random_page_cost 设置为较低的值——甚至可以等于 seq_page_cost。这使得 PostgreSQL 在更多情况下优先使用索引扫描。
更改 random_page_cost 可让您根据数据位于磁盘还是内存做出反应。让优化器知道缓存中存储了更多索引内容,有助于它理解使用索引实际上更便宜。
通过增加 effective_io_concurrency 允许多个异步 I/O 操作,还可以提高较大扫描的索引扫描性能。random_page_cost 和 effective_io_concurrency 均可针对特定表空间或单个查询进行设置。
PostgreSQL 不直接支持提示,但可以通过扩展pg_hint_plan 使用。不过和Oracle一样,hint通常不建议使用。
使用并行查询
通过使用并行处理,可以缩短长时间运行查询的响应时间。其原理是,如果我们将一个大型任务拆分成多个较小的部分,就能更快地获得结果,但同时也会消耗更多资源。
只有大的任务才建议开启并行,如BI。并行通过max_parallel_workers_per_gather设置(默认为2,设为0则禁止并行)。
在 PostgreSQL 9.6 和 10 中,并行查询仅适用于只读查询,因此仅适用于不包含 FOR 子句的 SELECT 语句(例如 SELECT … FOR UPDATE)。此外,并行查询只能使用标记为 PARALLEL SAFE 的函数或聚合函数。默认情况下,所有用户定义的函数均未标记为 PARALLEL SAFE,因此请仔细阅读文档,了解您的函数是否可以在当前版本中启用并行功能。
另,min_parallel_table_scan_size决定了启用并行的表大小阈值。
使用即时 (JIT) 编译
即时 (JIT) 编译是将某种形式的解释型程序执行结果转换为原生程序的过程,并在运行时执行。例如,与其使用可以执行任意 SQL 表达式的通用代码来执行特定的 SQL 谓词(例如 WHERE a.col = 3),不如生成一个特定于该表达式的函数,并使其能够由 CPU 原生执行,从而提高速度。
目前,PostgreSQL 的 JIT 实现支持加速表达式求值和元组变形。未来可能会加速其他一些操作。表达式求值用于计算 WHERE 子句、目标列表、聚合和投影。可以通过针对每种情况生成特定代码来加速。元组变形是将磁盘上的元组转换为其内存表示的过程。可以通过创建特定于表布局和要提取的列数的函数来加速。
JIT 编译主要适用于长时间运行且受 CPU 限制的查询。这些查询通常为分析型查询。对于短查询,执行 JIT 编译的额外开销通常会高于其节省的时间。
至于何时应该使用JIT,详见 30.2. When to JIT?。
JIT默认是开启的:
sampledb=> show jit;jit
-----on
(1 row)
使用分区创建时间序列表
如果您有一个很大的表,并且查询仅选择该表的一个子集,那么您可能希望使用块范围索引(BRIN 索引)。当数据在添加到表中时自然排序时,这些索引可以提高性能,例如日志时间列或自然升序的 OrderId 列。添加 BRIN 索引快速且非常简单,并且非常适合时间序列数据记录的用例,尽管在密集更新下效果不佳。向 BRIN 索引中插入命令经过专门设计,不会随着表变大而变慢,因此对于写入密集型应用程序,它们的性能比 B 树索引好得多。B 树确实具有更快的检索性能,但需要更多资源。
使用分区的最佳理由是允许您快速删除无关的数据。
您可以同时使用 BRIN 索引和分区,这样就无需创建大量分区。建议分区大小不要大于共享缓冲区,以便当前整个分区能够位于共享缓冲区内。
随着表的增大,B 树的性能下降非常缓慢,因此对于OLTP负载,大表并不用担心。而对于OLAP负载,使用分区并限制每个分区的大小可以避免数据量随时间增长而带来的任何不利影响。
使用乐观锁定避免长时间的锁等待
乐观锁定假设其他人不会更新相同的记录,并在更新时检查这一点,而不是在客户端处理信息所需的时间内锁定记录。
不过,PG原生并不支持乐观锁,这是通过修改应用实现的。实际就是通过Compare and Set实现的,因为这是一个原子操作。可以看一个示例。
在某些情况下可用的另一种设计模式是使用单个语句作为 UPDATE 子句,并通过 RETURNING 子句将数据返回给用户。
UPDATE table_name
SET column1 = value1, column2 = value2, ...
WHERE condition
RETURNING column1, column2, ...;
在某些情况下,将整个计算过程移至数据库函数中是一个非常好的主意。如果您可以将所有必要的信息作为数据库函数传递给数据库进行处理,那么运行速度会更快,因为您省去了多次往返数据库的麻烦。如果您使用 PL/pgSQL 函数,您还可以受益于在会话中第一次调用时自动保存查询计划,并在后续调用中使用已保存的计划。
报告性能问题
如果您需要获得有关性能问题的一些建议,那么正确的去处是性能邮件列表。
有关性能问题报告中应包含哪些内容的详细描述,请访问Guide to reporting problems。
更多与性能相关的信息可以在Performance Optimization wiki中找到。