分享到

简介

在使用数据库支持的应用程序时,性能管理是一项持续的任务。运行缓慢的查询会导致超时,降低用户体验,使用更多资源,甚至可能影响你的预算,具体取决于你如何支付数据库费用。这些问题使了解数据库的性能特征变得很重要,以便你可以识别和修复有问题的查询。

在本指南中,我们将讨论在 PostgreSQL 数据库中识别性能不佳的查询的不同方法。之后,我们将讨论你可以用来修复慢速查询以维持 PostgreSQL 性能的不同技术。

检查活动查询和进程

在尝试跟踪低效查询时,要检查的第一个地方是当前活动查询和进程的列表。PostgreSQL 通过 pg_stat_activity 视图 提供此数据。

The pg_stat_activity 视图是 PostgreSQL 累积统计系统中可用的视图之一。它包含每个服务器进程的一行,这将帮助你查看每个进程当前正在执行的操作。

要显示视图中的所有信息,请键入

SELECT * FROM pg_stat_activity \gx
-[ RECORD 1 ]----+-------------------------------
datid |
datname |
pid | 1963
leader_pid |
usesysid |
usename |
application_name |
client_addr |
client_hostname |
client_port |
backend_start | 2022-11-01 11:03:44.083043+01
xact_start |
query_start |
state_change |
wait_event_type | Activity
wait_event | AutoVacuumMain
state |
backend_xid |
backend_xmin |
query_id |
query |
backend_type | autovacuum launcher
-[ RECORD 2 ]----+-------------------------------
datid |
datname |
pid | 1965
leader_pid |
usesysid | 10
usename | postgres
application_name |
client_addr |
client_hostname |
client_port |
backend_start | 2022-11-01 11:03:44.083926+01
xact_start |
query_start |
state_change |
wait_event_type | Activity
wait_event | LogicalLauncherMain
state |
backend_xid |
backend_xmin |
query_id |
query |
backend_type | logical replication launcher
-[ RECORD 3 ]----+-------------------------------
datid | 13921
datname | postgres
pid | 836027
leader_pid |
usesysid | 10
usename | postgres
application_name | psql
client_addr |
client_hostname |
client_port | -1
backend_start | 2022-11-06 20:20:18.273218+01
xact_start | 2022-11-06 20:39:01.207078+01
query_start | 2022-11-06 20:39:01.207078+01
state_change | 2022-11-06 20:39:01.207088+01
wait_event_type |
wait_event |
state | active
backend_xid |
backend_xmin | 762
query_id |
query | select * from pg_stat_activity
backend_type | client backend
-[ RECORD 4 ]----+-------------------------------
datid |
datname |
pid | 1961
leader_pid |
usesysid |
usename |
application_name |
client_addr |
client_hostname |
client_port |
backend_start | 2022-11-01 11:03:44.082354+01
xact_start |
query_start |
state_change |
wait_event_type | Activity
wait_event | BgWriterHibernate
state |
backend_xid |
backend_xmin |
query_id |
query |
backend_type | background writer
-[ RECORD 5 ]----+-------------------------------
datid |
datname |
pid | 1960
leader_pid |
usesysid |
usename |
application_name |
client_addr |
client_hostname |
client_port |
backend_start | 2022-11-01 11:03:44.082065+01
xact_start |
query_start |
state_change |
wait_event_type | Activity
wait_event | CheckpointerMain
state |
backend_xid |
backend_xmin |
query_id |
query |
backend_type | checkpointer
-[ RECORD 6 ]----+-------------------------------
datid |
datname |
pid | 1962
leader_pid |
usesysid |
usename |
application_name |
client_addr |
client_hostname |
client_port |
backend_start | 2022-11-01 11:03:44.082653+01
xact_start |
query_start |
state_change |
wait_event_type | Activity
wait_event | WalWriterMain
state |
backend_xid |
backend_xmin |
query_id |
query |
backend_type | walwriter

注意: 使用 \gx 行终止序列而不是传统的 semicolon (;) 告诉 PostgreSQL 对当前查询使用扩展输出模式。这将以垂直而不是水平的方式显示每个记录的列和关联值,这在某些情况下可以提高可读性。

