2026/10/17 5:33:44

PostgreSQL系统表详解:用pg_catalog构建运维排障与元数据地图

PostgreSQL系统表详解:用pg_catalog构建运维排障与元数据地图 1. 先说结论pg系统表是PG的“元数据中枢”也是运维排障的藏宝图接触PostgreSQL这些年我最常被问的一个问题是“你们DBA平时到底怎么摸清一个数据库的家底的”。说真的答案就一句话——去翻pg_catalog下的系统表和视图。用过MySQL的可能对information_schema比较熟悉而PG的原生元数据体系比MySQL更底一层、也更全它就叫系统表。PG系统表本质上就是“关于数据的数据”也就是元数据。建表、加字段、建索引、设权限、跑VACUUM、查统计信息所有动作最终都会落到pg_catalog那几十张表里。换句话说你手工在psql里敲出一张表的字段清单本质上就是在查pg_attribute你查看某张表占了多少磁盘本质上是查pg_class表的relpages字段。所有我们能感知到的数据库状态最后都能追踪到这些底层表格里。这篇总结适合谁看一类是刚上手PG、想彻底搞懂“表结构从哪来”“权限怎么查”的开发者另一类是已经会用PG但总感觉缺一张“排障地图”的运维朋友。我会按系统表的定位、核心表逐个拆解、实战SQL、版本差异和常见坑这几个维度来展开尽量把每张表背后的“为什么”也讲明白而不是只丢给你一张清单。2. 先弄清整体布局pg_catalog里到底装着什么2.1 系统表的第一性原理元数据也是数据我在给团队做分享时最喜欢打一个比方PG系统表之于数据库就像“人体经络图”之于中医。你看不见它但它决定了所有气血运行的路由。每当一条SQL进来优化器嘴上说的是执行计划背后其实在疯狂查询pg_class、pg_attribute、pg_statistic这些表判断表是否存在、字段类型是什么、有没有可用的索引、数据分布大概什么样。这种“元数据也是数据”的架构是PG区别于很多商业数据库的底层基因。它带来的最大好处是一切都可查询。既然系统表是表我就能用SQL去查SQL用元数据去管理元数据。比如我想知道当前库中哪个表膨胀最厉害我就查pg_stat_all_tables视图并关联pg_class去取它的大小我想找出没有主键的表一条SQL就能扫遍全库。这些都是靠把系统表暴露成普通表来实现的。同时系统表默认都创建在名为pg_catalog的schema下。这里要注意pg_catalog不是传统意义上用户建的schema它是数据库初始化时就存在的内置schema排在search_path的第一个位置。所以你在psql里直接敲\dt看到的都是用户表但敲\dt pg_*看到的全是系统表。两者的对象ID空间也是区分的用户对象通常大于16384系统对象一般小于这个值。这个ID划分在后面排查OID冲突、备份恢复时特别关键。2.2 打开pg_catalog的正确姿势先分清“表”和“视图”这里必须强调一个容易被忽略的差异pg_catalog内部其实分两类对象。一类是真正的表比如pg_class、pg_attribute、pg_namespace它们直接存物理数据也可以被写但我不建议写后面会说。另一类是视图和系统视图比如pg_tables、pg_indexes、pg_stat_all_tables它们本质是拼好了方便人看的“报表”。刚开始学PG时我经常在pg_class、pg_tables、pg_stat_all_tables之间绕晕。实际上pg_tables是一个视图它内部JOIN了pg_class和pg_namespace把表名、表空间、所有者这些已经拼好的信息暴露出来pg_stat_all_tables则是基于pg_class和统计模块拼出的“带统计数字的表清单”。理解了这层关系你查数据时就有方向了如果只是日常快速看用视图最方便如果想做深层联合查询、拿底层字段做运算直接查基表更灵活。2.3 版本演进中的系统表别拿旧经验套新版本PG每出一个大版本系统表都会或多或少增加字段。比如pg_stat_all_tables在PG 9.x时还没有idx_scan之类详细索引统计到PG 14后视图背后的统计逻辑改了好几次。如果你是从MySQL转过来第一次用PG 15或16查到一些新增字段名觉得陌生完全正常——像pg_authid中的rolpassword在PG 14以后支持SCRAM验证存储形式都不一样了。因此读这篇总结时我建议你对照手头版本的官方文档再用思路和方法是通用的具体字段以本地环境的pg_attribute查询结果为准。3. 核心系统表逐个拆解从建表到权限全部都有迹可循3.1 pg_class与pg_attribute表结构信息的源头如果一辈子只能记两张系统表那就记pg_class和pg_attribute。pg_class是整个PG对象系统中最重要的表它记录了所有表、索引、序列、视图、物化视图、复合类型、TOAST表的元数据。字段名是rel开头relation的缩写我挑几个最常用的relname对象名比如表名、索引名relnamespace对象所属的命名空间OID关联pg_namespacerelkind对象类型r代表普通表i代表索引S代表序列v代表视图m代表物化视图c代表复合类型t代表TOAST表relpages和reltuples对象占用的磁盘页数和估算行数是优化器做代价估算的核心输入也是我们判断表膨胀的第一手数据relowner属主OID关联pg_authidrelhasindex该表是否有索引relpersistencep永久表、u非日志表、t临时表。pg_attribute则存每一列的属性信息重点字段包括attname列名、atttypid列类型OID关联pg_type、attlen类型长度、attnotnull是否非空、attnum列序号从1开始系统列是负数、atttypmod类型修饰符比如varchar的长度限制、attstattarget该列的统计信息采集目标。我实际排障时最常用的一条组合查询就是用这两张表连同pg_type、pg_namespace拼出“一张表的完整定义”。比如一位开发同事问你“那个订单表的user_id到底是什么类型有没有默认值”我从来不去翻建表脚本直接查pg_attribute。3.2 pg_namespace与pg_database命名空间和实例的“户口本”pg_namespace就是schema的元数据表改名为“命名空间”是为了更通用。它的主要字段是nspnameschema名和nspownerschema属主。这里有个很核心的查询逻辑一个表对象在PG里的完整标识是“database schema table”库内区分对象靠的是namespace跨库之间互不可见。所以如果你想找出当前库所有schema下的用户表关联pg_class和pg_namespace是必经之路。pg_database则是实例级别的表记录这个数据库集群里创建了哪些数据库字段有datname、datdba属主、encoding字符集、datcollate排序规则、datistemplate是否模板库、datallowconn是否允许连接。为什么需要它因为PG里“连接”是数据库维度的一个连接只能访问一个库而排障时经常要确认“某个实例到底建了多少库”“各自是什么字符集”“是不是模板库”直接查pg_database比用psql -l更灵活。3.3 pg_authid与pg_roles揭开权限体系的核心PG权限系统的底层信息主要落在两张表pg_authid和pg_roles。pg_authid存储角色和用户的认证信息敏感字段是rolpassword存的是密码哈希不是明文。请注意这张表的读取权限默认是受限的只有超级用户能查看rolpassword普通用户只能通过pg_roles视图查询不敏感部分。pg_roles直接暴露了角色名、是否超级用户、是否可以登录、连接数限制、有效期等信息。平时我们查权限更常用的视图是pg_user和pg_roles。pg_user是pg_shadow视图的对外简化版主要给“用户”查而pg_roles则把“角色”和“用户”放在一起。PostgreSQL的角色模型里用户和角色本质上是同一个对象组和用户没有硬性边界。角色可以有LOGIN属性就是用户没有LOGIN属性就是组角色。把权限通过GRANT给角色再把角色GRANT给用户就能实现组权限管理。这套体系的所有元数据底层就是pg_authid中每一行的rolname、rolsuper、rolcreaterole、rolcreatedb、rolcanlogin、rolconnlimit这些布尔或整型字段的组合。3.4 pg_constraint与pg_index约束和索引优化器的左膀右臂pg_constraint这张表记录所有约束包括主键、外键、唯一约束、检查约束、排除约束。它的关键字段是contype约束类型p代表主键u代表唯一f代表外键c代表检查conrelid是被约束的表OIDconfrelid是外键引用的表OID。当你想审计“这个库到底哪些表缺主键”或者“哪些外键没有被索引”你就要穿透到pg_constraint里查。pg_index可能是在性能排查里最需要看懂的一张表。它的核心逻辑是indrelid是索引所属的表OIDindexrelid是索引本身的pg_class OIDindisprimary表示是否主键索引indisunique表示是否唯一索引indisvalid表示索引是否有效VACUUM或CREATE INDEX CONCURRENTLY失败时可能失效indkey和indclass分别是索引列号和列类型OID数组。我印象很深的一次事故某张业务表的一个索引因为之前的一次大事务回滚状态停在INVALID。查询优化器完全不选择这个索引导致全表扫描CPU直接打满。当时就是靠查pg_index里indisvalidfalse找到的元凶。索引状态信息藏在系统表里不看真的不知道。3.5 pg_statistic与pg_views等统计信息与“视图背后的视图”pg_statistic是ANALYZE生成的列级统计信息表里面有每列的空值比例、平均字段宽度、最常用值MCV、直方图边界histogram等。注意你手动查pg_statistic看到的都是经过脱敏的信息里面并不直接存真实值比如MCV是以匿名的形式存放的。普通用户更常查的是pg_stats视图它是pg_statistic的可读版本会把数组展开成更容易理解的形式。比如优化器为什么选择某一列做等值搜索而不是另一列本质就是看了pg_stats里的n_distinct、most_common_vals、most_common_freqs等字段。除了统计信息还有一组“用户可读视图”值得经常翻阅pg_views所有视图的定义、pg_indexes所有索引的位置和定义、pg_tables所有表、pg_matviews物化视图。它们本身是视图查的是系统表组合结果用起来省心。比如想知道某张表到底建了哪些索引、索引定义是什么直接查pg_indexes不用再自己拼SQL拼到天亮。3.6 pg_depend和pg_description对象之间的隐藏关系网pg_depend是系统表里的“关系网”它记录数据库对象之间的依赖关系。比如一个视图引用了某张表、一个索引依赖某张表、一个约束依赖某个索引。这有什么用日常最典型的场景是DROP TABLE时PG报错“因为其他对象依赖它”你如果不查pg_depend只能瞎猜是谁依赖了它。我常用的做法是SELECT * FROM pg_depend WHERE refobjid public.order::regclass;查出所有依赖对象再结合pg_class和pg_rewrite等表看依赖类型基本就能定位下级依赖了。另外一个实用场景是排错时判断“某索引是否能删”。如果pg_index查到indisprimary那么它就是主键约束的依附对象不能直接DROP INDEX否则会连带约束报错。pg_description则是对象的注释表。它存的对象描述对象对应的“备注”就是你可以用COMMENT ON语句写入的文本。当你的库比较大、业务表很多时好的注释习惯能节省大量沟通时间而pg_description是保存这些文本的“仓库”。4. 实战用系统表拼出你自己的“数据字典”和排障手册4.1 一张SQL搞定全库表结构摆脱零散的手动翻建表脚本我工作里最常用、也最推荐你先存进工具集的第一条万能SQL是根据一个表名拼出完整的字段定义效果类似MySQL的DESC。核心逻辑就是pg_class、pg_attribute、pg_type、pg_namespace四表关联。以下是我长期在维护的“查询表结构”脚本基于PG 12测试PG 16也兼容SELECT a.attname AS column_name, format_type(a.atttypid, a.atttypmod) AS data_type, a.attnotnull AS not_null, COALESCE(pg_catalog.pg_get_expr(d.adbin, d.adrelid)::text, ) AS default_value, a.attnum AS ordinal_position FROM pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_attrdef d ON (a.attrelid d.adrelid AND a.attnum d.adnum) WHERE a.attrelid public.my_table::regclass AND a.attnum 0 AND NOT a.attisdropped ORDER BY a.attnum;解释几个关键点attnum0是为了排除系统隐藏列attisdropped是判断该列是否被DROP过PG里DROP COLUMN其实是标记删除物理文件里的数据不会立刻消失format_type是PG提供的格式化函数能把类型OID修饰符拼成人类可读的类型名pg_get_expr是反解析函数能从系统表里把默认值表达式转成SQL文本。这套查法的好处是就算建表脚本丢失了你也能从数据库里反向生成结构。把它套在information_schema.columns也可以但PG原生的方式更直接不会因为information_schema的兼容层过滤掉部分特殊类型数据。4.2 找出所有无索引外键这是最容易埋雷的“隐性地雷”外键如果没有匹配索引会造成两个问题一是删除主表记录时从表扫描代价极高二是并发场景下容易产生锁竞争。更麻烦的是PG不会自动为外键列建索引这和某些商业数据库的默认行为不同。所以很多团队都在不知不觉中埋了一堆雷。下面是我每次接手一个新PG库时必须执行的一条检查SQLSELECT tc.conname AS fk_name, tc.conrelid::regclass AS child_table, confrelid::regclass AS parent_table, (SELECT string_agg(att.attname, , ORDER BY ord.ord) FROM unnest(tc.conkey) WITH ORDINALITY AS ord(attnum, ord) JOIN pg_attribute att ON att.attrelid tc.conrelid AND att.attnum ord.attnum) AS fk_cols FROM pg_constraint tc WHERE tc.contype f AND NOT EXISTS ( SELECT 1 FROM pg_index i WHERE i.indrelid tc.conrelid AND tc.conkey i.indkey ) ORDER BY child_table::text;这条SQL的核心思路是找到所有外键约束然后用半连接排除那些已经有索引覆盖外键列的组合。操作符表示“左边的数组是否被右边的数组包含”。我实际跑下来的结果往往是惊人的——业务系统如果经历过多人迭代未索引外键数量常会超过总量的30%。把这些外键补上索引之后很多曾经偶发的死锁问题会自然消失因为锁扫描从全表缩小成了索引定位。4.3 结合pg_class与pg_statistic算表膨胀率决定何时该VACUUMVACUUM是PG运维里永远绕不开的话题。判断一张表需不需要VACUUM或VACUUM FULL最直接的方法是看它膨胀的程度。膨胀的本质是表中的死元组占用了大量空间导致实际存储远大于有效数据量。这个数据从哪里来从pg_stat_all_tables里取n_live_tup和n_dead_tup从pg_class里取relpages。把两者结合起来就能得到一个比较直观的膨胀率估算。SELECT schemaname, relname, n_live_tup, n_dead_tup, n_dead_tup::float8 / GREATEST(n_live_tup, 1) AS dead_ratio, pg_relation_size(relid) / 1024 / 1024 AS size_mb FROM pg_stat_all_tables WHERE schemaname NOT IN (pg_catalog, information_schema) ORDER BY dead_ratio DESC LIMIT 20;如果你发现某张表的n_dead_tup长期居高不下、dead_ratio超过50%而autovacuum却还没有把它标记为需要清理原因多半是autovacuum阈值设置不当或者该表过大导致默认触发阈值20%死元组比例 50个固定门槛远远不够灵敏。这种情况下可以手动执行VACUUM (ANALYZE, VERBOSE) 表名并考虑针对大表单独调高autovacuum_vacuum_scale_factor。这里注意VACUUM FULL会锁表不适合在业务高峰期执行应该放到维护窗口。4.4 锁等待与长事务用系统视图透视堵点锁问题排在PG排障前三名。当大量会话堆积在“waiting”状态时第一件事就是查pg_stat_activity同时关联pg_locks视图定位锁的持有者。我常用的定位长事务和锁等待的查询如下SELECT a.pid, a.state, a.wait_event_type, a.wait_event, a.query_start, a.xact_start, now() - a.xact_start AS xact_age, a.query FROM pg_stat_activity a WHERE a.state idle AND a.xact_start IS NOT NULL ORDER BY a.xact_start LIMIT 20;拿到pid之后继续查它持有哪些锁、阻塞了谁SELECT l.pid, l.locktype, l.relation::regclass AS rel, l.mode, l.granted FROM pg_locks l WHERE l.pid 要查的pid ORDER BY l.granted DESC, l.mode;如果发现一个会话的xact_age已经超过几小时而它的状态是idle in transaction这往往就是罪魁祸首——事务开着没提交手里的锁一直不放后面的写操作堆积成山。这种情况的应急处理是pg_terminate_backend(pid)终止会话但根治手段是查业务代码里的事务边界把长事务扼杀在源头。要记住系统表/view只是提供了“看得见”的能力最终能不能治本还得看应用侧。5. 版本差异和运维中的系统表避坑指南5.1 别在系统表版本差异上翻车PG版本升级时系统表的变更是升级脚本中工作量最大的部分之一。如果你保持“PG 10时代的pg_stat_all_tables查询”跑到PG 16上大概率不会报错但有些新字段你可能一直没用到比如pg_stat_all_tables在PG 14以后增加了用于统计WAL使用情况的信息字段。反过来如果你用了某些旧版本特有的字段新版本可能会移除或者字段被改名。这点在做跨大版本升级前的兼容性检查时尤其重要。我会在升级前把常用查询脚本全部跑一遍专门记录哪些系统字段是“当前版本特有”的做到心里有数。另外不同版本之间的系统视图定义也会有细节变化。以pg_roles为例早期版本中rolreplication字段的默认值写法可能不同后期版本增加了rolbypassrls等新属性。如果你在自动化脚本里硬编码了字段序号升级后极容易出错所以任何涉及系统表的查询都建议显式写字段名表名也用schema限定避免踩search_path的坑。5.2 永远不要直接改系统表血的教训这条内容我在团队里讲过很多次每次讲都把案例调出来有人为了性能优化直接UPDATE pg_class里的relhasindex或relpages有人为了改数据库名直接UPDATE pg_database里的datname。看起来SQL执行成功了但随后备份恢复失败、VACUUM报错、dump数据异常整个实例状态变得不可控。为什么不能手动改因为系统表之间存在大量内部引用关系索引、约束、统计信息、缓存都围绕这些表维持一致性。你直接改了一行pg_class却没有同步更新相关索引或者系统缓存后续操作就可能触发断言错误甚至导致实例起不来。正确做法是能用SQL命令完成的绝不直接操作系统表。比如重命名表用ALTER TABLE改注释用COMMENT ON调整权限用GRANT/REVOKE。只有在特殊场景比如某些极其冷门的扩展操作需要高权限介入时才在测试环境充分验证后才能谨慎使用系统表级的调整并且操作前一定要pg_dump备份。5.3 权限体系下“谁能看到什么”同样需要关注PG系统表的权限模型很有讲究。普通用户能查pg_class、pg_attribute的大部分字段但pg_authid的密码字段不可见pg_statistic里某些列级统计信息也受限于底层表权限。比如你在业务库中执行某些关于“全库所有表大小”的统计SQL时如果当前用户不是超级用户部分系统表的可见行数会少很多导致统计结果不完整。我踩过一个坑用某个只读账号去查pg_stat_all_tables发现有些业务表压根没出现在列表里。后来发现原因是该账号对这些表没有任何权限PG的统计视图默认不暴露不可见对象。解决方案有两个方向一是对排障账号授予足够权限如pg_read_all_stats和pg_read_all_settings等预定义角色二是明确告诉自己“不要用只读账号查全库统计”否则会得出错误结论。5.4 清理pg_catalog里的历史垃圾临时表、未使用索引别再藏系统表能帮我们定位“看不见的垃圾”。例如pg_class里可能存在大量临时表的残留元数据尤其是异常断连导致的临时schema对象它们占用pg_catalog的存储空间导致全库pg_dump变慢。用系统表查一下临时schema下到底有多少对象就能对症清理SELECT n.nspname, count(*) FROM pg_class c JOIN pg_namespace n ON n.oid c.relnamespace WHERE n.nspname LIKE pg_temp% OR n.nspname LIKE pg_toast_temp% GROUP BY n.nspname;如果发现临时schema对象特别多多半是应用连接池没有规范管理会话生命周期。这种情况下对会话做重启和清理更有效而不是直接去系统表里删行。记住系统表是“侦察兵”不是“清洁工”。6. 我整理的一套“系统表速查工具箱”经验我习惯把常用系统表的查询固化成一堆SQL脚本遇到PG实例就能直接跑。这里分享几个场景最常用的判断模板你完全可以在此基础上扩充成自己的工具箱。对象总览场景——查某库下的所有表、大小、行数、是否分区SELECT n.nspname AS schema_name, c.relname AS table_name, pg_relation_size(c.oid) AS size_bytes, c.reltuples::bigint AS estimated_rows, c.relkind FROM pg_class c JOIN pg_namespace n ON n.oid c.relnamespace WHERE c.relkind IN (r, p) AND n.nspname NOT IN (pg_catalog, information_schema) ORDER BY pg_relation_size(c.oid) DESC;从PG 10开始分区表在pg_class里的relkind显示为p普通表为r这在使用时要注意区分。索引诊断场景——找出重复或冗余索引SELECT indrelid::regclass AS table_name, indexrelid::regclass AS index_name, indkey::text, pg_get_indexdef(indexrelid) AS index_def FROM pg_index ORDER BY indrelid::regclass::text;我常拿这段SQL配合视图pg_indexes进一步人工审查重点看相同列组合是否有多个索引尤其是“标准索引”和“表达式索引”是否功能重复。通常一个表上超过5个索引写入性能就会开始变差而系统表能帮你快速定位哪些是“僵尸索引”长期没有idx_scan记录。会话与等待场景——查当前运行时间最长的SQL和锁情况在前文4.4已给出模板。再加一个更直接的组合查“当前所有阻塞源头”SELECT blocked.pid AS blocked_pid, blocking.pid AS blocking_pid, blocked.query AS blocked_query, blocking.query AS blocking_query FROM pg_stat_activity blocked JOIN pg_stat_activity blocking ON blocking.pid ANY ( SELECT unnest(pg_blocking_pids(blocked.pid)) ) WHERE blocked.wait_event_type Lock;pg_blocking_pids是官方提供的函数专门返回阻塞指定进程的PID数组这个组合查询遇到锁问题时几乎“一抓一个准”。7. 写在最后的个人体会系统表这东西初学时容易觉得“又多又乱”用久了就会发现它才是PG最迷人的地方之一。所谓“数据库管理”管的不只是数据和权限更是管这套元数据体系是否健康、是否高效、是否安全。熟练掌握pg系统表相当于给自己的运维工具箱装上了一套“X光机”很多隐性问题还没爆发成事故就能在巡检SQL中被提前发现。我个人的习惯是每到一个新PG环境第一件事就是跑一遍全库对象清单、权限清单、索引使用统计和锁等待历史。这些脚本几乎全部构建在pg_catalog之上短短几分钟就能把库的“性格”摸得清清楚楚。这套方法我用了很多年至少帮我提前发现了十几次潜在故障其中一半以上是外键缺索引和失效索引导致的性能隐患。最后分享一个小技巧在使用psql查系统表时可以多用\gset或配合元命令\watch比如反复监控某个锁等待状态时用\watch 2能每两秒刷新一次结果比手动反复执行SQL顺手得多。记住系统表不是用来背的是用来查的不是用来敬畏的是用来驾驭的。希望这篇总结能帮你在PG管理的路上少翻几次文档多省几个小时的排查时间。