1. 为什么要关心系统视图
接手一套新环境,数据库里所有重要的信息都藏在系统视图里。
很多人第一次接触 PostgreSQL 系统视图,都是从pg_stat_activity开始的——某个应用报连接数不够、数据库卡住了,老同事让你跑一句select * from pg_stat_activity,看看有没有奇怪的会话占着资源。跑完之后你会意识到,原来数据库自己一直在记录“体检数据”,只是平时没人去看。
这些数据就放在系统视图里。PostgreSQL 把“当前有哪些连接”“每张表被读了多少次”“有没有长事务”“锁等多久了”“复制延迟多少”之类的运行状态,整理成一张张只读的视图,供你用 SQL 查询。它们不占额外的业务表空间,不要求你装额外的插件(少数几个视图除外),也不需要专门的客户端工具,psql 连上去就能查。
系统视图解决的是一类很实际的问题:你怎么快速知道数据库现在到底怎么了。做开发的同学通过系统视图可以确认自己是不是踩了锁、能不能找到慢 SQL;做运维的同学通过系统视图可以做容量评估、做变更前的健康检查;架构师在做技术选型或方案对比时,也需要用系统视图来核实真实负载情况,而不是拍脑袋。
这篇文章不是把官方文档抄一遍,而是按我实际使用频率来梳理:哪些视图真的值得记住,遇到具体问题时该查哪几张表,以及我在生产环境踩过哪些坑。
1.1 系统视图和系统表是什么关系
PostgreSQL 把自身元数据存在pg_catalog模式下面,里面既有表也有视图。表是底层存储,比如pg_class记录表和索引的基本信息,pg_attribute记录每个字段的定义,pg_namespace记录模式信息。这些表上的数据,严格来说你也可以直接select,字段不少,命名也偏工程师风格,用起来不太友好。
视图则是在这些表之上封装好的窗口,比如pg_tables、pg_indexes、pg_views,名字直观,字段也整理过,专门给人查。像pg_stat_user_tables这种“统计类视图”,底层是动态统计数据,里面存的是数据库运行时的计数器和采样结果,并不是纯粹的元数据。
所以你可以这样理解:系统表是“底账”,系统视图是“报表”。你日常应该优先查视图,少碰系统表。在官方文档里,凡是名字以pg_开头、同时relkind = 'v'的对象,基本都属于系统视图范畴。一个比较常见的误解是以为系统视图必须superuser才能查,实际上 PostgreSQL 默认给public开放了查询权限,普通用户一样能查大部分视图,只是能看到的行数会因权限有所裁剪,这个后面细说。
1.2 先建立一张自己的“体检SQL清单”
上手最快的方式,不是背视图名,而是建立一份属于自己的常用查询清单。我刚接触 PostgreSQL 的时候,把平时排查问题用到的 SQL 存在一个pg_daily_check.sql文件里,每次接到告警,先跑一遍,基本能定位八成问题。
我建议你也这么做。打开 psql,先用下面几条语句建立手感:
-- 1. 看这个库的整体状态 SELECT datname, numbackends, xact_commit, xact_rollback, blks_read, blks_hit FROM pg_stat_database WHERE datname = current_database(); -- 2. 看当前所有会话 SELECT pid, usename, application_name, client_addr, state, wait_event_type, wait_event, now() - state_change AS state_duration, left(query, 120) AS query FROM pg_stat_activity WHERE state IS NOT NULL ORDER BY state_change; -- 3. 看每张表的访问情况和膨胀迹象 SELECT schemaname, relname, seq_scan, idx_scan, n_live_tup, n_dead_tup, last_autovacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 20;这三条是很好的起点。它们分别对应“库的健康度”“当前活动”“表和索引的热度”。之后你在实际工作中会逐渐发现,很多更复杂的排查,本质上都是在给这几类查询加条件、加关联、加统计函数。
2. 常用系统视图怎么分类记忆
PostgreSQL 的系统视图非常多,如果一个个零散记忆,很容易记混。我自己习惯把它们分成三类:描述静态结构的、描述实时状态的、描述历史统计的。分好类之后,遇到问题先判断属于哪一类,再去对应的视图里翻,效率高很多。
2.1 描述“家底”的结构类视图
这一组视图回答的问题是:库里有哪些库、有哪些模式、有哪些表、有哪些索引、字段类型是什么。
pg_database:集群里所有数据库的基本信息,包括数据库名、所有者、字符集、datfrozenxid(事务冻结边界)。pg_namespace:所有模式(schema)的列表。pg_class:所有表、索引、视图、序列等对象的底层记录,信息很全,但字段晦涩。pg_tables:包装后的“用户表清单”,包含表名、所属模式、是否有分区、是否带WITH选项等。pg_indexes:索引清单,包含索引定义语句。pg_views和pg_matviews:普通视图和物化视图的清单,物化视图还多一个是否已填充的字段。
结构类视图最大的价值是帮你快速摸清环境。举个例子,刚到一个新项目,想确认某个库下面到底有哪些业务表,表都建在哪个 schema 里,直接执行:
SELECT schemaname, tablename, tableowner, tablespace FROM pg_tables WHERE schemaname NOT IN ('pg_catalog', 'information_schema') ORDER BY schemaname, tablename;想确认某张大表的字段结构,除了\d+,你也可以直接查information_schema.columns或者pg_attribute,但实际工作中\d已经足够。所以结构类视图里,真正高频率用的是pg_tables、pg_indexes、pg_matviews,因为它们已经把多张底层表关联好了,查询成本低,结果直观。
顺便提醒一点:information_schema里的tables、columns这类视图,是为了兼容 SQL 标准而存在的,结构固定,跨数据库通用性好,但它不含 PostgreSQL 特有的统计信息和表内部细节。所以跨库兼容场景用information_schema,深入排查 PostgreSQL 自身问题时用pg_catalog下的视图,两者不冲突。
2.2 描述“此时此刻”的状态类视图
这一组回答的问题是:现在数据库里正在发生什么,有没有会话在跑,有没有锁在等,复制有没有追上主库。
pg_stat_activity:当前所有后端进程的状态,包含 pid、用户、数据库、客户端地址、当前执行的 SQL、等待事件、事务开始时间等。这是整个 PostgreSQL 排查体系里最重要的一张视图,没有之一。pg_locks:当前锁信息,包含锁类型、锁模式、是否已获得授权,关联的 pid 和事务 ID。配合pg_stat_activity,可以还原一条完整的锁等待链。pg_stat_replication:流复制中 standby 节点的状态,包含发送 LSN、写入 LSN、刷盘 LSN、回放 LSN,以及各候选节点的同步状态。pg_stat_wal_receiver:只读节点视角下的接收状态,查看接收进程的连接信息和落后程度。pg_stat_io:PG16 引入的 IO 统计视图,可以按 backend 类型和 IO 上下文查看物理读、写、扩展操作的耗时和次数。
状态类视图的时效性不太一样。pg_stat_activity、pg_locks是即时状态,每次查询都会重新读取当前进程信息,所以是实时的。pg_stat_replication也是当前值,延迟几秒就能反映出来。
这里有一个所有新手都会踩的坑:状态类视图大多数是“实时反映当前活动”,但也包含“自统计重置以来的累计值”的字段,比如pg_stat_database里的xact_commit、tup_inserted,这些是累计计数器,不是实时速率。如果你要判断“最近一分钟是不是有大量写入”,正确的做法是连续取两次快照,做差值,而不是直接看那个很大的累计值。
2.3 描述“历史行为”的统计类视图
这一组回答的问题是:过去一段时间里,系统的行为模式是什么样的,哪些 SQL 消耗最高,哪些索引根本没人用。
pg_stat_user_tables:每张用户表上的顺序扫描次数、索引扫描次数、插入/更新/删除行数、活元组数、死元组数、最后一次 vacuum 和 analyze 的时间。判断表膨胀、vacuum 健康度主要靠它。pg_stat_user_indexes:每个索引的扫描次数、读取行数、取回行数。找无用索引、判断索引命中率靠它。pg_stat_database:数据库级累计指标,包括提交/回滚事务数、读写块数、临时文件大小。pg_stat_bgwriter:后台写进程的统计信息,包括检查点次数、缓冲区写入次数。pg_stat_statements:不是默认视图,需要安装pg_stat_statements扩展。它记录每条 SQL 的调用次数、总耗时、平均耗时、共享缓冲区读写、本地临时文件读取等,是分析慢 SQL 最靠谱的入口之一。
统计类视图一般都会有一个reset操作,pg_stat_statements可以用pg_stat_statements_reset()清空,其他很多统计视图也提供了对应的重置函数。不过生产环境不要轻易 reset,历史累计数据对排查“是不是从某个变更之后开始变慢”非常有用,一旦清零,对比基线就没了。
3. 四个真实场景:怎么用系统视图定位问题
死记视图列表没意义,关键是知道什么场景查什么。下面我用四个在生产环境里反复出现的场景来说,每一个都给出可以直接拷贝的 SQL 和排查思路。
3.1 场景一:数据库变慢,连接数告警
数据库慢,应用超时,监控告警“连接数接近上限”,这时候别急着重启数据库,先看pg_stat_activity。
我遇到过很多次,问题根本不是硬件不够,而是几十个会话卡在同一个地方不动,比如都在等一把锁,或者都在跑一个低效的大查询。第一步,先看当前有多少会话、各自处于什么状态:
SELECT state, count(*), min(now() - state_change) AS min_idle_since FROM pg_stat_activity WHERE state IS NOT NULL GROUP BY state ORDER BY count(*) DESC;state字段常见的值有active、idle、idle in transaction、fastpath function call、disabled。这里最值得警惕的是idle in transaction——会话开启了事务,但迟迟不提交或回滚。它自己没干什么事,却可能占着事务 ID,堵住 autovacuum 的推进,还会阻止其他会话清理死元组。
我见过一个典型案例:应用代码里有一段逻辑,获取数据库连接后先begin,然后去调外部接口,外部接口超时设置是 30 秒,但连接池却一直没有释放这个事务。结果高峰期几百个会话全卡在idle in transaction,其他正常请求拿不到连接。当时就是用pg_stat_activity按 state 分组,瞬间看到了异常分布,再查具体卡住的会话:
SELECT pid, usename, client_addr, backend_start, xact_start, now() - xact_start AS xact_age, state, wait_event_type, wait_event, left(query, 200) AS query FROM pg_stat_activity WHERE state = 'idle in transaction' ORDER BY xact_start;找到具体的 pid 之后,先联系业务方确认是否可以安全终止会话。如果确认可以,用pg_terminate_backend(pid)结束对应会话,注意这个函数返回true只代表发送了终止信号,不代表进程立刻结束,观察一下再确认。不要上来就杀所有会话,尤其别在没确认业务影响的情况下重启整个数据库实例。
3.2 场景二:某张表被锁死了,所有更新都在等
锁问题大概是 PostgreSQL 运维里最常被问到的。现象很统一:某张表的 UPDATE 突然全卡住,应用侧不断报“锁等待超时”。这时候要做的,是把pg_locks和pg_stat_activity关联起来,找出“谁锁了谁”。
我自己比较推荐先跑这个经典查询:
SELECT blocked.pid AS blocked_pid, blocked.usename AS blocked_user, blocked.query AS blocked_query, blocker.pid AS blocking_pid, blocker.usename AS blocking_user, blocker.query AS blocking_query FROM pg_stat_activity AS blocked JOIN pg_stat_activity AS blocker ON blocker.pid = ANY(pg_blocking_pids(blocked.pid)) WHERE blocked.state IS NOT NULL AND blocked.wait_event_type = 'Lock';pg_blocking_pids(pid)是 PostgreSQL 提供的一个辅助函数,它返回阻塞指定 pid 的所有后端进程的 pid 组成的数据。用它来关联pg_stat_activity,比直接解析pg_locks的字段简单得多,而且能在多级阻塞场景下直接给出链的起点。
看到结果后,通常阻塞源是一个“开了事务但没提交”的会话,或者是一个正在跑全表更新的长事务。如果阻塞源是业务侧临时会话,确认无人使用后可以pg_terminate_backend;如果是真实业务在更新大表,那就只能等它跑完,同时评估索引是否缺失、更新条件是否有问题。
另一个常见情况是 DDL 锁。比如有人执行了ALTER TABLE,哪怕只是加个字段,在事务未提交前,对目标表的AccessExclusiveLock会让所有普通读写全部排队。这种锁比较隐蔽,因为 DDL 通常执行很快,但事务开着不提交,锁就一直不释放。遇到这个问题,除了看pg_stat_activity的活跃查询之外,还要注意xact_start很早但query很短的情况,那大概率是应用程序没有及时提交事务。
3.3 场景三:磁盘快满了,怀疑是表膨胀
表膨胀是 PostgreSQL 长年运行的普遍问题。简单解释一下原理:PostgreSQL 使用 MVCC 机制,一条被 UPDATE 或 DELETE 的旧版本行,并不会立刻物理消失,而是先标记为“死元组”,事务并发结束后,由 VACUUM 负责回收。如果 autovacuum 没跟上,死元组越积越多,表占用的物理磁盘空间就会持续膨胀。
判断表是否膨胀,主要看两个指标:n_dead_tup是否持续增长,以及last_autovacuum是否长时间没更新。下面这条 SQL 基本能看出全库“最危险的几张大表”:
SELECT schemaname, relname, n_live_tup, n_dead_tup, round(100 * n_dead_tup::numeric / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_pct, pg_size_pretty(pg_total_relation_size(relid)) AS total_size, last_autovacuum, last_autoanalyze FROM pg_stat_user_tables WHERE n_live_tup + n_dead_tup > 100000 ORDER BY n_dead_tup DESC LIMIT 20;dead_pct超过 20% 甚至 50%,基本可以确认膨胀已经比较严重。如果last_autovacuum是 “-” 或者时间非常久远,说明 autovacuum 根本没跑到这张表上,或者被某种原因卡住了,例如长事务导致不能清理死元组,或者表的空闲空间映射块连续较多。
触发手动 VACUUM 时的判断,我可以给一个实用建议:优先跑普通VACUUM,它不会持有排他锁,不阻塞业务。VACUUM FULL会重写整个表并持有AccessExclusiveLock,在业务高峰期跑VACUUM FULL,很可能直接把系统干停摆。如果膨胀到必须收缩物理文件,建议安排在维护窗口期,并且提前确认复制延迟可以接受。
膨胀问题通常还伴随着另一个信号:idx_scan很低而seq_scan很高,说明即使有索引,优化器也可能因为统计信息陈旧而选择了顺序扫描。看到这种情况,第一步不是加索引,而是先ANALYZE一下相关表,让统计信息重新生效。
3.4 场景四:主从复制延迟越来越大
复制延迟在 PostgreSQL 里不算罕见,但排查时要注意一个点:到底是网络传输慢,还是只读节点回放慢。pg_stat_replication提供了不同阶段的 LSN,正好能把这两类问题区分开。
看主库上的复制状态:
SELECT client_addr, state, sent_lsn, write_lsn, flush_lsn, replay_lsn, pg_size_pretty(pg_wal_lsn_diff(flush_lsn, replay_lsn)) AS replay_lag FROM pg_stat_replication;主库上能看到每个 standby 对应的sent_lsn(已发送)、write_lsn(standby 已写入操作系统)、flush_lsn(standby 已刷盘)、replay_lsn(standby 已回放)。如果sent_lsn明显落后于主库当前写入位点,那大概率是网络带宽或主库 WAL 发送进程的问题。如果sent_lsn已经跟上,而replay_lsn落后很多,说明 standcuby 上的回放进程在等什么,常见原因是 standby 端有长查询在跑,冲突了;或者 standby 磁盘 I/O 太差,回放速度跟不上。
在 standby 节点上,可以配合看pg_stat_activity里walreceiver进程的等待事件,再结合系统负载判断是 I/O 瓶颈还是 CPU 瓶颈。如果确认是回放慢,常见做法是调大max_standby_streaming_delay,但这是缓解手段,根本解法通常是检查 standby 上的长查询和索引缺失。
4. 系统视图使用中最容易踩的坑
4.1 统计数字不是实时的
这是新手最容易犯的错。看到n_dead_tup很大就立刻判断膨胀严重,看到pg_stat_user_tables里的计数跟业务实际不一致就怀疑统计丢了,其实这些数字的刷新有固定机制。
pg_stat_activity是实时的,因为它是直接读取当前进程状态的。但pg_stat_user_tables这类统计视图,是在事务提交之后才更新对应的计数器。也就是说,一个事务在运行中产生的更新,还没提交之前,你看到n_tup_upd可能还是旧值。另外,last_vacuum、last_autovacuum、last_analyze、last_autoanalyze这些时间字段,也依赖后台进程真正执行完对应动作之后才会写进去。
还有一个容易忽略的点:pg_stat_statements中 SQL 的平均耗时会受系统时钟和采样周期影响,它记录的是整个执行过程的累计耗时,不包含网络传输时间,所以不能直接等同于应用看到的总耗时。
所以排查问题时,我建议用“两次快照差值”的方法看趋势,而不是单次绝对值。比如要判断某个时段内的写入速率:
-- 第一次取快照 SELECT tup_inserted, tup_updated, tup_deleted, now() AS sample_time FROM pg_stat_database WHERE datname = current_database();间隔几分钟后再执行一次,前后两个tup_inserted之差除以时间差,就是这段时间的平均写入速率。这个方法比任何单一的累计值都可靠。
4.2 pg_stat_statements 为空或全是 0
很多人在需要分析慢 SQL 时,发现pg_stat_statements里一条数据都没有,或者所有耗时全都是 0。原因通常不是 SQL 本身没有执行,而是扩展没启用正确。
pg_stat_statements不是默认开启的。要让它生效,必须在postgresql.conf中设置:
shared_preload_libraries = 'pg_stat_statements'注意,这个参数修改后必须重启 PostgreSQL 才生效。光执行CREATE EXTENSION pg_stat_statements;不行,因为扩展对应的共享库没有提前加载到共享内存里。我身边已经不止一个人在这里踩坑,以为执行完建扩展就万事大吉,结果查询还是空。
另外有一个比较容易忽略的细节:如果你需要blk_read_time和blk_write_time有有效数值,还要确认track_io_timing参数是on。默认情况下这个参数是off,即使 SQL 执行了大量磁盘读取,这两个字段也会显示 0。在生产环境调整track_io_timing是安全的,但会带来一点点额外的性能开销,实测下来通常可以接受。
还有一个权限问题:普通用户查询pg_stat_statements时,默认只能看到自己的 SQL。需要看全库所有 SQL 的话,要授予相应角色pg_read_all_stats权限,或者直接使用超级用户查询。
排查慢 SQL 时,我一般会先看总耗时排序,再关注平均耗时和调用次数。有些 SQL 调用次数极多但单次很快,累计耗时也不低,这种通常不是“慢 SQL”,而是“频繁调用”,解决思路可能要从应用层合并请求或者加缓存入手。
SELECT queryid, calls, round(total_exec_time::numeric, 2) AS total_ms, round(mean_exec_time::numeric, 2) AS mean_ms, rows, round(shared_blks_hit::numeric * 8192 / 1024 / 1024, 2) AS hit_mb, round(shared_blks_read::numeric * 8192 / 1024 / 1024, 2) AS read_mb FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;4.3 普通用户看不到别人的查询
PostgreSQL 从 9.6 开始,对非超级用户隐藏了pg_stat_activity中的query、query_start、state_change等字段值。这是默认行为,目的是保护不同用户之间的 SQL 隐私。
如果你需要让某个运维账号能看到所有用户的活动 SQL,可以把它加进pg_read_all_stats角色:
GRANT pg_read_all_stats TO your_ops_role;反过来,如果你觉得默认权限过于开放,希望普通用户连“自己数据库里有哪些表”都看不到,那要理解清楚:对系统视图的权限,实际上由底层系统表和视图的 ACL 控制,一般不建议在生产环境乱动。更稳妥的保护边界是通过pg_hba.conf或独立业务账号做访问控制,而不是试图收紧系统表的默认权限。
还有一个容易被误解的点:pg_stat_activity里的query字段显示的内容会受track_activity_query_size参数限制,默认是 1024 字节,超出部分会被截断。如果应用里都是几千字的长 SQL,你在视图里看到的只是 SQL 的前半段。需要保留更长上下文时,可以调大这个参数,但要注意每个后端进程都会按这个大小占用内存,不宜调得过大。
4.4 视图名相近但含义完全不同
pg_stat_user_tables和pg_user_tables很容易混淆。前者是统计视图,关注访问量、死元组、vacuum 时间;后者是对象清单视图,关注表名、所有者、是否分区。名字只差一个stat,性质完全不一样。
pg_stat_bgwriter和pg_stat_io也经常被搞混。pg_stat_bgwriter统计的是后台写进程的检查点和缓冲区写入行为;pg_stat_io是 PG16 之后按 IO 类型细粒度的统计。两者都不直接反映业务 SQL 的性能,而是反映实例整体 I/O 行为。
建议查文档时重点看pg_stat_*前缀,它代表“运行时统计”;不带stat的pg_*视图则多数描述对象结构。遇到不确定的视图,我的习惯是先用\d+ view_name看一下它的列注释,很多视图字段都写清楚了含义,比硬记靠谱。
5. 从视图到监控体系
单个系统视图查询能解决突发问题,但真正稳定运维,需要把系统视图串成体系。我自己在项目里会把这些查询整理进监控脚本,定时采集并贮存到独立的监控库,然后围绕几个核心指标做告警。
重点看的指标不外乎这几类:连接数使用率、idle in transaction会话数、最长事务时间、等待事件分布、n_dead_tup总量增长趋势、复制延迟、pg_stat_statements里 Top SQL 的平均耗时变化。这些全部能从系统视图查到原始数据。
有一点值得注意,系统视图查询本身也会产生数据库负载,尤其是联合多张大视图做实时聚合的时候。比如在内网监控频率很高的场景下,可以适当拉长采集间隔,或者把采集脚本放到从库上执行,避免对主库造成额外压力。
最后提一个我自己的习惯:凡是修改postgresql.conf里的参数,比如开启pg_stat_statements、调整track_activity_query_size,都会顺手在变更记录里写明“改动后需要重启”或“仅需 reload”,避免过两个月回头看配置发现不生效。这个习惯虽然简单,但真的能帮你省掉很多排查时间。
系统视图就像数据库的仪表盘,平时不一定要一直盯着,但遇到问题的时候,它们是最好的第一现场。先把pg_stat_activity、pg_locks、pg_stat_user_tables、pg_stat_replication、pg_stat_statements这几张常用视图用熟练,你就能覆盖大多数日常排查场景了。