输出中包含许多字段,这些字段在查找较慢的查询时可能会有所帮助。一些最相关的字段包括

  • state: 进程的当前状态。列为 active 的行当前正在执行。其他状态包括 idle(用于等待新客户端命令的进程)、idle in transaction(用于在事务上下文中等待命令的进程)以及 idle in transaction (aborted)(用于语句导致错误的事务)。
  • query: 最近执行的查询。对于活动进程,这将是当前正在执行的查询。
  • usename: 与进程关联的用户的名称。
  • application_name: 连接到进程的应用程序的名称。
  • datname: 用户连接到的数据库的名称。
  • wait_event: 进程正在等待的事件的名称(如果有)。如果进程处于 active 状态并且存在 wait_event,则意味着查询当前被系统其他部分阻塞。
  • wait_event_type: 进程正在等待的事件类别。
  • pid: 进程的进程 ID。
  • query_start: 对于活动查询,当前查询开始的时间戳。
  • xact_start: 当前事务开始的时间戳(如果进程正在执行事务)。

我们可以根据与当前上下文相关的任何列过滤查询。一个有用的模式是使用 age() 函数来计算查询运行了多长时间。例如

SELECT
age(clock_timestamp(), query_start),
usename,
datname,
query
FROM pg_stat_activity
WHERE
state != 'idle'
AND query NOT ILIKE '%pg_stat_activity%'
ORDER BY age desc;

这将显示非空闲查询的执行时间、用户名、数据库和查询文本。我们根据最长到最短运行的查询对结果进行排序,并将此特定查询排除在结果之外。

类似地,你可以看到所有非空闲但确实具有等待事件的进程

SELECT
usename,
datname,
query,
wait_event_type,
wait_event
FROM pg_stat_activity
WHERE
state != 'idle'
AND query wait_event != ''

这可以帮助你看到由于系统其他部分(例如,锁争用)而当前没有进展的查询。

检查其他系统统计信息

虽然 pg_stat_activity 视图可能提供您识别较慢查询所需的大部分信息,但查看其他系统统计信息也很有用,有助于识别其他需要优化的目标。

查看数据库统计信息

pg_stat_database 表包含有关每个数据库的统计信息

SELECT * FROM pg_stat_database \gx
. . .
-[ RECORD 2 ]------------+------------------------------
datid | 13921
datname | postgres
numbackends | 1
xact_commit | 266
xact_rollback | 9
blks_read | 229
blks_hit | 11263
tup_returned | 118708
tup_fetched | 3563
tup_inserted | 0
tup_updated | 0
tup_deleted | 0
conflicts | 0
temp_files | 0
temp_bytes | 0
deadlocks | 0
checksum_failures |
checksum_last_failure |
blk_read_time | 0
blk_write_time | 0
session_time | 5303626.534
active_time | 200.906
idle_in_transaction_time | 0
sessions | 2
sessions_abandoned | 0
sessions_fatal | 0
sessions_killed | 0
stats_reset | 2022-11-06 20:20:18.279798+01
. . .

一些对我们目的而言有趣的列包括

  • blks_read: 数据库中读取的磁盘块数。
  • blks_hit: 磁盘块在缓冲区缓存中找到的次数(避免从磁盘进行缓慢读取)。
  • xact_commit: 提交的事务数。
  • xact_rollback: 回滚的事务数。

正如 Data Egret 团队在其博客中展示的那样,您可以使用这些原始值计算有趣的统计数据,例如缓存命中率

SELECT
datname,
100 * blks_hit / (blks_hit + blks_read) as cache_hit_ratio
FROM
pg_stat_database
WHERE
(blks_hit + blks_read) > 0;
datname | cache_hit_ratio
-----------+-----------------
| 99
postgres | 98
template1 | 99
(3 rows)

这可以提供宝贵的信息,帮助您评估将 RAM 添加到数据库集群中是否会有益,以便有效地缓存最常见的查询。

查看表统计信息

另一个有用的视图系列是 pg_stat_all_tablespg_stat_user_tablespg_stat_sys_tablespg_stat_all_tables 视图显示所有数据库的访问统计信息,而另外两个视图根据表是用户表还是系统表来筛选表。

