作者:胖头鱼的鱼缸(尹海文) Oracle ACE Pro: Database PostgreSQL ACE 10年+数据库行业经验 拥有OCM 11g/12c/19c、MySQL 8.0 OCP、Exadata、CDP等认证 墨天轮MVP,ITPUB认证专家 圈内拥有“总监”称号,非著 名社恐(社交恐怖分子) 全网同名:胖头鱼的鱼缸 ITPUB:yhw1809 除授权转载并标明出处外,均为“非法”抄袭

最近,一个以前的客户找到我,开场就一句:现在这帮 DBA 做优化,只会加索引。
我说你先别急着发火,把表结构发我看看。发过来一看,有几张核心表,索引数量已经超过列数了。列就那么几个,索引一排一排往上堆,同一个列在三个索引里都露过面。
后果也很直接:表和索引占的空间涨得飞快,白天业务一跑 DML,明显能感觉到慢。
聊到最后,客户补了一句:其实还是怀念以前你给我们做的那套优化,从业务上改,不是从数据库上改。
这两句话听着是两件事,其实是一件事: 只会加索引,和只盯着数据库,是同一个毛病。 眼睛都在库里,没人往上看业务是怎么用库的。
索引在库里是一份独立的结构,排好序,单独占地方,跟着基表一起改。
插入一行数据,不是写一处就完了:表里写一份,表上每个索引各写一份。更新只要碰到索引列,对应的索引项就得先删再插(PostgreSQL 里不改索引列的更新走 HOT,索引可以不跟着动,这是少数派)。删除更麻烦,索引上的条目得留标记,等清理。
一处写变成一堆写,日志量跟着往上翻,缓冲池里索引页和热数据抢位置,备份、恢复、DDL、统计信息收集全按对象数量往上走。

图 1 一次 INSERT 到底写了几处
还有个不太被提的代价:索引多了,优化器可选的路径就多,估算误差一放大,计划波动更难收敛。同一条 SQL 早上一个计划、晚上另一个计划,这种事在索引堆得多的表上更常见。
索引是拿写换读。摊子铺得越大,这笔买卖越亏。
不是索引越少越好,是 少建几个、每个多干点活。要做到这一点,先得知道每个索引到底在给谁干活。
客户缺的其实不是索引,是没人把这张表上的 SQL 拉出来看过一遍。
做法不复杂:把一段时间内的语句统计抓出来(Oracle 的 AWR、PostgreSQL 的
pg_stat_statements、MySQL 的慢日志和
performance_schema 都能拿到),筛出打在这张表上的语句,再按过滤列的组合归类。
归类完,通常会看到三种东西。
第一种,一条月报 SQL 养着一个索引。这条 SQL 一个月跑一次,索引天天跟着维护。
第二种,几条 SQL 的过滤列高度重合,本来可以并进一个复合索引,结果是各建各的。
第三种,索引建完就没人用过。Oracle 可以开
dba_index_usage 的监控看,PostgreSQL 直接查
pg_stat_user_indexes 里的
idx_scan。

图 2 索引台账长什么样
一张表的索引清单,应该是这张表 SQL 清单的投影,不是 DBA 手写的记录。
索引数量是个结果,不是指标。 拿索引数量当优化产出,方向从一开始就错了。
客户那堆索引里,绝大部分是"一列一个"的单列索引,一条 SQL 配一个。这也是索引数能超过列数最典型的来路:列就那么多,一个列被拆进好几个索引里反复出现。
复合索引的道理众所周知:靠最左前缀干活。一个
(a,b,c) 的索引,能服务
a、
a+b、
a+b+c 三类条件组合,一次区间扫描加一次回表,一条索引干三份活。

图 3 (a,b,c) 能覆盖哪些条件组合
索引在物理上是按 a、然后 b、然后 c 排好的一棵树。b 没有约束住,c 在树里就是断开的:每个 b 下面各自有序,一跨过 b 就乱了。
a+c 这种条件,索引只能靠 a 定位,c 只能在索引里做过滤,跟真正拿 c 去定位差着一档。
Oracle 的 index skip scan、MySQL 8.0.13 之后的 skip scan,在前导列基数很低的时候能跳过中间列,把 c 也拿来定位。但要不要走由成本模型定,不是想用就能用;PostgreSQL 这边没有对应的能力。 别把它当通用解。
同理,a 本身是范围条件的时候(
a > 100 and b = 5),b 和 c 也只能当过滤用。列序规则里"等值在前、范围在后",根子就在这。
那几个单列索引呢?它们靠索引合并(Oracle 的索引合并与位图转换、PostgreSQL 的
BitmapAnd、MySQL 的 index merge)也能覆盖 7 种条件组合,看着更灵活。代价在三份存储、三份写,加上合并行号再回表这一步,返回行数一多就不划算了。

