
Postgres Language Server duplicateIndex 规则详解如何检测与清理数据库中的重复索引【免费下载链接】postgres_lspA Language Server for Postgres项目地址: https://gitcode.com/GitHub_Trending/po/postgres_lsp本文围绕 Postgres Language Server以下简称 PLS内置的数据库级 Lint 规则duplicateIndex展开详细说明该规则能发现什么问题、其背后 SQL 查询的工作原理、如何在postgres-language-server.jsonc中配置与忽略特定对象以及如何结合 CLI 命令落地到日常的数据库体检流程中。读完本文你将掌握该规则的完整语义能够在自己的 Postgres 项目里一键发现长得一模一样的重复索引并安全清理。规则概览诊断类别、严重级别与默认行为duplicateIndex是 PLS 中 Splinter数据库级规则集里的一条性能规则其完整定义位于 duplicate-index.md。该规则的核心职责一句话即可概括检测数据库中是否存在两个或更多**完全相同identical**的索引。规则的基本元信息如下属性值规则名称诊断 codesplinter/performance/duplicateIndex诊断类别Diagnostic Categorysplinter/performance/duplicateIndex严重级别默认Warning警告所属规则组splinter/performance性能组是否推荐启用recommended是是否需要 Supabase 专用角色否在 database_rules.md 的规则索引表中duplicateIndex一行的属性列标记为✅推荐规则且不带⚡说明它属于所有普通 Postgres 数据库都能直接使用、不依赖 Supabase 环境的规则。这与源码实现一致在 duplicate_index.rs 中规则被声明为recommended: true、severity: Warning并且REQUIRES_SUPABASE: false。由于它是 recommended 规则只要在配置中没有显式关闭PLS 默认就会对数据库执行重复索引检查无需额外开启。什么才算重复索引规则的判定口径要正确使用这条规则必须先理解它对identical完全相同的界定。先看一个最简单的例子-- 对同一张表的同一列创建两个定义完全一致的索引 create table public.orders (id bigint, status text); create index orders_status_idx on public.orders (status); create index orders_status_idx2 on public.orders (status);第二个索引orders_status_idx2与第一个在列集合、排序方向、访问方法上完全一致仅仅是名字不同。它不会加速任何已有查询优化器选哪个都一样却要为每次写入付出额外的维护成本这就是duplicateIndex规则要抓的对象。从规则的 SQL 实现看判定重复的粒度是同一张表或物化视图上剔除索引名称后剩余的定义文本完全相同的索引集合。定义文本中只要有一处不同例如列顺序不同、加了WHERE条件变成部分索引、DESC排序、不同的INCLUDE列、不同的填充因子就不算重复。因此两条CREATE INDEX ... ON t (col)仅索引名不同 → 判定为重复普通索引 vs 部分索引带WHERE→ 不算重复索引 vs 唯一索引UNIQUE→ 定义文本不同不算重复不同表上的同构索引 → 不算重复分组键包含表名。这个判定口径在源码中有直接依据详见下文对 SQL 查询的分段拆解。核心机制规则背后的 SQL 查询逐段解析duplicateIndex与 PLS 的普通文件级 Lint 规则有本质区别它不解析 SQL 语法树而是对运行中的数据库直接执行一条 SQL 查询从系统目录读取真实 schema 状态。这正是 rule.rs 中SplinterRuletrait 所描述的设计——规则逻辑在 SQL 文件里而不是 Rust 代码里。规则的 SQL 源文件位于 duplicate_index.sql其查询主体如下与文档中给出的完整查询一致( select duplicate_index as name!, Duplicate Index as title!, WARN as level!, EXTERNAL as facing!, array[PERFORMANCE] as categories!, Detects cases where two ore more identical indexes exist. as description!, format( Table %s.%s has identical indexes %s. Drop all except one of them, n.nspname, c.relname, array_agg(pi.indexname order by pi.indexname) ) as detail!, https://supabase.com/docs/guides/database/database-linter?lint0009_duplicate_index as remediation!, jsonb_build_object( schema, n.nspname, name, c.relname, type, case when c.relkind r then table when c.relkind m then materialized view else ERROR end, indexes, array_agg(pi.indexname order by pi.indexname) ) as metadata!, format( duplicate_index_%s_%s_%s, n.nspname, c.relname, array_agg(pi.indexname order by pi.indexname) ) as cache_key! from pg_catalog.pg_indexes pi join pg_catalog.pg_namespace n on n.nspname pi.schemaname join pg_catalog.pg_class c on pi.tablename c.relname and n.oid c.relnamespace left join pg_catalog.pg_depend dep on c.oid dep.objid and dep.deptype e where c.relkind in (r, m) -- tables and materialized views and n.nspname not in ( _timescaledb_cache, _timescaledb_catalog, _timescaledb_config, _timescaledb_internal, auth, cron, extensions, graphql, graphql_public, information_schema, net, pgmq, pgroonga, pgsodium, pgsodium_masks, pgtle, pgbouncer, pg_catalog, pgtle, realtime, repack, storage, supabase_functions, supabase_migrations, tiger, topology, vault ) and dep.objid is null -- exclude tables owned by extensions group by n.nspname, c.relkind, c.relname, replace(pi.indexdef, pi.indexname, ) having count(*) 1)这条查询的设计意图可以拆解为五步1. 数据来源pg_indexes视图查询从pg_catalog.pg_indexes视图出发。该视图会列出数据库中每一个索引及其indexdef即CREATE INDEX的完整定义文本包括为UNIQUE等约束自动创建的底层索引。这也是为什么你在实践中偶尔会看到规则把约束索引也算进去——从规则的角度看它们同样是重复的索引。2. 关联命名空间与表pg_namespacepg_class通过pi.schemaname n.nspname关联pg_namespace拿到 schema 名再通过pi.tablename c.relname且n.oid c.relnamespace关联pg_class拿到表的元数据relkind、relname用于在输出中拼出schema.table这样的完整对象名。3. 排除系统与扩展对象where子句做了两类过滤c.relkind in (r, m)只检查普通表r和物化视图m跳过序列、视图、外部表等对象n.nspname not in (...)跳过一大串系统/托管 schema包括pg_catalog、information_schema、auth、extensions、storage、realtime、vault以及 TimescaleDB 相关的_timescaledb_*等。这些 schema 里的索引不受用户控制或属于托管服务报告它们没有实际意义left join pg_depend ... dep.objid is null利用pg_depend的依赖关系把由扩展extension拥有的表排除掉避免把扩展自动创建的索引误报为重复。4. 分组判重replace(indexdef, indexname, )这是整个判重逻辑的灵魂group by使用replace(pi.indexdef, pi.indexname, )作为分组键之一即把索引定义文本中的索引名抠掉后剩下的部分必须完全一致才算一组。同一组内再按n.nspname、c.relkind、c.relname细分确保只对同一张表/物化视图上的索引进行比较。索引名本身不参与比较这正是两个仅名字不同的索引算重复的实现来源。5. 触发条件having count(*) 1每个分组内索引数量大于 1 时就产生一条诊断。输出的indexes字段用array_agg(pi.indexname order by pi.indexname)收集该组内全部索引名并排序方便你一眼看清该保留哪个、该删哪个。查询输出字段说明规则的查询结果被 PLS 包装成诊断信息各列含义如下以!结尾的列名是 Splinter 规则输出约定的必填字段标记字段含义示例name规则内部名称duplicate_indextitle诊断标题Duplicate Indexlevel日志级别WARNfacing面向对象EXTERNALcategories所属类别数组[PERFORMANCE]description规则描述Detects cases where two ore more identical indexes exist.detail人类可读的详情Table public.orders has identical indexes {orders_status_idx,orders_status_idx2}. Drop all except one of themremediation修复指引地址官方修复文档链接见原文档 Remediation 一节metadata结构化元数据{schema:public,name:orders,type:table,indexes:[...]}cache_key结果缓存键duplicate_index_public_orders_{orders_status_idx,orders_status_idx2}其中cache_key按duplicate_index_schema_table_indexes的格式生成用于对规则结果做缓存去重避免同一对象被重复报告。诊断类别与修复文档的映射可以在 categories.rs 中看到splinter/performance/duplicateIndex对应的正是该规则的修复指引。如何配置从推荐默认到精确控制PLS 的数据库规则配置统一放在项目根目录的 postgres-language-server.jsonc 中仓库自带的示例配置已将splinter.enabled置为true。文档给出的最简配置如下把规则从默认的warn提升为error{ splinter: { rules: { performance: { duplicateIndex: error } } } }支持的级别duplicateIndex支持四个配置级别字符串形式级别含义error作为错误报告可用于 CI 强制拦截warn作为警告报告默认值info作为信息提示off完全关闭该规则从配置层的源码看splinter/rules.rsperformance组内维护了duplicateIndex字段camelCase 命名序列化时自动转换并提供了recommended与all两种组级开关。规则的默认严重级别在 rules.rs 的 severity 映射中定义duplicateIndex默认即为Warning。组级与顶层开关除了逐条配置还可以用组级或顶层开关批量控制{ splinter: { enabled: true, rules: { recommended: true, performance: { duplicateIndex: warn } } } }顶层splinter: { enabled: false }关闭整个数据库 lint 功能rules: { recommended: true }启用全部推荐规则duplicateIndex在其中rules: { all: true }启用规则组内全部规则nursery 除外显式duplicateIndex: off的优先级最高可单独剔除该规则。忽略特定对象全局与单规则两种方式真实项目中可能存在明知重复但暂时不能删的历史包袱此时不需要关闭整个规则而可以用 glob 模式精确忽略。忽略规则详见 database_linting.md模式格式为schema.object_name*匹配任意字符序列。全局忽略所有数据库规则生效{ splinter: { ignore: [ audit.*, temp_* ], rules: { // ... } } }仅对duplicateIndex生效的忽略{ splinter: { rules: { performance: { duplicateIndex: { level: warn, options: { ignore: [ public.temp_*, staging.* ] } } } } } }常用模式示例模式匹配对象public.my_tablepublicschema 中的指定表audit.*auditschema 中的所有对象*.temp_*任意 schema 中带temp_前缀的对象public.log_*publicschema 中以log_开头的表在配置层的实现中这些忽略模式会被编译为pgls_matcher::Matcher规则的ignore选项非空时构建对应的匹配器见 splinter/rules.rs 的 get_ignore_matchers 实现运行时对规则结果逐一匹配过滤。通过 CLI 执行数据库 lintduplicateIndex作为数据库级规则需要连接到运行中的 Postgres 实例才能执行因此它不在普通的文件 lint 流程里而是通过dblint命令触发。数据库连接信息同样配置在postgres-language-server.jsonc的db段host、port、database、username、password等详见 configure_database.md。# 运行全部数据库 lint 规则包含 duplicateIndex postgres-language-server dblint # 只跑重复索引这一条规则 postgres-language-server dblint --only performance/duplicateIndex # 跳过重复索引规则 postgres-language-server dblint --skip performance/duplicateIndex--only与--skip的参数格式为组名/规则名camelCase与诊断 codesplinter/performance/duplicateIndex的后两段一一对应。这样你可以把重复索引检查单独接入定时任务或 CI 流水线例如在发布前执行dblint --only performance/duplicateIndex并把规则级别提升为error以强制阻断。源码实现与代码生成流程duplicateIndex在仓库中的实现链路非常清晰体现了 PLS 数据库规则的统一架构规则源文件真相来源vendor/performance/duplicate_index.sql。文件头部以-- meta: ...注释声明规则的name、title、severity、category、description、remediation等元信息生成的 Rust 规则rules/performance/duplicate_index.rs。由xtask/codegen根据 SQL 元信息自动生成声明规则版本1.0.0、名称duplicateIndex、严重级别Warning、recommended: true并实现了SplinterRuletrait规则 traitrule.rs 定义了SplinterRule与 AST 无关、逻辑在 SQL 文件中、通过SQL_FILE_PATH定位查询文件、通过REQUIRES_SUPABASE标记是否依赖 Supabase 环境本规则为false纯标准 Postgres 即可运行注册与配置规则在 pgls_splinter 的 registry 中注册配置结构由 splinter/rules.rs 的Performance结构体承载诊断类别到修复文档的映射在 categories.rs 中维护。从源码结构可以推断新增或修改这类数据库规则的标准流程是编辑vendor/下的 SQL 文件 → 运行 codegen 重新生成 Rust 规则与配置结构 → 编译并跑测试测试入口见 pgls_splinter/tests/diagnostics.rs。修复建议安全地删除重复索引当规则命中时detail字段会明确告诉你Tableschema.tablehas identical indexes {idx_a, idx_b}. Drop all except one of them。修复思路如下确认保留哪一个通常保留名字更规范、被约束引用如UNIQUE约束索引或历史更久的一个逐一删除其余重复索引drop index public.orders_status_idx2;删除后复查再次运行postgres-language-server dblint --only performance/duplicateIndex确认诊断消失注意反向依赖如果重复索引被UNIQUE/PRIMARY KEY约束所依赖DROP INDEX会失败此时需要改为ALTER TABLE ... DROP CONSTRAINT或先处理约束关注写放大成本每多一个索引INSERT/UPDATE/DELETE都要多维护一棵 B-tree。清理重复索引不仅能消除冗余存储还能降低写入路径的开销——这正是它被归类为performance规则的原因。常见误区与边界情况UNIQUE约束产生的索引也会被报告pg_indexes包含约束索引若一张表上同时有UNIQUE (col)约束和手工创建的CREATE UNIQUE INDEX ON t (col)定义文本一致时会被判为重复。处理时应优先保留约束DROP INDEX直接删手工索引即可部分索引不会误报带WHERE的 partial index 与普通索引定义文本不同不会互相匹配大小写与空白敏感判重基于indexdef文本扣除索引名后的完全一致因此ON t (a, b)与ON t (b, a)、或文本中存在细微空白的两个定义都不会被视为重复——规则判定的是真正相同而不是语义等价仅限表与物化视图relkind in (r, m)决定了分区父表、外部表、视图等对象不在检查范围内无需 Supabase该规则对标准 Postgres 直接可用不需要anon/authenticated等 Supabase 专用角色也不要求 PostgREST 等扩展存在。小结duplicateIndex是 PLS 数据库 lint 体系中一条开箱即用recommended的性能规则通过一段精心设计的目录查询能够在秒级完成对全库重复索引的扫描并以结构化的诊断信息给出可操作的删除建议。它在架构上是 Splinter 数据库规则的一个典型样本SQL 定义规则逻辑、Rust 负责元数据与注册、配置层负责级别与忽略策略、CLI 负责执行入口。对于追求 Postgres 写入性能与整洁 schema 的团队把它接入日常dblint检查或 CI 流程是成本极低、收益明确的数据库治理手段。相关资源规则参考文档docs/reference/rules/duplicate-index.md数据库规则总览docs/reference/database_rules.md数据库 lint 使用指南docs/features/database_linting.md规则 SQL 源文件crates/pgls_splinter/vendor/performance/duplicate_index.sql生成的 Rust 规则crates/pgls_splinter/src/rules/performance/duplicate_index.rsSplinter 规则 traitcrates/pgls_splinter/src/rule.rs配置结构定义crates/pgls_configuration/src/splinter/rules.rs示例配置文件postgres-language-server.jsonc【免费下载链接】postgres_lspA Language Server for Postgres项目地址: https://gitcode.com/GitHub_Trending/po/postgres_lsp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考