news 2026/10/3 12:34:59

胖头鱼的技术专栏-471 数据库优化的三层楼——为什么多数人总在第二层打转(20261002)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
胖头鱼的技术专栏-471 数据库优化的三层楼——为什么多数人总在第二层打转(20261002)

数据库管理471期 2026-10-02

  • 胖头鱼的技术专栏-471 数据库优化的三层楼——为什么多数人总在第二层打转(20261002)
    • 一、一层:参数是地基,交付那天就得浇
    • 二、同一个库,跑了一年之后
    • 三、二层:加索引,最容易出成绩的一步
    • 四、每个索引,都要还的账
    • 五、把一张表的 SQL 摊开,再谈索引
    • 六、三层:壮汉挤门
    • 七、顺序其实是反的:先看业务,最后看参数
    • 八、总结

胖头鱼的技术专栏-471 数据库优化的三层楼——为什么多数人总在第二层打转(20261002)

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

前阵子在客户现场,跟一位客户的 DBA 聊他们那套库为什么慢。聊下来十分钟,他判断问题的方式是这样排的:

“内存是不是不太够?先加上去。”
“这条 SQL 是全表扫描,加个索引。”
“那个参数要不要调一下,我查过别人写的文档。”

熟悉得让人心疼。这三句话几乎是很多数据库优化的标准起手式——不是因为它们效果最好,而是因为它们的成果最容易看见、最容易拿到手。

我干了十年多数据库,见过太多优化是这样收场的:内存加到位了,索引加上去了,参数也照着某份文档调过一轮,报告写得漂漂亮亮,两个月后业务量或者数据量涨了一截,慢的还是慢。

问题不在这些动作错——它们都对,区别在于它们是三层楼里的两层,而且很多人一辈子没爬过第三层。

这篇文章想聊的就是这三层:参数、索引、业务。以及一个我自己越来越笃定的判断——真正决定这套库能跑多快的,往往是最不愿意被碰的那一层。

一、一层:参数是地基,交付那天就得浇

先说第一层。

我一直坚持一个观点:**参数不是调优旋钮,是交付前的施工图纸。**它应该在数据库搭建阶段就基本定下来,而不是上线之后慢慢试。

道理很简单——有些东西,一旦定下去就很难改了:

  • 字符集、大小写敏感与否、日期精度:这类决定了数据怎么存,改一遍基本等于全量导出导入
  • 数据块 / 页大小:直接决定一次 IO 拿多少数据、内存里怎么组织,几乎所有数据库都不支持原地改
  • 重做日志的大小和组数:太小,日志切换频繁到能把业务拖出周期性卡顿;上线后改当然能改,但代价远不如当初一次定好
  • 存储一旦选错类型:跑 OLTP 的库落在了只适合顺序写的存储上(某些分布式存储、开了某种压缩的文件系统),后面怎么调参数都是在补一个先天缺陷

这类决策有个共同特征:错在前期,痛在后期,且后期改的成本高得不成比例。

打个比方的话,参数像装修时的水电走线和承重墙位置。房子还在毛坯阶段,你想怎么排管都行;等瓷砖贴完家具进门,再想挪一个插座位置,就得砸墙。

那么这些"结构性"的参数该怎么定?我的做法是回到三个问题:

要回答的问题对应的那类参数
这台机器上的内存怎么分给库和操作系统各类缓冲池、共享内存、会话级私有内存的上限
谁来连、连多少、每个连接干多重的话最大连接数、会话内存、排序与哈希的工作区大小
磁盘能扛多少、能不能扛住突发写入检查点频率、后台写盘力度、IO 相关上限

这里有一个新手最容易踩的坑:恨不得把机器内存全喂给数据库。

不是内存不重要,是数据库跟操作系统在同一个屋檐下。文件缓存、临时文件、连接本身、备份时的 staging(排队过程中的临时落脚点),还有同一台机上可能跑着的其他进程,全都要吃内存。把八成以上内存划给共享内存看着很豪气,等哪天来了个大排序,或者操作系统开始回收页缓存,机器一抖,你都不知道抖在哪。

还有连接数。max_connections 往上堆是没有任何代价感的——改个数字重启而已。但每个会话背后都挂着一份私有内存,真实的连接数一上来,几千个会话能把内存吃得干干净净。这也是我一直建议把连接池压在合理数量上的原因:数据库的并发不是"同时开着多少扇门",而是"它能多快把门口排队的人处理完"(这话题在《数据库管理-第二十六期 数据库设计-使用篇(20220718)》里聊过,后面第六节还要捡回来)。

