数据库

调试与监控


数据库性能是一个广泛的话题,涉及多种影响因素。常见的性能问题原因包括:

  • 低效的数据库模式设计
  • 低效的查询设计
  • 缺乏索引导致对大表的查询速度过慢
  • 未使用的索引导致 INSERTUPDATEDELETE 操作变慢
  • 计算资源不足(如内存),导致数据库频繁访问磁盘获取结果
  • 高负载表上的多查询操作引发的锁竞争
  • 表中存在大量膨胀数据导致查询计划不佳

您可以使用 Supabase CLI 或 SQL 来检查数据库和查询中的这些问题。

使用 CLI 工具

Supabase CLI 提供了一系列工具来帮助检查 Postgres 实例中的潜在问题。CLI 从 Postgres 内部机制获取信息,因此大多数工具兼容任何 Postgres 数据库,无论是否为 Supabase 项目。

Supabase CLI 的安装指南可在此处查阅。

inspect db 命令

Postgres 数据库的检查工具位于 inspect db 命令下。运行 supabase inspect db help 可获取完整的可用命令列表。

1
2
3
4
5
6
7
8
9
10
11
12
$ supabase inspect db help用于检查 Supabase 数据库的工具用法: supabase inspect db [command]可用命令: bloat 估算关系中被死元组占用的空间 blocking 显示持有锁的查询及等待这些锁释放的查询 cache-hit 显示表和索引的缓存命中率...

连接任意Postgres数据库

大多数检查命令都与Postgres无关。您可以通过--db-url提供连接字符串,在任何Postgres数据库上运行检查例程,即使它不是Supabase项目。

例如,您可以连接到本地Postgres实例:

1
supabase --db-url postgresql://postgres:postgres@localhost:5432/postgres inspect db bloat

连接Supabase实例

在使用Supabase时,您可以将Supabase CLI与项目关联:

1
supabase link --project-ref <project-id>

之后,当您位于项目文件夹中时,CLI会自动连接到您的Supabase项目,无需再提供—db-url参数。

检查命令

以下是提供的db检查命令,按不同使用场景分组。

磁盘存储

如果您磁盘存储空间不足,这些命令会很有用:

查询性能

当您的Postgres数据库消耗大量CPU、内存或磁盘IO资源时,以下命令非常有用。您也可以使用它们来调查慢查询问题。

  • cache-hit - 显示整体缓存使用效率
  • unused-indexes - 显示扫描次数较少的索引
  • index-usage - 显示索引使用效率信息
  • seq-scans - 显示所有表上记录的顺序扫描次数
  • long-running-queries - 显示当前正在执行的长时间运行查询
  • outliers - 显示执行时间长但调用次数少的查询,以及同步I/O占用执行时间比例高的查询

锁监控

  • locks - 显示已获取关系排他锁的语句
  • blocking - 显示正在等待锁释放的语句

连接管理

关于 pg_stat_statements 的注意事项

以下命令需要启用 pg_stat_statements 扩展才能使用:calls(调用)、locks(锁)、cache-hit(缓存命中)、blocking(阻塞)、unused-indexes(未使用索引)、index-usage(索引使用情况)、bloat(膨胀)、outliers(异常值)、table-record-counts(表记录数)、replication-slots(复制槽)、seq-scans(顺序扫描)、vacuum-stats(vacuum统计)、long-running-queries(长时间运行查询)。

使用 pg_stat_statements 时还需注意,它仅存储最新的 5,000 条语句。此外,在优化任何查询后,建议通过执行 select pg_stat_statements_reset(); 来重置分析数据。

了解更多关于 pg_stats 的信息请点击此处

使用 SQL

Postgres 累积统计系统

Postgres 通过累积统计系统收集自身运行数据。此外,每个 Supabase 项目默认启用了 pg_stat_statements 扩展。该扩展记录查询执行性能详情,是发现低效查询的最佳方式。这些信息可以与 Postgres 查询计划分析器结合使用,以开发更高效的查询。

以下是一些入门查询示例。

最频繁调用的查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
select auth.rolname, statements.query, statements.calls, -- -- Postgres 13, 14, 15 statements.total_exec_time + statements.total_plan_time as total_time, statements.min_exec_time + statements.min_plan_time as min_time, statements.max_exec_time + statements.max_plan_time as max_time, statements.mean_exec_time + statements.mean_plan_time as mean_time, -- -- Postgres <= 12 -- total_time, -- min_time, -- max_time, -- mean_time, statements.rows / statements.calls as avg_rowsfrom pg_stat_statements as statements inner join pg_authid as auth on statements.userid = auth.oidorder by statements.calls desclimit 100;