SELECT * FROM pg_stat_all_tables \gx
. . .
-[ RECORD 104 ]-----+------------------------
relid | 1262
schemaname | pg_catalog
relname | pg_database
seq_scan | 5168
seq_tup_read | 20655
idx_scan | 20539
idx_tup_fetch | 20539
n_tup_ins | 0
n_tup_upd | 0
n_tup_del | 0
n_tup_hot_upd | 0
n_live_tup | 0
n_dead_tup | 0
n_mod_since_analyze | 0
n_ins_since_vacuum | 0
last_vacuum |
last_autovacuum |
last_analyze |
last_autoanalyze |
vacuum_count | 0
autovacuum_count | 0
analyze_count | 0
autoanalyze_count | 0

这些视图中一些有趣的列包括

  • seq_scan: 对表执行的顺序扫描次数。
  • seq_tup_read: 从顺序扫描返回的行数。
  • idx_scan: 对表执行的索引扫描次数。
  • idx_tup_fetch: 通过索引检索的行数。

这些列中的数字可以帮助您评估索引的性能以及索引是否被您运行的查询有效使用。如果您发现表存在许多顺序扫描,您可能需要创建其他索引,以便被最常见的查询使用。

查看索引命中次数

如果您需要有关当前索引的更多信息,可以查看 pg_stat_all_indexespg_stat_user_indexespg_stat_sys_indexes 视图

SELECT * FROM pg_stat_all_indexes \gx
. . .
-[ RECORD 6 ]-+----------------------------------------------
relid | 1249
indexrelid | 2659
schemaname | pg_catalog
relname | pg_attribute
indexrelname | pg_attribute_relid_attnum_index
idx_scan | 822
idx_tup_read | 1670
idx_tup_fetch | 1670
. . .

这些视图为您提供有关每个索引的使用频率的信息。 idx_scan 列显示索引扫描的次数。 idx_tup_read 列显示扫描返回的条目数,而 idx_tup_fetch 列显示索引扫描返回的总行数。

此信息有助于您了解何时存在未被查询使用的索引。确定这些索引后,您可以重写查询以利用索引,也可以删除未使用的索引以提高写入性能。

查看锁定信息

您收集的有关缓慢查询的一些信息可能指向锁定问题。您可以通过查询 pg_locks 视图来获取有关当前持有的所有锁的更多信息

SELECT * FROM pg_locks \gx
-[ RECORD 1 ]------+----------------
locktype | relation
database | 13921
relation | 12290
page |
tuple |
virtualxid |
transactionid |
classid |
objid |
objsubid |
virtualtransaction | 3/3920
pid | 967262
mode | AccessShareLock
granted | t
fastpath | t
waitstart |
-[ RECORD 2 ]------+----------------
locktype | virtualxid
database |
relation |
page |
tuple |
virtualxid | 3/3920
transactionid |
classid |
objid |
objsubid |
virtualtransaction | 3/3920
pid | 967262
mode | ExclusiveLock
granted | t
fastpath | t
waitstart |

输出将提供有关 PostgreSQL 中所有锁的信息。这可以帮助您诊断在不同进程请求对同一对象的控制时可能发生的争用问题。

一些可能有助于您调查有问题的锁的列包括

  • locktype: 可锁定对象 的类型
  • database/relation/page/tuple: 锁定项目的 ID。对于数据库和关系,这些 ID 可以与 pg_databasepg_class 进行交叉引用。
  • mode: 实现或请求的 锁模式
  • granted: 表示是否授予锁的布尔值。

启用慢查询日志记录

一种更轻松地查找有关长时间运行的查询的信息的方法是启用慢查询日志记录。启用慢查询日志记录允许 PostgreSQL 自动记录任何执行时间超过特定时间的查询。这使您能够收集有关当前未调查时执行缓慢的查询的信息。

检查 PostgreSQL 是否已在记录慢查询

您应该做的第一件事是验证慢查询日志记录的当前状态。如果慢查询日志记录已启用,则无需执行任何操作。