二、同一个库,跑了一年之后

话说回来,我说参数是交付前就该定的东西,不代表上线之后它就一成不变了——这是很多人对我那句话的误解。

参数是跟着 workload 走的。业务一年前和一年后大概率不是同一个东西,参数凭什么不动?

我自己惯用的判断节奏是这几条:

1. 数据量跨数量级时,必须回头看。

几百万行的表和几亿行的表,对内存、对统计信息采样、对优化器判断的影响完全不同。数据量翻了几十倍而你一个参数没动过,那不是稳定,是没人管。

2. 接入方式变了要重算。

比方说多了个定时任务每晚批量跑,多了个外部对接方在同一时刻推数据,或者应用那边改了连接池策略。并发模型变了,原来的会话内存配额、后台清理线程数、并行度上限,统统要重新看。

3. 硬件换了一定跟着改。

从机械盘换成闪存之后,某些为"省 IO"而设的保守默认值就可以松一松;反过来,如果底层存储换成了带网络延迟的分布式存储,原来的假设可能全部失效。参数是给物理环境打工的,环境变了它还端着老架子,就不合适了。

最后补一个操作层面的习惯,看着朴素但真的救命:一次只改一处,改前留基线,改后有对比。

一次调十个参数,最后有效果了,你不知道是哪个起了作用;没效果,你也不知道该回滚哪个。而且很多参数是有联动关系的——你把写盘力度调大,前台可能更顺,后台 IO 压力就上去了;你把并行度放松,单条查询快了,整体吞吐可能反而掉下来。这种互相牵扯的东西,只有对着指标一点点试才说得清楚。

所以第一层的准确定位是这样:它是一次性的基础决策,外加周期性的回归审视。别把它当成万能药,也别把它当成一次性考试——交完卷就再也不看了。

三、二层:加索引,最容易出成绩的一步

现在上二楼。

二楼是绝大多数优化停留的地方,因为它见效最快、见效最直观。

方法论大家都熟:打开数据库自己收集的那些性能信息(Oracle 的 AWR / ASH、PostgreSQL 的 pg_stat_statements、MySQL 的慢日志与分析表),按耗时或者消耗的资源排个序,挑出消耗资源最多、耗时最长的那几条 SQL,看它们的执行计划是不是走了全表扫描,是的话,给谓词列加个索引。

然后执行时间从 12 秒变成 0.3 秒。

这个瞬间是很有成就感的。我早年也很享受这个过程,甚至有点上瘾——抓慢 SQL 像钓鱼,加完索引看见那条 SQL 从报表里消失,比什么都痛快。

这条路本身没毛病。数据库厂商把这些信息收集出来给你(TOP SQL、平均逻辑读、等待事件分类),就是让你这么用的。有问题的是只做这一步。

我们先把好处说完,再算账。

一条 SQL 缺索引时,数据库能做的事非常有限:把整张表读一遍,把不符合条件的行丢掉。表越小越无所谓(这也是为什么开发环境永远测不出问题),表一大,这个"读一遍"的成本就直接线性堆上去了。索引做的,是把"读一遍"变成"精准点几下",顺带省掉排序——这是实打实的收益,不是障眼法。

问题在于,多数优化走到这儿就停了。

四、每个索引,都要还的账

索引不是白拿的。每建一个,后台就多一本要维护的账。

我把这笔账摊开列一下,各位对照自己的库看看:

代价项具体表现
空间占用大表上的索引体积常常接近表的一半,某些组合索引甚至超过原表
写入放大每次 INSERT 要往所有索引里插一项;UPDATE 索引列是一次删除加一次插入
优化器选择面变大可选路径一多,执行计划漂移的概率就上升——昨天走得好好的计划,今天突然变了,而没人动过代码
统计信息收集变慢索引列越多,收集一次要算的分布就越多,维护窗口被拖长
DDL 与备份跟着变重加字段、改类型可能触发重建;备份恢复、主备搭建的时间都随索引体积走
无用索引的隐性成本有些索引建完从没被用过,但每次写入都在为它付费

最后一条最扎心。很多库上线几年之后,攒了一堆"当年为了解决某条 SQL 而建"的索引。那条 SQL 可能早下线了,业务可能改版了,索引还挂在那,每一个写入都在为它交税,而没有任何一笔查询享受过它。