该查询显示:

  • 按执行次数排序的查询统计信息
  • 执行查询的角色
  • 查询被调用的次数
  • 返回的平均行数
  • 查询运行累计总时间
  • 最小、最大和平均查询时间

这提供了关于您最频繁运行的查询的有用信息。那些具有较高max_timemean_time值且被频繁调用的查询,通常是优化的良好候选对象。

按执行时间排序的最慢查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
select auth.rolname, statements.query, statements.calls, -- -- Postgres 13, 14, 15 statements.total_exec_time + statements.total_plan_time as total_time, statements.min_exec_time + statements.min_plan_time as min_time, statements.max_exec_time + statements.max_plan_time as max_time, statements.mean_exec_time + statements.mean_plan_time as mean_time, -- -- Postgres <= 12 -- total_time, -- min_time, -- max_time, -- mean_time, statements.rows / statements.calls as avg_rowsfrom pg_stat_statements as statements inner join pg_authid as auth on statements.userid = auth.oidorder by max_time desclimit 100;

该查询会显示按最大执行时间排序的查询统计信息。它与上面按调用次数排序的查询类似,但这个查询会突出显示可能存在高执行时间的异常查询。具有高执行时间或平均执行时间的查询是优化的良好候选对象。

最耗时的查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
select auth.rolname, statements.query, statements.calls, statements.total_exec_time + statements.total_plan_time as total_time, to_char( ( (statements.total_exec_time + statements.total_plan_time) / sum( statements.total_exec_time + statements.total_plan_time ) over () ) * 100, 'FM90D0' ) || '%' as prop_total_timefrom pg_stat_statements as statements inner join pg_authid as auth on statements.userid = auth.oidorder by total_time desclimit 100;

此查询将按累计总执行时间排序显示查询统计信息。它会展示查询运行所花费的总时间以及该查询占总执行时间的比例。

最耗时的查询并不一定是有问题的,您可能有一些非常高效且频繁运行的查询最终占据了较大的总时间百分比。但这个查询有助于发现那些耗时超出预期的查询。

命中率

对于大多数应用程序而言,通常只有一小部分数据会被频繁访问。为确保这些常用数据能够快速获取,Postgres 会追踪数据访问模式,并将其保存在 shared_buffers 缓存中。

缓存命中率较低的应用程序通常性能较差,因为它们需要从磁盘而非内存中获取结果。极低的命中率还可能导致您突破磁盘 IO 限制,从而引发严重的性能问题。

您可以通过执行以下查询查看缓存和索引命中率:

1
2
3
4
5
6
7
8
9
select 'index hit rate' as name, (sum(idx_blks_hit)) / nullif(sum(idx_blks_hit + idx_blks_read), 0) * 100 as ratiofrom pg_statio_user_indexesunion allselect 'table hit rate' as name, sum(heap_blks_hit) / nullif(sum(heap_blks_hit) + sum(heap_blks_read), 0) * 100 as ratiofrom pg_statio_user_tables;

该查询显示了从 Postgres shared_buffers 缓存获取的数据块与从磁盘/操作系统缓存读取的数据块的比例。

如果您的索引或表命中率低于 99%,则可能表明当前计算方案对于您的工作负载来说过小,增加内存会带来性能提升。升级计算资源非常简单,可直接在项目仪表板中完成。

优化性能不佳的查询

Postgres 内置了工具来帮助您优化性能不佳的查询。您可以在识别出的高成本查询上使用查询计划分析器

1
explain analyze <query-statement-here>;

当您在 explain 语句中包含 analyze 时,数据库会尝试执行查询并提供详细的查询计划以及实际执行时间。因此,在 insert/update/delete 查询中使用 explain analyze 时要小心,因为查询会实际执行,可能会产生意外的副作用。

如果只运行 explain 而不带 analyze 关键字,数据库只会进行查询计划而不实际执行查询。当您想检查查询计划而不影响数据库,或者遇到查询超时时,这种方法会很有帮助。

使用查询计划分析器优化查询是一个广泛的主题,有许多在线资源可供参考:

您可以将 pg_stat_statements 提供的信息与通过指标端点获取的详细系统指标相结合,以更好地理解数据库的行为以及您执行的查询。