您可以通过键入以下命令来检查是否启用了慢查询日志记录

SELECT * FROM pg_settings WHERE name = 'log_min_duration_statement'\gx
-[ RECORD 1 ]---+---------------------------------------------------------------------------
name | log_min_duration_statement
setting | -1
unit | ms
category | Reporting and Logging / When to Log
short_desc | Sets the minimum execution time above which all statements will be logged.
extra_desc | Zero prints all queries. -1 turns this feature off.
context | superuser
vartype | integer
source | default
min_val | -1
max_val | 2147483647
enumvals |
boot_val | -1
reset_val | -1
sourcefile |
sourceline |
pending_restart | f

如果您检查 short_descextra_desc 列的值,您将找到允许我们评估日志记录当前是否启用的信息。我们可以看到,慢查询日志记录当前启用,因为 setting 列当前设置为 -1

既然您了解当前状态,可以根据需要更改它。

配置 PostgreSQL 以记录慢查询

在继续之前,请注意,尽管慢查询日志记录非常有用,但它可能会对性能产生额外的影响。PostgreSQL 必须执行其他操作来计时每个查询并将结果记录到日志中。这可能会影响性能并意外地填满硬盘空间。

不建议始终记录慢查询。相反,在您积极调查问题时启用该功能,并在完成调查后禁用它。

全局记录慢查询

考虑到这一点,您可以通过修改 PostgreSQL 服务器的配置文件来全局配置慢查询日志记录。您也可以交互式地修改这些值,但设置配置中的良好默认值将使以后更轻松地交互式地调整它们。

打开 PostgreSQL 的配置文件。您可以通过键入以下命令找到当前配置文件的位置

SHOW config_file;
config_file
-----------------------------------------
/etc/postgresql/14/main/postgresql.conf
(1 row)

在文件中搜索 log_min_duration_statement 设置。如果我们上面示例输出值是从配置文件中读取的,它将设置为 -1 以指示该功能当前处于禁用状态。还有许多其他相关的设置,您可以根据需要进行调整

. . .
# Query logging configuration
#log_min_duration_statement = -1 # -1 is disabled, 0 logs all statements
# and their durations, > 0 logs only
# statements running at least this number
# of milliseconds
#log_min_duration_sample = -1 # -1 is disabled, 0 logs a sample of statements
# and their durations, > 0 logs only a sample of
# statements running at least this number
# of milliseconds;
# sample fraction is determined by log_statement_sample_rate
#log_statement_sample_rate = 1.0 # fraction of logged statements exceeding
# log_min_duration_sample to be logged;
# 1.0 logs all such statements, 0.0 never logs
#log_transaction_sample_rate = 0.0 # fraction of transactions whose statements
# are logged regardless of their duration; 1.0 logs all
# statements from all transactions, 0.0 never logs
. . .

当前,log_min_duration_statement 设置被注释掉,其当前值为 -1,表示默认值。其他设置在文件中已得到很好的注释,允许您对超过最小值的语句进行采样,而不是记录所有语句。最后一个设置允许您对事务中发生的语句进行采样。

您可以通过取消注释 log_min_duration_statement 并将其设置为另一个值来启用长时间查询日志记录。例如,我们可以将其设置为 5 秒,以记录任何完成时间超过该时间的语句

log_min_duration_statement = 5s

保存文件后,您可以通过在 PostgreSQL 中键入以下命令来重新加载 PostgreSQL 服务器

SELECT pg_reload_conf();

您可以通过再次检查当前值来验证服务器是否正在使用您的新设置

SELECT * FROM pg_settings WHERE name = 'log_min_duration_statement'\gx
-[ RECORD 1 ]---+---------------------------------------------------------------------------
name | log_min_duration_statement
setting | 5000
unit | ms
category | Reporting and Logging / When to Log
short_desc | Sets the minimum execution time above which all statements will be logged.
extra_desc | Zero prints all queries. -1 turns this feature off.
context | superuser
vartype | integer
source | configuration file
min_val | -1
max_val | 2147483647
enumvals |
boot_val | -1
reset_val | 5000
sourcefile | /etc/postgresql/14/main/postgresql.conf
sourceline | 506
pending_restart | f