好消息是这笔成本是可以查出来的——各家的做法不同,但思路一致:数据库会记录每个索引被使用(或被扫描)的次数。长期零使用又不是唯一约束的索引,就是候选清理对象。我知道很多人不敢删,怕影响线上;稳妥点的做法是先把索引标记为不可见 / 暂不可用(各家叫法不同),观察一两个业务周期,没事再真删。

这儿有个很有意思的现象值得单独说一句:自动化工具在这块比人强。

抓 TOP SQL、识别缺失索引、给出建议索引,这类事情工具做得又快又全。市面上不少产品能直接吐出"建议创建以下索引"的清单,甚至带着预估收益。

所以问题就来了:如果优化只剩下"抓慢 SQL → 加索引"这一步,那它的门槛其实很低,低到不需要人来做判断。

真正需要人做的,是下一步。

五、把一张表的 SQL 摊开,再谈索引

这一步,是我在二楼看到的分水岭。

回到第四节的场景:报表里有 20 条慢 SQL,涉及某张业务表。多数人的做法是逐条处理——给这条加一个索引,给那条加一个索引。二十条处理完,这张表上多了几十个索引,每条 SQL 都快了。

然后呢?这张表的写入开始变慢,备份窗口从 40 分钟变成 2 小时,某天夜里优化器突然给一条老 SQL 换了个计划,第二天早上业务炸了,排查了一整天。

缺的不是索引,是把这张表当成一个整体来设计的那道工序。

我的做法是把"这一条 SQL 缺不缺索引"这个问题,换成"这张表上的所有 SQL,最少需要几个索引才能覆盖"。听起来像绕口令,实际操作就是这几步:

第一步:把这张表相关的所有 SQL 抓出来。

不是抓 TOP 10,是抓全。性能视图里能按对象过滤,慢日志里能按表名匹配,把涉及这张表的语句都捞出来。这一步常被跳过,因为大家急着去看那"最慢的一条"。

第二步:逐条拆解谓词。

对每条 SQL,列出它在 WHERE 里用作等值条件的列、用作范围条件的列、ORDER BY / GROUP BY 涉及的列、JOIN 的关联列,以及 SELECT 出来又被频繁用到的那几列。

这么一列就会发现一件有意思的事:二十条 SQL 看着五花八门,抽出来的列往往集中在五六个字段上,而且组合方式高度重复。

第三步:按"等值在前、范围在后"归并。

这是复合索引的老规矩了——等值的列放在前面把结果集切到最小,范围的列跟在后面。比方说user_id = ? AND create_time > ?和user_id = ? ORDER BY create_time DESC,在多数情况下可以被同一个(user_id, create_time)覆盖。

于是二十条 SQL 收敛到了三个索引:

-- 归并前:每条慢 SQL 各自加一个索引,累计九个CREATEINDEXidx_aONorders(user_id);CREATEINDEXidx_bONorders(create_time);CREATEINDEXidx_cONorders(user_id,create_time);CREATEINDEXidx_dONorders(shop_id);CREATEINDEXidx_eONorders(status);CREATEINDEXidx_fONorders(shop_id,status);CREATEINDEXidx_gONorders(create_time,status);-- ……以及后面的 idx_h、idx_i-- 归并后:三条复合索引覆盖绝大多数热点访问路径CREATEINDEXidx_o1ONorders(user_id,create_timeDESC);CREATEINDEXidx_o2ONorders(shop_id,status,create_time);CREATEINDEXidx_o3ONorders(status,pay_time)INCLUDE(amount,buyer_id);

第四步:清理前缀冗余。

有了(shop_id, status, create_time)之后,单独的(shop_id)就完全可以退掉了——它是前者的前缀。这一步在空间和维护成本上的收益,常常比新建索引还大。

第五步:剩下的那几条,走别的路。

归并完总会剩下几条索引救不了的。这类通常是大范围统计、模糊查询、或者压根就是写得不对。这时候要上的,就是三楼。

为什么我坚持这一步机器做不了?不是说工具不能做归并,而是归并过程中有几个判断必须代入业务:

一是列的选择性会随时间变。status这种低基数列,现在数据分布均匀,半年后可能 90% 的行都是同一个状态;create_time的范围谓词在业务爆发期会从一个小时拉到一个季度。这些变化工具看不到,但它直接决定索引还有没有用。

