
postgres_lsp 数据库 Linter 之 tableBloat 规则基于统计信息估算 PostgreSQL 表膨胀并驱动 vacuum 维护【免费下载链接】postgres_lspA Language Server for Postgres项目地址: https://gitcode.com/GitHub_Trending/po/postgres_lsp本文基于 postgres_lspPostgres Language Server数据库 linterSplinter 引擎中的tableBloat性能规则展开。该规则通过对pg_stats、pg_class、pg_namespace等系统目录的统计信息建模估算每张堆表heap table的膨胀率与空间浪费量并在膨胀率超过阈值时输出splinter/performance/tableBloat诊断提示你执行VACUUM FULL或CLUSTER以及调优 autovacuum 参数。读完本文你将掌握该规则的完整 SQL 计算原理、触发阈值、诊断输出结构、配置文件写法以及通过 CLIdblint运行与跳过该规则的具体方法。规则一览基本信息tableBloat是数据库 linterSplinter 引擎内置的一条性能类规则其核心元数据如下属性值规则名称camelCasetableBloat规则名称SQL 结果中的nametable_bloat诊断分类Diagnostic Categorysplinter/performance/tableBloat严重级别默认 SeverityInfoInformation所属分组performance性能报告级别SQL 输出levelINFO输出朝向facingEXTERNAL类别categories[PERFORMANCE]是否推荐启用recommended是recommended: true见 crates/pgls_splinter/src/rules/performance/table_bloat.rs是否依赖 Supabase 角色否REQUIRES_SUPABASE false可在任意标准 PostgreSQL 实例上运行规则版本1.0.0该规则的作用正如其描述所说检测某张表是否出现了过度膨胀excess bloat以及是否可以从VACUUM FULL或CLUSTER这类维护操作中获益。需要强调的是它与基于 AST 解析 SQL 文件的文件级 linter 不同属于 Splinter 引擎中一类直接连库执行 SQL 查询的规则——规则逻辑本身就是一个 SQL 查询文件而不是 Rust 代码详见 crates/pgls_splinter/src/rule.rs 中SplinterRuletrait 的注释They execute SQL queries against the database。在规则索引文档 docs/reference/database_rules.md 中tableBloat被列为推荐启用✅的性能规则之一。什么是表膨胀Table Bloat在深入 SQL 之前先厘清概念。PostgreSQL 采用MVCC多版本并发控制实现并发隔离每次UPDATE或DELETE都不会原地改写或移除旧行而是写入一个新版本旧版本行保留在数据页中直到被VACUUM清理。由此造成两个后果死元组dead tuple堆积表占用的磁盘空间远大于实际有效数据所需的空间碎片化与页内空洞频繁更新还会造成页内空间碎片即使VACUUM普通回收清除了死元组页内布局也不会重新紧凑排列。tableBloat规则的目标正是量化这种空间浪费它先根据统计信息估算如果表被完全重写、每页都紧凑排布理想情况下应该占用多少页otta再与实际页数relpages比较得出膨胀倍数与浪费的字节数。核心 SQL 查询逐段拆解规则的实际 SQL 存放在 crates/pgls_splinter/vendor/performance/table_bloat.sql文档 docs/reference/rules/table-bloat.md 中收录了同一份查询。SQL 文件头部通过-- meta:注释声明了规则的 name、title、severity、category、description 与 remediation这些元数据与 Rust 侧declare_rule!生成的定义保持一致。整个查询是一个 CTE 链从内到外依次为constants→bloat_info→table_bloat→bloat_data→ 最终select。第一步constants —— 常量基准with constants as ( select current_setting(block_size)::numeric as bs, 23 as hdr, 4 as ma )bs从运行参数block_size动态读取页大小通常为 8192 字节hdr23表示每个数据页的页头page header估算字节数ma4表示元组对齐MAXALIGN的字节数。这些常量是后续所有估算公式的基准。第二步bloat_info —— 基于 pg_stats 估算每行实际占用bloat_info as ( select ma, bs, schemaname, tablename, (datawidth (hdr ma - (case when hdr % ma 0 then ma else hdr % ma end)))::numeric as datahdr, (maxfracsum * (nullhdr ma - (case when nullhdr % ma 0 then ma else nullhdr % ma end))) as nullhdr2 from ( select schemaname, tablename, hdr, ma, bs, sum((1 - null_frac) * avg_width) as datawidth, max(null_frac) as maxfracsum, hdr ( select 1 count(*) / 8 from pg_stats s2 where null_frac 0 and s2.schemaname s.schemaname and s2.tablename s.tablename ) as nullhdr from pg_stats s, constants group by 1, 2, 3, 4, 5 ) as foo )这一层完全基于pg_stats的列统计信息datawidth sum((1 - null_frac) * avg_width)对每个列用非空比例 × 平均宽度累加估算一行非空数据的平均字节数maxfracsum max(null_frac)取所有列中最大的 NULL 比例用于估算 NULL 位图的开销nullhdr估算 NULL 位图null bitmap占用——固定hdr基础上每 8 个可空列增加 1 字节1 count(*) / 8外层把datawidth加上页头与对齐填充得到datahdr单行含头部后的总占用把 NULL 比例折算进nullhdr2NULL 行相关的额外占用。注意这里对每个表只输出一行按schemaname, tablename分组也就是说avg_width、null_frac等列级统计量被聚合到了表级。第三步table_bloat —— 估算理想页数 ottatable_bloat as ( select schemaname, tablename, cc.relpages, bs, ceil((cc.reltuples * ((datahdr ma - (case when datahdr % ma 0 then ma else datahdr % ma end)) nullhdr2 4)) / (bs - 20::float)) as otta from bloat_info join pg_class cc on cc.relname bloat_info.tablename join pg_namespace nn on cc.relnamespace nn.oid and nn.nspname bloat_info.schemaname and nn.nspname information_schema where cc.relkind r and cc.relam (select oid from pg_am where amname heap) )关键点ottaOptimal Tuples per Table Area理想页数ceil(reltuples × 单行总占用 / (bs - 20))。reltuples是pg_class中缓存的估算行数bs - 20扣掉了页尾的元组指针区line pointer开销。otta 即如果表被紧凑重写理论应占用的页数只处理堆表relkind r普通表且访问方法为heap通过pg_am校验amname heap因此索引、视图、物化视图、分区表等不会被误判排除information_schema系统 schema 不在检查范围内。第四步bloat_data —— 计算膨胀率与浪费字节bloat_data as ( select table as type, schemaname, tablename as object_name, round(case when otta 0 then 0.0 else table_bloat.relpages / otta::numeric end, 1) as bloat, case when relpages otta then 0 else (bs * (table_bloat.relpages - otta)::bigint)::bigint end as raw_waste from table_bloat )bloat膨胀倍数 实际页数relpages÷ 理想页数otta保留 1 位小数。例如bloat 2.0表示表实际占用页数是理想情况的 2 倍。当otta 0无统计信息时置为0.0避免除零raw_waste浪费字节数(relpages - otta) × bs。若实际页数小于理想页数统计偏差则按 0 处理同时输出对象类型table供后续元数据组装使用。第五步最终 select —— 组装诊断输出与触发阈值select table_bloat as name!, Table Bloat as title!, INFO as level!, EXTERNAL as facing!, array[PERFORMANCE] as categories!, Detects if a table has excess bloat and may benefit from maintenance operations like vacuum full or cluster. as description!, format( Table %s.%s has excessive bloat, bloat_data.schemaname, bloat_data.object_name ) as detail!, Consider running vacuum full (WARNING: incurs downtime) and tweaking autovacuum settings to reduce bloat. as remediation!, jsonb_build_object( schema, bloat_data.schemaname, name, bloat_data.object_name, type, bloat_data.type ) as metadata!, format( table_bloat_%s_%s, bloat_data.schemaname, bloat_data.object_name ) as cache_key! from bloat_data where bloat 70.0 and raw_waste (20 * 1024 * 1024) -- filter for waste 200 MB order by schemaname, object_name触发阈值双重条件缺一不可该规则只有当同时满足以下两个条件时才输出诊断膨胀率阈值bloat 70.0。即实际页数超过理想页数的70 倍。这是一个相当保守的门槛只有膨胀极其严重的表才会命中——普通程度的膨胀并不会触发浪费量阈值raw_waste (20 * 1024 * 1024)。即浪费字节数必须大于 20 × 1024 × 1024 字节。需要指出的是SQL 中的注释写的是filter for waste 200 MB但按字面计算20 * 1024 * 1024 20,971,520字节即20 MB注释文字与实际数值存在出入读者在实际解读时请以表达式计算值为准。order by schemaname, object_name保证输出按 schema 与表名排序结果稳定可复现。诊断输出字段带!后缀的列SQL 返回的每一行都带有!后缀的列名它们直接映射到 Rust 侧SplinterQueryResult结构体的字段见 crates/pgls_splinter/src/query.rs其中注释说明!表示 NOT NULL。完整字段如下SQL 列名含义在本规则中的值name!规则唯一标识snake_casetable_bloattitle!规则人类可读标题Table Bloatlevel!严重级别INFO对应 Rust 侧Severity::Informationfacing!输出朝向EXTERNALcategories!类别数组[PERFORMANCE]description!规则通用描述Detects if a table has excess bloat…detail!本次具体违规信息Tableschema.tablehas excessive bloatremediation!修复建议Consider running vacuum full…metadata!结构化对象元数据JSONB{schema: ..., name: ..., type: table}cache_key!该问题的唯一缓存键table_bloat_schema_table这些字段在 crates/pgls_splinter/src/convert.rs 中被转换为最终的SplinterDiagnosticmetadata!中的schema/name/type会被抽取为DatabaseObjectOwned { schema, name, object_type }供编辑器跳转、批量忽略等场景使用level!字符串则通过parse_severity映射为SeverityINFO→InformationWARN→WarningERROR→Error。修复建议Remediation规则给出的官方修复建议原文为Consider running vacuum full (WARNING: incurs downtime) and tweaking autovacuum settings to reduce bloat.即考虑执行VACUUM FULL警告会引发停机/锁表并调整 autovacuum 设置来减少膨胀。同时规则描述中也将CLUSTER列为可获益的维护操作。实际维护时通常的组合手段包括VACUUM FULL重写整张表并紧凑排列数据能彻底消除页内碎片与空洞但会获取ACCESS EXCLUSIVE锁、阻塞读写且需要与表大小相当的额外磁盘空间——所以规则明确给出停机警告建议在维护窗口执行CLUSTER按指定索引的顺序重写表既回收膨胀又顺便整理物理顺序同样需要排他锁调整 autovacuum 参数调高autovacuum_vacuum_scale_factor/autovacuum_vacuum_threshold之外还应关注autovacuum_naptime、autovacuum_vacuum_cost_limit等使普通VACUUM更积极、更频繁地回收死元组从源头抑制膨胀积累对超大表也可使用pg_repack一类在线重写工具规则文档未涉及属于社区实践此处仅作提示不在仓库证据范围内。配置方式Configuration基础开关与级别调整在postgres-language-server.jsonc中通过splinter.rules.performance.tableBloat配置该规则配置结构定义见 crates/pgls_configuration/src/splinter/rules.rs 中Performance结构体的table_bloat字段。将默认的Info提升为error{ splinter: { rules: { performance: { tableBloat: error } } } }也可设为warn/info或用off关闭该规则。仓库根目录的示例配置 postgres-language-server.jsonc 展示了全局骨架splinter.enabled: true、db连接信息等。分组与全局开关由于tableBloat属于performance分组RuleGroup::Performance分组常量GROUP_NAME performance你还可以{ splinter: { rules: { recommended: true, performance: { recommended: true, all: true } } } }splinter.rules.recommended默认true启用推荐规则集tableBloat在推荐集中见 crates/pgls_configuration/src/splinter/rules.rs 的RECOMMENDED_RULES_AS_FILTERSperformance分组 7 条规则全部属于推荐集performance.all启用该分组全部规则配置解析逻辑as_enabled_rules/collect_preset_rules会以显式规则配置优先、推荐集兜底、禁用集剔除的方式计算最终启用的规则集合。按规则忽略对象如果某些表确实不需要检查例如日志表、临时表本身膨胀是常态可以利用规则级ignore选项用 Unix 风格 glob 按schema.object_name格式匹配详见 docs/features/database_linting.md{ splinter: { rules: { performance: { tableBloat: { level: warn, options: { ignore: [ public.log_*, audit.* ] } } } } } }从源码看ignore模式会经pgls_matcher::Matcher编译为匹配器见 crates/pgls_configuration/src/splinter/rules.rs 的get_ignore_matchers对命中的数据库对象跳过该规则。audit.*匹配 audit schema 下全部对象public.log_*匹配 public schema 下以log_开头的表。通过 CLI 运行dblinttableBloat作为数据库 linter 规则可以通过 CLI 的dblint子命令对目标数据库执行# 运行全部数据库 lint 规则含 tableBloat postgres-language-server dblint # 只运行 tableBloat postgres-language-server dblint --only performance/tableBloat # 跳过 tableBloat例如迁移期已知的膨胀表较多时 postgres-language-server dblint --skip performance/tableBloat上述用法见 docs/features/database_linting.md 的 CLI Usage 小节--skip performance/tableBloat正是文档中的示例。CLI 依赖postgres-language-server.jsonc中的db连接配置host/port/database/username/password详细参数可查阅 docs/reference/cli.md。源码实现链路SQL 如何变成诊断从仓库源码可以完整还原tableBloat的执行链路规则声明declare_rule!宏在 crates/pgls_splinter/src/rules/performance/table_bloat.rs 生成TableBloat类型实现SplinterRuletrait提供SQL_FILE_PATH performance/table_bloat.sql、DESCRIPTION、REMEDIATION、REQUIRES_SUPABASE false四个常量SQL 加载crates/pgls_splinter/src/registry.rs 中的get_sql_content(tableBloat)通过include_str!在编译期把vendor/performance/table_bloat.sql嵌入二进制get_sql_file_path已标记 deprecatedSQL 改为编译期内嵌以保证二进制自包含、运行期零 IO结果映射查询返回的带!列由sqlx::FromRow填充进SplinterQueryResultcrates/pgls_splinter/src/query.rs诊断转换FromSplinterQueryResult for SplinterDiagnosticcrates/pgls_splinter/src/convert.rs解析level!与metadata!并通过registry::get_rule_category(table_bloat)将 snake_case 名称映射为分类splinter/performance/tableBloat该分类同时也注册在 crates/pgls_diagnostics_categories/src/categories.rs其 URL 映射表中该规则的 remediation 与规则文档一致严重级别覆盖若用户在配置中显式设置了级别Rules::get_severity_from_code会以配置为准否则回落到规则默认值Severity::Informationcrates/pgls_configuration/src/splinter/rules.rs 的Performance::severity(tableBloat)。一句话概括tableBloat 的逻辑 100% 由 SQL 承载Rust 侧只负责加载 SQL → 连库执行 → 把结果行转成诊断这也正是 Splinter 类规则与文件级 linter 规则的根本区别后者基于 AST 执行前者基于数据库查询执行。小结tableBloat是 postgres_lsp 数据库 linter 中一条基于统计估算、面向维护决策的性能规则它用pg_stats的列统计 pg_class的relpages/reltuples建立理想页数 otta模型以relpages / otta量化膨胀倍数、(relpages - otta) × bs量化浪费字节只有同时满足bloat 70.0与raw_waste 20 MBSQL 注释写作 200 MB以表达式计算值为准才报告误报面极小触发后给出INFO级别诊断建议VACUUM FULL含停机警告或CLUSTER并配合 autovacuum 参数调优在postgres-language-server.jsonc中通过splinter.rules.performance.tableBloat配置级别与ignore忽略列表亦可通过postgres-language-server dblint --only/--skip performance/tableBloat在 CLI 中精确控制。掌握该规则意味着你可以把哪张表该重写了从人工巡检变成自动化诊断——这正是数据库 linting 的价值所在。相关配套资料可继续阅读docs/features/database_linting.md数据库 linting 总览、docs/reference/database_rules.md全部规则索引以及 docs/reference/cli.mdCLI 参考。【免费下载链接】postgres_lspA Language Server for Postgres项目地址: https://gitcode.com/GitHub_Trending/po/postgres_lsp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考