现在,setting 字段设置为 5000,unit 字段设置为 ms,表明我们 5 秒的设置已转换为 5000 毫秒并已应用。 sourcefile 行还确认此值正在从我们修改的配置文件中读取。

针对特定数据库记录慢查询

在尝试检测慢查询时,另一个选择是将慢查询日志记录限制为特定数据库。虽然 log_min_duration_statement 可以全局设置,正如我们在上一节中展示的那样,它也可以在数据库级别进行配置。

要为单个数据库开启慢查询日志记录,请使用 ALTER DATABASE 命令

ALTER DATABASE helloprisma SET 'log_min_duration_statement' = 2000;
ALTER DATABASE

请注意,与全局设置不同,当使用 ALTER DATABASE 命令时,该值必须是一个无单位的整数,表示以毫秒为单位的时间。

我们可以通过查询每个数据库的角色设置来验证该设置是否已应用

\drds
List of settings
Role | Database | Settings
------+-------------+-------------------------------
| helloprisma | log_min_duration_statement=2000
(1 row)

我们可以验证这是否影响了我们之前设置为 5 秒阈值的全局设置

SELECT * FROM pg_settings WHERE name = 'log_min_duration_statement'\gx
-[ RECORD 1 ]---+---------------------------------------------------------------------------
name | log_min_duration_statement
setting | 5000
unit | ms
category | Reporting and Logging / When to Log
short_desc | Sets the minimum execution time above which all statements will be logged.
extra_desc | Zero prints all queries. -1 turns this feature off.
context | superuser
vartype | integer
source | configuration file
min_val | -1
max_val | 2147483647
enumvals |
boot_val | -1
reset_val | 5000
sourcefile | /etc/postgresql/14/main/postgresql.conf
sourceline | 506
pending_restart | f

测试慢查询日志记录

通过发出超过最小日志记录持续时间的语句来测试设置

SELECT pg_sleep(10);
pg_sleep
----------
(1 row)

检查日志,你应该会找到表明长时间运行的查询发生的语句

2022-11-11 17:58:04.719 CET [1121088] postgres@postgres STATEMENT: select sleep(10);
2022-11-11 17:58:42.635 CET [1121088] postgres@postgres LOG: duration: 10017.171 ms statement: select pg_sleep(10);

由于我们对全局限制和特定表有不同的阈值,我们可以使用应该触发一个阈值但不触发另一个阈值的查询时间来测试每个阈值是否正在正确应用。

例如,我们可以连接到具有较低阈值的数据库并在其中休眠 4 秒,这应该会触发一条日志行

\c helloprisma
SELECT pg_sleep(4);

我们的日志显示

2022-11-13 14:46:07.361 CET [1252789] postgres@helloprisma STATEMENT: alter database helloprisma set log_min_duration_statement=2s;
2022-11-13 14:53:05.027 CET [1309069] postgres@helloprisma LOG: duration: 4022.546 ms statement: select pg_sleep(4);

现在,我们可以切换到一个应该只受全局设置影响的不同数据库。相同的休眠语句不应该触发日志行

\c postgres
SELECT pg_sleep(4);

不应记录新的日志行。

结论

在本文中,我们介绍了如何查看和理解 PostgreSQL 提供的一些性能信息。查看这些信息可以让你了解系统资源、查询模式、配置设置中的不同瓶颈。当遇到性能缓慢时,你可以检查 PostgreSQL 提供的信息来开始调查问题行为。

我们还讨论了如何使用慢查询日志记录来准确指出哪些查询正在占用系统资源,以及比预期执行时间更长。记录这些数据并评估生成的日志可以帮助你识别可能需要额外索引、不同查询结构或更有效查询设计的地方。了解如何识别这些昂贵的操作是运行更具功能的数据库支持应用程序的第一步。

关于作者
Justin Ellingwood

Justin Ellingwood

Justin 自 2013 年起就开始撰写有关数据库、Linux、基础设施和开发工具的文章。他目前与妻子和两只兔子住在柏林。他通常不必以第三人称写作,这对所有相关方来说都是一种解脱。