二是业务上真的重要程度。同样慢的两条 SQL,一条是每天跑一次的后台报表,一条是每分钟几百次的交易路径,它们的优化优先级完全不同,而这个信息不在数据库里,在开发同事的脑子里。

三是有些查询本身就是错的。给它建索引,等于给一条走错的路铺沥青。

六、三层:壮汉挤门

上三楼之前,先把我自己以前写过的一个场景搬过来。

《数据库管理-第二十六期 数据库设计-使用篇(20220718)》那篇里,为了解释并发,我写过这么个比方:有一扇 80 厘米宽的门,现在要通过 1000 个壮汉——是排队一个一个过更快,还是让这 1000 人凭实力一股脑挤上去更快?(很可能一个都过不去)

当时我在那篇文章里记了一个真实案例:某流程类系统靠一张表来维护唯一 ID,每个流程一行,用一次就在那一行上update + 1。没有队列,没有限流。压力小的时候啥事没有,某天某个流程的使用量突然上来——相当于两个星期的量顶得上以前这个项目两年的量——结果就是:那一行上的行锁等待堆成了山,一个多用户、多线程的系统,硬生生在数据库里跑成了单线程。

这就是壮汉挤门。

三楼这一整层,本质上都是在处理同一件事:不是数据库跑不动,是数据库被要求以一种它不擅长的方式在跑。

我在现场攒下来的一张清单,各位可以对着自己的业务看看,中了几条:

业务侧的写法数据库的感受换一种思路
所有流程共抢一行 / 一页做计数或取号行锁等待、缓冲忙等待拆成多行加标识轮转,或交给序列,或前移缓存
所有操作都往一张日志表里记单表写热点 + 体积疯长按时间分区,或交给更适合存日志的系统
循环里一条一条 INSERT / SELECT(N+1)网络往返 × N,逻辑读 × N批量提交,一次拿回
SELECT *拉到应用再做过滤排序无用 IO + 无用传输条件下推,只取要用的列
深分页LIMIT 100000, 20前面十万行白扫一遍记住上一次的位置,改成基于键值的翻页
定时任务每晚全量重算统计每晚一次全表扫描 + 大聚合增量累加,或用物化视图让数据库自己维护
拿表当队列用(状态字段轮询)一堆无效扫描 + 反复更新同一批行交给真正的消息中间件
事务里调外部接口 / 发短信 / 等支付回调本地事务被拉到几秒甚至几十秒事务只管自己的一致性,外部调用挪出去异步化
一次 DELETE 几千万行长事务 + 巨量重做日志 + 回滚段压力分批删除,或用分区直接截断

这张表里没有一条是靠"加内存"或者"再加个索引"能解决的。它们全都是写法问题——不是数据库不够快,是这段活本来就不该这么丢给它。

三楼为什么难?因为它不通向数据库,它通向人。

你要从数据库那一侧的证据(等待事件、热点块、逻辑读高得离谱的 SQL、长时间不释放的事务)出发,反推出业务在干什么,然后去跟开发坐下来聊:这段为什么要这么写?当初是什么约束?能不能换个写法?

这需要两头的知识:一头看得懂数据库在抱怨什么,一头听得懂业务为什么这么写。而这两头的人,在绝大多数公司里,坐在不同的工位上,用不同的术语说话,KPI 也各不相同。

我见过的最好的一次合作是这样:DBA 拿着等待事件去找开发,说"你这个更新在等另一个会话,不是慢,是堵";开发愣了一下,说"我们这么写是因为要保证 ID 不重复"。两个人加起来不到十分钟,把 ID 的生成方式换掉,第二天那个指标就消失了。

三楼的工程量通常不大,但它要求两个人愿意聊十分钟。

顺带说一句,这也是我在《胖头鱼的技术专栏-465 数据库能力上移还是下沉:库变弱了,应用就变重了(20260828)》那篇里说过的老话:把该数据库扛的事交给数据库,把该应用层自己解决的事拦在应用侧——两边各管一段,别互相倒。业务层解决了不该推给库的活,后面两层的工作量能少一大半。

七、顺序其实是反的:先看业务,最后看参数

到这里,三层都摆出来了。最后说说我自己的顺序主张。

多数人的实际顺序是:参数 → 索引 → 业务。

这完全可以理解。参数最好动——改个值重启,不用改代码,不用找开发排队;索引次之,DBA 自己就能搞定;业务最难,要跨部门、要排期、要说服人。

但收益的量级恰好是反过来的。