图 4 同样的覆盖面,两种建法的代价
一个索引干三件事,比三个索引各干一件事便宜,这在写多的表上差得更明显。
前提是这几条 SQL 的前缀真能叠在一起。叠不上的硬并,只会白存一截宽列,还不如各建各的。
列序怎么排,四条:
SQL 那一侧要跟着动。这里有个传了很多年的误解: WHERE 里条件的书写顺序,不影响走不走索引,优化器不看这个。真正要调的是索引列序,以及让 SQL 把前缀列的等值条件补齐。
具体动作就几条:业务上本来就确定的常量(租户号、机构号、状态、类型)显式带进 SQL,别让索引第一列悬空;索引列别用函数或者表达式包住;范围条件和等值条件别混在同一层;
like '%x' 这种前置通配符,直接把这一列废掉。
这几条改起来要业务配合,改 SQL、改传参、改接口。
列序不是照着建表时的字段顺序排的,是按这张表最常被怎么查排的。
说白了,复合索引像抽屉里的分隔盒,从第一格开始用,前面几格能单独使;一堆单列索引就是一堆独立小盒,每个只装一样东西,柜子很快就塞满了。
例外也得说清:唯一约束和主键该建就建;写多读极少、SQL 又杂的表,先砍数量比抠列序更划算;几列合起来的选择性远高于任何单列时,宽一点的复合索引反而更省;需要免回表的覆盖索引,热点查询值得单独建一个。
回到客户那段话的另一半。
那套优化干活的方式不一样:先看业务怎么用库,再看库里怎么执行。改的多半是用法,不是配置。

图 5 优化往下走,收益和难度一起涨
常见的几类,做过这类活的人应该都眼熟:分页用大偏移,翻到后面几页越来越慢;循环里一条一条查,一次列表页打几百次库;报表直接压在业务主库上;字段类型不一致,条件上做了隐式转换;
select * 把大字段全拉出来;时间表只增不归档,越跑越厚。
还有两个更隐蔽的。一个是编号不用序列,拿一张表存当前最大值,每次插入前先 update 加一,再把值查出来用。所有插入都挤在同一行上排队,行锁一个一个等,并发一上来就堵住。
另一个是同一件事的多条 DML 各提交各的:主表一条、明细三条,每条都 commit。一次业务操作要等四回日志同步,网络往返和落盘次数一起翻上去,业务侧还看不出哪里慢。
这些改完,一批索引的需求自己就没了。 很多索引是在替不合理的用法兜底,用法改了,兜底的东西就不需要了。
索引该清的时候也别手快。Oracle 可以先把索引改成不可见(invisible index),观察一段;PostgreSQL 没有这一手,就盯着
idx_scan,而且要跨过月结、季结、年末这些跑批周期,确认整个业务周期里都是 0 再动手。直接 drop 的那种,出了事只能重建,大表重建索引的时间不好估。
参数和索引是给现有用法打补丁,业务用法才是那块布。
这事不能全怪 DBA。
加索引是不用改代码、不用排期、不用说服业务、不用回归测试、当天就能上线的优化动作,而且一时想不出第二个。建完,语句快了,事情就算交代了。
改 SQL 呢?要跨团队,要业务确认口径,要回归测试,要排窗口,还要担一句"改坏了算谁的"。
跨团队那一步,考的不只是技术。得跟开发讲清为什么改,跟业务把口径对明白,跟领导要排期:交流能力、人际关系、流程走到哪一环,哪一样差一点都推不动。同样是一个下午,加索引能加完十个,改 SQL 可能连评审都没过。
所以"只会加索引"不一定是能力问题,是 成本问题,而且是组织里被奖励出来的行为: 它见效快、责任轻、还能立刻被看见。
总监这个称号挂我头上有些年头了,说句自嘲的:要论性价比,加索引在我见过的优化手段里排第一。
想把主动权拿回来,三件事就够:
这三件都是事后收拾,管得住增量,管不了根子。再往前一步,DBA 该进到业务的库表逻辑设计阶段去:哪些是一类实体、哪些只是扩展属性、表怎么拆、键怎么定。这一步定歪了,后面加多少索引都是在补窟窿。
川序这套东西是我自己带着 AI 一点点写出来的,功能和表也是一路加一路长:面多了加水,水多了加面,字段和表越铺越散。最后还是我一条条要求它按可扩展的实体化设计重来,才收住。AI 写表尚且这样,业务方顺手画出来的表更难说。
回到开篇那张表,目标不是把索引删干净,是 让每个索引都能说出自己服务谁、列序为什么这么排。说不出来的,就该进观察名单了。
索引数量不是优化成果。能用一个复合索引盖住几条查询的前缀,就别用三个单列去凑;该有几个、列序怎么排,取决于这张表被怎么查。
索引和参数能解决的问题有边界。省下来的大头多半在业务怎么用库上:分页怎么写、循环里怎么查、报表打哪儿、表要不要归档。这部分最费口舌,改一次能管很久。
老规矩,知道写了些啥。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/31466763/viewspace-3188654/ ,如需转载,请注明出处,否则将追究法律责任。