维护基于 PostgreSQL 内核的数据库产品时几乎每天都会和各种系统表打交道而 PG_CLASS 是其中最绕不开的一张。HIGHGO 这个名字是我对某个基于 PG 内核的数据库项目的代号它的目录体系和原生 PostgreSQL 保持一致所以很多排查思路可以直接套用。这篇文章会把 PG_CLASS 的字段、原理、实战查询和容易踩的坑完整拆一遍适合刚开始接触数据库内核、或者被各类元数据查询搞到头秃的 DBA 和开发同学。1. HIGHGO 里的 PG_CLASS 到底是什么角色1.1 先从一个常见场景说起假设你刚接手一套数据库拿到账号的第一天老板让你快速汇报实例里有多少表、多少视图、哪些表已经膨胀到占了好几个 GB。大部分人第一反应是打开图形客户端一个一个看或者翻监控平台。但真正干这行的都知道最快的方式是在 psql 里跑一条查询直接查 PG_CLASS。这张表就像户籍科的档案柜库里所有表、索引、视图、序列、物化视图等对象的“户口信息”全部登记在里面。它不存业务数据只存数据字典层面的元数据。PG_CLASS 之所以重要是因为几乎所有数据库变动都会先反映到这里建表、删表、加索引、改字段、甚至调整访问权限都需要在 PG_CLASS 里留下记录。它既是数据库对象的管理入口也是排查问题的第一现场。1.2 PG_CLASS 和其他系统表的分工PG_CLASS 不是单打独斗。PostgreSQL 的目录体系是一整套互相引用的表PG_NAMESPACE 记录命名空间也就是我们常说的 schemaPG_ATTRIBUTE 记录每个表的字段列表PG_TYPE 记录类型信息PG_INDEX 记录索引的键PG_DEPEND 记录对象之间的依赖PG_ROLES 负责用户和角色。PG_CLASS 处在这些表的中心位置用 OID 和它们关联。打个比方PG_CLASS 是人的身份证主卡PG_NAMESPACE 告诉你这个人户籍在哪个区PG_ATTRIBUTE 列出他身上带的所有证件PG_TYPE 说明这些证件的类型PG_DEPEND 则记录他和家庭成员之间的法律关系。实际查询时我们经常要把多张系统表 join 在一起才能得到完整的答案。整个系统表都放在名为 pg_catalog 的 schema 里它是数据库初始化时就创建的对任何连接都可见。HIGHGO 这类基于 PostgreSQL 的项目一般不会轻易改动这套目录结构但在某些兼容场景下可能会挂载额外系统扩展这一点稍后会提到。1.3 哪些对象会写进 PG_CLASSPG_CLASS 里并不是只有我们平时创建的普通表。按照 relkind 字段的不同取值它还能区分出很多对象类型普通表、索引、序列、视图、物化视图、复合类型、TOAST 表、外部表、分区表等等都会以独立行存在。只要这个对象在数据库目录中“注册”了PG_CLASS 里就会有对应的一行记录。一个容易忽略的点是PG_CLASS 只负责登记对象存在和它的基本信息不直接保存业务数据。业务数据是写在堆表文件里的而不是写在 PG_CLASS 里。如果把数据库比作一栋大楼PG_CLASS 就是物业处的登记簿它告诉你哪一家住了谁、谁是户主、房子多大但不会替你摆家具。2. 逐字段拆解一张表说清 PG_CLASS 能查什么2.1 先把字段按职责分个组PG_CLASS 拥有几十个字段全部硬背没有意义。按我的习惯把它们分成四组身份归属组、物理存储组、统计优化组、权限扩展组。这样遇到具体需求时能快速判断该看哪一列。分组核心字段用途简述身份归属oid, relname, relnamespace, relowner, relkind对象是谁、叫什么、归哪里管物理存储relfilenode, reltablespace, relpages, reltoastrelid对象的数据存在哪、占多少个页统计优化reltuples, relallvisible, relhasindex, relnatts估算行数、可见页数、结构规模权限扩展relacl, reloptions, relpersistence, relispartition访问权限、存储参数、持久性属性实际开发中最常用的字段是 oid、relname、relnamespace、relkind、relowner、relfilenode、relpages、reltuples。时间长了你会发现80% 的排查需求靠这几个字段就能完成。2.2 relkind 一眼定类型每个数据库对象在 PG_CLASS 里对应一行究竟是哪种对象就看 relkind。这张取值表建议存下来排查时经常要用。relkind 取值含义备注r普通表最常见的堆表i索引包括普通索引和部分专用索引S序列自增序列对象v视图逻辑视图m物化视图实体化存储的视图c复合类型由表结构自动生成的行类型tTOAST 表变长字段的辅助存储表f外部表外表数据存在外部数据源p分区表分区父表I分区索引分区父表的索引注意分区表和分区的子表有所区别。分区父表的 relkind 是 p而实际的分区子表仍然是 r。如果你统计表数量时把 p 和 r 混在一起数字很容易看糊涂。2.3 容易踩坑的几个隐藏字段relfilenode 是很多新手会忽略的字段。它决定对象在磁盘上的物理文件名。正常表的 OID 和 relfilenode 大多数情况下是同一个数字但经过 VACUUM FULL、CLUSTER、扩容重建之后relfilenode 会被重新分配OID 是不变的。所以要对应物理文件必须看 relfilenode不能想当然按照 OID 去找。reltoastrelid 用起来也有讲究。它指向该表关联的 TOAST 表的 OID。包含 text、varchar 等变长字段的表通常会自动创建一张 TOAST 表用来存储超出页大小的数据。日常巡检大表时如果不把 TOAST 表算进去大小统计会少一大截。relpersistence 字段决定表的持久性。p 代表永久表t 代表临时表u 代表 UNLOGGED 表。UNLOGGED 表不写 WAL 日志崩溃后数据会丢但写入性能快不少。有时候业务表莫名其妙丢了数据排查完发现是 UNLOGGED 表就是没看这个字段。relacl 字段存储的是访问控制列表ACL长得像这样{ownerarwdDxt/owner,user1arwdDxt/owner}。其中每个字母对应一种权限r 表示 SELECTw 表示 UPDATEa 表示 INSERTd 表示 DELETED 表示 TRUNCATEx 表示 REFERENCESt 表示 TRIGGER。直接读字符串很痛苦平时建议配合 has_table_privilege 函数来查而不是硬解析文本。3. 实战把 PG_CLASS 高频查询直接跑起来3.1 一个命令摸清 schema 资产全貌初到一个环境先做资产盘点。下面的 SQL 能快速列出 public schema 下所有对象以及类型分布。SELECT c.relkind, count(*) AS object_count FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid c.relnamespace WHERE n.nspname public GROUP BY c.relkind ORDER BY c.relkind;结果会按 r、i、v、S、m 等类型分别给出数量瞬间知道 schema 下有几张表、几个视图、多少索引。这里要养成习惯给系统表加上 pg_catalog. 前缀避免因为 search_path 设置导致匹配到同名自定义表。虽然 pg_catalog 在搜索路径中通常优先级很高但显式写出来永远不会出错。3.2 按大小排序找出 TOP 大表找出占用空间最大的表是 DBA 的常规动作。PG_CLASS 里的 relpages 表示这张表在磁盘上占用的数据页数量默认每页 8KB。直接把页数乘以 8192 字节再除以 1048576就能得到以 MB 为单位的估算大小。SELECT schemaname, relname, relpages, reltuples, round(relpages * 8 * 1024 / 1048576.0, 2) AS size_mb FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid c.relnamespace LEFT JOIN pg_catalog.pg_stat_user_tables s ON s.schemaname n.nspname AND s.relname c.relname WHERE n.nspname public AND c.relkind r ORDER BY relpages DESC LIMIT 20;用这种方式排序的结果是“估算值”它的精度取决于上次 ANALYZE 的状态。要拿到精确大小应该用 pg_total_relation_size() 函数它会把表数据、索引、TOAST 全部算进去。不过 PG_CLASS 的优势在于快一条语句扫完整个实例不用逐个对象调用函数。3.3 抓出长期不增长的僵尸对象有时候我们需要找出长期没有写入、没有更新的表用来决定是否可以归档或下线。PG_CLASS 的 reltuples 字段是上次统计后的估算行数配合统计视图中最后一次写入时间能快速筛出疑似僵尸表。SELECT n.nspname AS schema_name, c.relname AS table_name, c.reltuples AS estimated_rows, s.last_analyze FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid c.relnamespace JOIN pg_catalog.pg_stat_user_tables s ON s.schemaname n.nspname AND s.relname c.relname WHERE c.relkind IN (r, p) AND c.relnamespace NOT IN (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname LIKE pg_toast%) ORDER BY c.reltuples DESC;注意业务表如果长期没有写入reltuples 可能还保留着历史最高值所以必须结合 last_analyze 或统计视图中 updates、deletes 等指标一起判断不能只看一个字段就下结论。3.4 用 PG_CLASS 查找权限异常的对象权限排查是运维高发场景。比如业务反馈无法查询某张表但检查了半天找不到原因。此时用 PG_CLASS 里的 relacl 能快速定位问题。默认情况下如果 relacl 为空表示对象使用默认权限也就是只有对象所有者有权限。如果 relacl 里有类似 “user1arwdDxt/user1” 的记录说明这个用户被显式授予了权限。SELECT c.relname, n.nspname, c.relowner::regrole AS owner, c.relacl FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid c.relnamespace WHERE n.nspname public AND c.relacl IS NOT NULL AND c.relkind r;直接解析 relacl 文本虽然可行但不够优雅。更可靠的写法是结合 has_table_privilege() 判断某个指定角色有没有特定权限例如SELECT c.relname, has_table_privilege(some_user, c.oid, SELECT) AS can_select, has_table_privilege(some_user, c.oid, UPDATE) AS can_update FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid c.relnamespace WHERE n.nspname public AND c.relkind r;如果返回的是 false权限问题基本就坐实了再顺着去查 GRANT 语句或角色继承关系即可。4. 避坑实录查系统表最容易翻车的几个现场4.1 reltuples 不准先别急着骂我曾经排查过一个慢 SQL优化器死活不走大表字段上的索引explain 显示表行数只有几百行实际上已经上百万。问题出在 reltuples 是统计数据不是实时值它在上一次 VACUUM、ANALYZE 或 CLUSTER 之后才会刷新。很久没跑统计的表reltuples 严重滞后优化器拿到假的估算值自然选错执行计划。遇到这种场景先执行 ANALYZE 再重看计划。如果你是经常遇到这种问题建议把 autovacuum 的分析阈值调低一点或者对关键表建立定时 ANALYZE 任务。不要试图去 UPDATE PG_CLASS 里的 reltuples这是一种非常危险的越权操作后面会单独说。4.2 不要把 OID 当成永久身份证明PG_CLASS 里的 OID 是对象在数据库内部使用的逻辑编号。看起来稳定但在某些操作后会变化比如数据库进行逻辑迁移、pg_dump 再 restore 之后OID 可能会重新分配。如果哪个脚本里硬编码了 “WHERE oid 54321” 这样的条件换环境之后大概率查不到数据。正确做法是使用 REGCLASS 类型转换例如将表名直接转成 OIDSELECT mytable::regclass::oid;。这样既保证可读性又能拿到当前的正确 OID。需要说明的是OID 与 relfilenode 的关系同样不绝对稳定。当执行 VACUUM FULL 时relfilenode 会被重置但 OID 不变这两者在物理层面的对应关系也要靠查询而不是记忆。4.3 不要轻易手动修改 PG_CLASS这是系统表使用的铁律绝对不要在业务环境里直接 UPDATE、DELETE、INSERT PG_CLASS。数据库目录表在启动阶段就会被加载到内存中系统表结构被修改后轻则造成对象不一致重则直接崩溃无法启动。很多“我改了 pg_class 里一个字段库启动不了了”的求助帖多半都是这里出了问题。如果需要修改对象的存储参数、持久性、权限请使用标准的 SQL 或命令例如 ALTER TABLE、ALTER INDEX、GRANT、VACUUM FULL 等。它们会经过内核所有的校验和关联处理安全得多。把 PG_CLASS 当成只读视图来使用是底线。4.4 看不到对象不代表对象不存在另一种常见问题是普通用户查询 PG_CLASS 时发现少了很多表。不是因为表不存在而是系统的元数据可见性受到权限控制。SQL 标准要求用户只能看到自己拥有或有权限访问的对象。如果当前角色不是超级用户查询 PG_CLASS 时最好用pg_has_role或相关权限函数过滤一下否则容易被误导。对于只有部分 schema 权限的账号查询 PG_CLASS 可以考虑加上pg_table_is_visible()条件这样只返回当前搜索路径中可见的对象更贴近实际场景。4.5 relfilenode 与磁盘文件的对应关系遇到表文件损坏或需要手工清理孤儿文件的时候relfilenode 是最重要的线索。默认表空间下数据文件路径一般是 base/数据库OID/relfilenode。如果表存放在自定义表空间则路径会变成 pg_tblspc/表空间OID/数据库OID/relfilenode。找到 relfilenode 之后用操作系统的 find 命令找对应文件能快速定位物理文件。需要特别注意的是TOAST 表和索引也都有自己的 relfilenode它们在磁盘上同样是独立文件。所以查看一张大表的文件占用不能只看主表文件还要把父表关联出的 TOAST 文件算进去。5. 从 PG_CLASS 开始做数据字典监控5.1 建一张快照表做长期对比系统表最有价值的使用方式是做周期性快照。我习惯建一张独立的数据字典表定期从 PG_CLASS 采集对象信息存成历史记录用于日后的对比分析。这样哪天业务方突然说“某个表消失了”直接查快照就能定位到具体变化时间点。CREATE TABLE highgo_dict_snapshot ( snap_time timestamptz NOT NULL, obj_oid oid NOT NULL, obj_name text NOT NULL, nsp_name text NOT NULL, relkind char, relowner name, relpages bigint, reltuples real, relfilenode oid );每次采集时把当前 PG_CLASS 数据插入进去后续用差异查询就能发现新增对象或删除对象。当然快照表本身也会占用空间存重点对象的行数就够用不用把整个 PG_CLASS 全量复制到库里导致二次膨胀。5.2 结合统计视图做容量预估单看 PG_CLASS 的行数意义有限更实用的是把它和 PG_STAT 系列视图联合起来。我常用的一个方法是按天采集 relpages 和 reltuples然后计算增长斜率。斜率持续为正且数值很大的表基本可以判定为高速增长表需要提前规划扩容或归档。SELECT date_trunc(day, snap_time) AS stat_day, sum(relpages) * 8 * 1024 / 1048576.0 AS total_mb FROM highgo_dict_snapshot WHERE relkind r GROUP BY 1 ORDER BY 1;这个结果能画出整个实例数据量增长曲线比监控平台更接近物理真实情况因为它是直接从数据字典里读的页数。5.3 给刚开始用 PG_CLASS 的人一些习惯用了这么多年系统表我给自己定了几条小规矩一是查询系统表永远写全 schema 前缀二是只用只读方式不用 UPDATE 去“修数据”三是不随便在博客或脚本里暴露真实数据库对象的 OID 和具体结构遇到示例统一用匿名方式呈现四是所有关键对象的历史变化都留快照别靠记忆。PG_CLASS 虽然只是一张系统表但把它的字段吃透再掌握几个固定查询套路日常的元数据排查效率会提升一大截。尤其是遇到那种“莫名其妙的表变多了”“某张表没有权限查询”“磁盘空间被神秘文件占满”的疑难杂症时回 PG_CLASS 看一遍往往比翻半小时日志更直接。