一个 N+1 的循环带来的多余访问,是几十倍上百倍的差别;一次把它改成批量,收益可能超过你把内存翻倍。而参数调得再精,也只是在既有负载下挤出来百分之几十。

所以我主张的顺序是:

落到具体排查,可以按症状找入口:

你看到的现象先怀疑谁进哪一层
某几条 SQL 逻辑读高得离谱,一看就是全表扫缺索引 / 写法不对二层,先按整表归并,再看是否要改写法
CPU、IO 都不高,但就是慢,锁等待时间长写法扎堆抢同一份资源三层
全库普遍慢,慢得很均匀,没有特别突出的 SQL资源配置跟不上当前负载一层
周期性地卡一阵,其他时候正常后台任务(检查点、批量、备份、统计信息)一层 + 三层(有些批量该挪出去)
单条 SQL 时快时慢,计划像抽奖一样变索引太多 / 统计信息失真二层,往往是索引泛滥的后遗症
上线时很快,跑了一年后越来越慢数据量或负载变了一层回归 + 三层(业务量级的活,参数扛不住)

这张表送给各位,希望能省下几次通宵。

还有一层窗户纸想捅一下:为什么厂商来做优化,几乎都是从参数开始的?

话说公道,这不全是不负责任。参数是"交付友好"的——不需要读你的业务代码,不需要改动线上对象,风险可控,交付周期短,还能写进报告里形成漂亮的一页。而业务优化,做成了是业务的功劳,做砸了是数据库的责任。激励就是这样,动作自然会往省力的那一侧偏。

但作为甲方,你得清楚自己买的是什么。

八、总结

回到开头那次客户现场的对话。

内存、索引、参数,这三样会做的事,我一点也不反对——它们有用,而且在某些时候就是那一味药。我反对的是把它们当成优化的全部。

三层楼可以这样记:

  • 一层是地基,交付那天就该浇筑好,但地基上面的负载年年在变,所以要周期性回来看
  • 二层是装修,能让某一处立刻好用起来,但每添一样东西都要记账,而且"一处一处加"很快就会变成一团乱麻;真正该做的是把一张表的 SQL 摊开,用尽量少的索引覆盖尽量多的路径——这一步需要业务判断,机器替不了
  • 三层是户型,是多数人不愿碰的那层,因为它通向的不是数据库,而是人;但收益最大的往往就在这儿

如果只能给一个判断标准,我会这么说:当你准备给某张表加第四个索引的时候,先回去看看这张表上的 SQL 是怎么写的。

这套顺序能不能让你少熬两个通宵,我不知道。但至少下一次,不会在同一个地方再栽一遍。

老规矩,知道写了些啥。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 12:34:20

从HDC看开发者新范式:AI、鸿蒙与云计算的端云协同

如果你最近关注开发者圈,大概率会看到不少关于华为开发者大会(HDC)的消息。我个人读完整体信号后的判断是:别急着把它当成一场新品发布会来追,真正值得琢磨的,是这次大会把AI、鸿蒙生态、云计算三件事放在同…

作者头像 李华
网站建设 2026/10/3 12:34:03

工业级配电开关控制设备核心参数体系与选型逻辑深度解析

1. 工业级配电开关控制设备的核心参数体系拆解工业级配电开关控制设备,说白了就是那些在工厂车间、变电站、大型商业综合体配电房里天天干活、一年到头不能停的开关电器。它跟家里墙上那个五孔插座旁边的空气开关完全不是一个量级的东西。家用开关可能几年都不跳一次…

作者头像 李华
网站建设 2026/10/3 12:33:52

ARMxy模块化工业控制器:替代PLC+网关+工控机的储能与自动化方案

1. 从一台设备要干三台活说起:ARMxy 模块化工业控制器的核心逻辑第一次接触 ARMxy 这类模块化工业控制器是在一个储能柜项目上。当时柜内空间已经非常紧张,电芯、BMU、高压箱、消防、空调全塞在一起,甲方还要求把本地控制、协议转换、数据上云…

作者头像 李华
网站建设 2026/10/3 12:33:07

一个Demo如何讲清无人零售三端闭环-超级无人售货机全景解读

01-一个Demo如何讲清无人零售三端闭环-URM Ultra全景解读作者:黒漂技术佬|系列:URM Ultra 方案理念与架构篇大家好,我是黒漂技术佬。今天开一个新坑——讲讲我自研的无人零售整合方案 URM Ultra。 先别被这个名字吓到。“URM” 你…

作者头像 李华