☰
Oracle存储参数pctfree与pctused的深度解析:从行迁移到性能优化
2026/10/21 17:19:55 网站建设 项目流程

前两天有个同事跑过来找我,说他们某张订单表又变慢了。索引明明都在,统计信息也刚收集过,但全表扫描一天比一天慢,等值查询偶发性地要多几十毫秒。我让他跑了一条SQL看dba_tables里的chain_cnt,结果链行率已经接近18%。他一脸懵地问我:“这个字段不是自动管理的吗,怎么会链行?”于是我们花了半个小时聊了聊pctfree和pctused,这两个几乎每个建表脚本里都会写、但很少有人认真算过的存储参数。

简单说,pctfree和pctused管的是Oracle数据块里“空间怎么分配、什么时候允许再插入”。它们不决定SQL怎么写,不参与执行计划选择,但它们会悄悄决定表的物理布局,进而影响行迁移、块密度、freelist竞争和全表扫描代价。很多性能问题查到最后,根因往往不是SQL写得差,而是表空间和段的物理参数从一开始就没配合好。

这篇文章适合三类人:刚接触Oracle、准备接手建表脚本的人;被“行迁移”“链行率”“更新后变慢”坑过的DBA;以及那些想搞清楚“为什么改了pctfree没效果”的开发者。我会把参数原理、计算公式、真实案例和排查脚本一次性说透。

1. 先说清楚这两个参数到底管什么

1.1 默认值背后的含义

如果你建表时不写存储参数,Oracle会自动给表段设置PCTFREE 10和PCTUSED 40。这个默认值从Oracle 6/7时代一直沿用到现在,能在绝大多数场景下工作正常,但“正常”不等于“最优”。

我从两个角度理解这两个参数:

  • PCTFREE管的是“插入时最多填到多少就不再填”。它的默认意思是:一个数据块在插入过程中,最多用到可用空间的90%,剩下10%故意空着,留给未来的UPDATE。
  • PCTUSED管的是“使用率降到多低之后,这个块才允许被重新插入”。默认意思是:一个块的使用率要跌到40%以下,才会重新出现在空闲列表里。

很多文档把这两个参数写得非常官方,比如“pctfree指定数据块中保留的最小空间百分比,用于更新现有的行”。如果只是背书,永远理解不了它为什么重要。我换个说法:pctfree是给块留“后手”,pctused是决定块什么时候“重新营业”。

1.2 pctfree:给未来的更新留后路

Oracle的数据块默认8KB,扣除块头、事务槽、行目录这些开销,一个块能放的数据大约在7.5KB到7.7KB。插入数据时,块会一直往里装,直到剩余空间小于PCTFREE对应的比例,这个块就从空闲列表里摘除,不再接受新的INSERT。

那10%的空间空着干什么?就是为了让已经放进来的行在UPDATE时变大还有地方放。举个例子:一行数据原来占200字节,UPDATE时把某个VARCHAR2字段从10个字符改成500个字符,这行可能要占700字节。如果块里没有预留空间,这行就塞不回原来的位置了。

这时候Oracle只能做一件事:把整行搬到另一个有空闲的块里,在原来的位置留下一个“转发地址”。这个行为叫行迁移(row migration)。以后不管是通过全表扫描还是索引访问,只要碰到这一行,都要先找到旧位置,再顺着转发地址跳到新位置,额外多一次甚至多次物理读。行多了,IO自然就涨。

pctfree不是越大越好,后面我会讲怎么算。但你先记住一点:它存在的目的,是牺牲一点点空间,换未来的更新稳定。

1.3 pctused:什么时候让块重新“接客”

理解pctused之前,先搞清楚Oracle判断块状态的依据是“块使用率”,不是“剩余空间”。

块使用率高到什么程度会停止插入,由pctfree和100之间的差值决定:100 - pctfree。比如pctfree=10,块使用率到90%就不再接收新插入。之后块里的行继续被UPDATE变长,或者被DELETE删掉一部分,使用率会慢慢降。当使用率降到pctused以下,比如默认的40%,这个块才会重新回到空闲列表,允许INSERT再次写入。

我见过很多刚入门的人理解反了,以为“剩余空间大于某个比例就能继续插”。实际上流程是:插入看剩余空间,回列看使用率。pctfree管的是“出列”,pctused管的是“回列”,方向完全不一样。

举个直观的例子:一个块使用率到了95%,拔出空闲列表。你删了块里两行,使用率掉到70%。这个块依然不会回到空闲列表,因为70%还是高于pctused=40。要等删除足够多的行,让使用率跌破40%,这个块才会重新被INSERT选中。这个“从90%到40%”的跨度,就是很多空间利用率问题的来源。

2. 底层机制:freelist 与空间状态切换

2.1 手工段空间管理下的完整生命周期

在Oracle 9i之前,或者说在手工段空间管理(MSSM)模式下,每个段头里维护一个或多个空闲列表(freelist),用来记录哪些块当前是可以插入的。块的状态切换其实是一套完整的生命周期:

  1. 新建块刚分配出来时,使用率为0,会在空闲列表上。
  2. INSERT不断向块写入数据,使用率上升。当使用率超过100 - pctfree,块从空闲列表摘除,只能被UPDATE和DELETE访问,不再接收新的INSERT。
  3. 块里的行被DELETE,或者UPDATE把行长度缩短,使用率下降。只要使用率低于pctused,块重新回到空闲列表。
  4. 回到空闲列表后,这个块又可以参与后续INSERT分配。直到它再次被填满,再次摘除,循环往复。

这套机制本身不复杂,但有个容易忽略的细节:块离开空闲列表的门槛和使用率有关,回到空闲列表的门槛也和“使用率”有关,而不是和“还有多少字节空闲”有关。也就是说,一个块即使空出了30%的空间,只要使用率还高于pctused,它就一直处于“闲置但不可插入”的尴尬状态。这个状态会让已经分配出去的块不能被充分利用,进而导致段不断扩张,段变大了但数据密度反而没跟上。

2.2 两个参数怎么配:间距的艺术

pctfree和pctused之间隔着一段区间,这段区间决定了块处于“非空闲但也不满”状态的时间长短。

默认组合是pctfree=10、pctused=40,区间的跨度是100 - 10 - 40 = 50个百分点。一个块从摘除到重新回列,中间要经历使用率从90%降到40%的过程。这个跨度大,块在“闲置不可插”状态待的时间长,空间利用率可能下降。

如果反过来,把pctused调得很高,比如70,那么pctfree=10时区间跨度只有100 - 10 - 70 = 20个百分点。块的使用率稍微降一点就会回到空闲列表,看起来“空间复用更快”,但代价是块会频繁进出空闲列表,并发INSERT争夺freelist的开销变大,链表扫描次数增多,最终插入性能反而下降。

所以这两个参数不能孤立看待,它们是一对组合。区间太大,空间浪费;区间太小,空闲列表抖动。这也是为什么Oracle默认重心偏保守:10/40在大多数场景下让块留在空闲列表外的时间足够长,避免频繁切换,同时也能容忍一定程度的空间闲置。

2.3 ASSM 为什么把 pctused“废了”

如果你查的是12c、19c的Oracle数据库,创建表空间时默认使用自动段空间管理(ASSM)。在ASSM下,段的空闲空间信息不再维护在freelist里,而是用位图记录每一组块的使用状态。块被分成三类:全空、部分使用、已满。

插入时,Oracle直接根据位图找到“部分使用”的块,直接写入。块满了,位图把它标记成已满;块因为删除而空出空间,位图自动把它从已满降级成部分使用,于是又可以插入。

这套流程根本不需要pctused来“复位”。所以在ASSM表空间里,pctused参数会出现在建表语句里,但不生效。你改成多少都行,Oracle都会忽略它。pctfree在ASSM下依然有效,但它只是影响块内预留空间,不参与空闲列表管理。

这里有个很常见的坑:有人在ASSM表空间里把pctused从40改成70,想提高空间复用效率,跑了一周发现表大小和插入延迟完全没变,还以为参数失效了。实际上不是失效,是它压根没参与工作。查一下表空间的segment_space_management字段就能确认,AUTO就是ASSM,MANUAL才是MSSM。

select tablespace_name, segment_space_management from dba_tablespaces where tablespace_name = 'USERS';

3. 参数应该怎么定:公式、场景与权衡

3.1 最实用的 pctfree 估算公式

pctfree设置的核心,是为了覆盖“块内行未来增长的空间需求”。最直接的办法是根据当前行均长和未来行均长来估算:

pctfree ≈ (projected_avg_row_len - current_avg_row_len) / projected_avg_row_len × 100%

什么意思?假设一张表现在平均行长480字节,下一步应用要增加一个备注字段,将来平均行长会到610字节。理论需要预留的比例是:

(610 - 480) / 610 ≈ 21.3%

也就是说,pctfree理论上至少要21%。但这只是单行角度的理论底线,实际使用里我会再加10到15个百分点作为安全垫,因为avg_row_len是平均值,总会有一些行比均值涨得更多。

另一个角度是看“块内所有行的总增长量”。8KB的块,可用空间按7.6KB算,480字节一行大概能放15到16行。如果每行都要涨130字节,16行总计要额外约2080字节,占块可用空间的27%左右。这样算出来pctfree至少要27%,和前面21%的结论方向一致,只是更保守一些。这也是为什么我遇到行增长较明显的表,习惯把pctfree放在30到40之间,而不是默认的10。

注意:pctfree是按百分比预留的,不是在块里留固定字节数。块大小不同,同样比例对应的绝对值不同,但百分比逻辑是一样的,所以公式不随块大小变化。

3.2 不同负载下的推荐组合

没有万能参数,只有适合负载的参数。下面是我在项目里常用的参考值,基于手工段空间管理场景,最好结合前面公式最终确认:

场景pctfreepctused说明
只读/归档表0~55~20没有UPDATE需求,尽可能把块塞满,减少空间占用
常规OLTP混合负载1040Oracle默认,适合大多数情况,不用乱改
UPDATE频繁且行长会变长25~4050~60为行增长留足空间,块使用率下降后较快回列
以INSERT为主的流水表0~520~30减少freelist运维开销,空间优先
索引段PCTFREE15~30无索引没有pctused概念,pctfree高可以减少索引块分裂

这里需要特别说明索引:索引段只认pctfree,不认pctused。如果你在创建索引时写了pctused,Oracle会忽略它。索引的pctfree作用是给索引块里的条目预留空间,让未来键值增长或INSERT时不至于频繁触发索引块分裂。对递增主键这类有序插入,pctfree默认10足够;对随机键值插入频繁的索引,可以适当调高到20~30,代价是索引尺寸变大。

3.3 修改参数不等于立即生效

很多人改了表参数之后,发现链行率没有下降,表大小也没有变化,于是以为参数没用。这不是参数没用,而是你只改了“元数据”,没有让现有块按新参数重新布局。

ALTER TABLE ... PCTFREE只是修改了段的属性,它只影响后续新分配的块,对已经存在的数据块基本没有作用。想让存量块按新pctfree重新排布,你必须移动数据:

alter table orders move pctfree 35 pctused 60;

MOVE会把表的行重新物理分布,按新的空间参数重新写入块。但MOVE会让所有索引失效,所以紧跟着必须重建索引:

alter index idx_orders_01 rebuild;

如果是分区表,可以针对单个分区操作:

alter table orders modify partition p2024_01 pctfree 35; alter table orders move partition p2024_01 pctfree 35;

MOVE期间表会被锁定,在线业务不能访问,所以大表一定要安排在维护窗口。如果是几十GB级别的大表,MOVE的锁表时间可能无法接受,这时候要优先考虑在线重定义(DBMS_REDEFINITION),通过重建临时表并交换数据的方式把影响降到最低。

4. 一个真实案例:更新频繁的大表为什么越来越慢

4.1 症状与初步判断

某应用系统里有一张订单表,大约320万行,占用约330MB空间,平均行长480字节。应用上线了一个新版本,把订单附言字段从40字节扩到200字节,并且大量历史订单在后台被程序批量更新,附言字段从短文本变成了长文本。

上线两周后,DBA发现两个明显的性能劣化:

  • 全表扫描这张订单表从8秒涨到了30秒以上。
  • 索引等值查询偶发变慢,有时同一个查询的耗时相差一倍。

索引没失效,统计信息也重新收集过,执行计划看到的变化只是全表扫描的IO代价明显增加。一开始大家怀疑是缓冲池不够,后来才发现问题在于表段内部出现大量行迁移,全表扫描读取了大量无效的转发地址。

初始排查直接看表的统计信息:

select table_name, num_rows, blocks, empty_blocks, avg_row_len, chain_cnt from dba_tables where owner = 'APP' and table_name = 'ORDERS';

结果里chain_cnt达到57万多,除以行数约18%。也就是说每100行里就有18行是链行或迁移行。再看avg_row_len已经涨到610字节,比之前的480字节增长了约27%。结合应用更新的字段大小,基本能确定:pctfree默认10%完全覆盖不了27%的单行增长,大量行在UPDATE时因为空间不足被迫搬家。

4.2 用链行诊断脚本确认根因

chain_cnt这个值只反映规模,要定位哪些行受影响,需要把链行明细导出来看。Oracle提供了一个标准脚本,用来创建chained_rows表,然后把链行数据灌进去。

@?/rdbms/admin/utlchain.sql analyze table app.orders list chained rows into chained_rows; select count(*) from chained_rows;

执行之后,chained_rows里记录的就是所有发生链行或迁移的行。接下来要区分是“行链接”还是“行迁移”。

  • 行链接:一行数据本身就超过了一个块能容纳的大小,天然要跨块存储,通常是超大行。
  • 行迁移:行本来在一个块里放得下,因为UPDATE变大,被移动到新块,旧位置留转发地址。

怎么区分?很简单,看这表的平均行长。这里的avg_row_len是610字节,8KB的块即使扣除块头也能容纳十几行,根本不可能产生物理上的行链接。所以这18%的链行几乎全部来自行迁移,根因就是pctfree不足。如果一张表的avg_row_len接近甚至超过块大小,那才是行链接问题,调pctfree基本没用,要考虑增大块大小或者把大字段拆分到单独表。

注意:chained_rows表如果存在,analyze之前最好先清空,否则结果会不断累积,影响排查。每次排查前先truncate table chained_rows。

4.3 处理过程与效果对比

确认根因之后,处理方案就是按新行长重新计算pctfree,并让数据块重新排布。

前文提到,理论需要的pctfree至少在27%左右,所以我把pctfree直接定到35%,给尾部数据留了一点安全余量。pctused从40提高到60,让删除空间后使用率下降快的块能较快回到空闲列表,减少空间浪费。然后在维护窗口执行:

alter table app.orders move pctfree 35 pctused 60; -- 重建所有索引 alter index idx_orders_01 rebuild; alter index idx_orders_02 rebuild;

MOVE执行完,观察新统计信息和实际效果:

指标调整前调整后
链行率18%0.4%
全表扫描耗时30秒以上9秒左右
表段大小约330MB约375MB

表段大小增加了约13%,这是提高pctfree的直接代价,但换来了链行率大幅下降和全表扫描性能恢复。代价完全值得。

这里要说一个很关键的细节:MOVE之后一定要立即重建索引,不是“建议”,是“必须”。移动行的物理位置后,索引里旧的rowid全部指向错误位置,不重建索引的话,等值查询会直接产生大量错误读,性能比不处理前更差。我遇到过不止一次同事MOVE完忘了重建索引,第二天上线被查询慢的问题搞到焦头烂额。

这次案例中,我们本来也想试试pctfree=50能不能让链行率直接降到0,但算了一下空间代价和收益:从35%再往上提,每个块里的行数会明显变少,表段会额外膨胀很多,全表扫描的块数也要增加,反而可能抵消部分收益。0.4%的链行率已经可以接受,不需要追求绝对零迁移。

5. 常见误区与排查速查表

5.1 三个高频误区

误区一:pctfree越大越好。这是最常见的误解。pctfree的本质是“用空间换更新稳定性”,但如果预留过大,块里能装的行数变少,全表扫描和索引范围扫描要读更多的数据块,表段膨胀,空间成本反而拖累性能。我看到有些建表脚本把pctfree写50,理由是“防止以后更新”,结果一张100GB的表膨胀到180GB,扫描性能急剧下降。合理做法是先量化行增长,公式算出来再给安全垫,不要拍脑袋定一个很大的值。

误区二:pctused越高,空间复用越快、利用率越高。pctused高,块确实更容易回到空闲列表,但代价是空闲列表抖动。我来描述一下这个场景:pctused=80时,一个块刚被填到90%摘除,删除几行之后降到75%,立刻回列,INSERT两三条又到90%,再次摘除。这个块在空闲列表上反复横跳,每次进出都要操作链表,并发插入时freelist会成为热点。实际经验是,pctused设得太高,插入TPS反而下降。

误区三:ASSM下调整pctused能优化空间管理。前面已经说过,ASSM表空间里pctused被Oracle直接忽略,位图管理机制已经替代了freelist。除非你的表空间是segmentspacemanagement=manual的老古董,否则不要在pctused上浪费时间。查看表空间管理方式用第2.3节那条SQL,一目了然。

5.2 快速排查清单

我平时排查时有一套固定的动作,分享出来供参考:

现象可能原因优先检查
链行率大于5%pctfree不足,行长增长看avg_row_len与历史对比,analyze list chained rows
插入越来越慢,等待事件频繁freelist争用,pctused过高查表空间是否ASSM,查v$segment_statistics
全表扫描IO很大pctfree过大,块内密度低查avg_space、empty_blocks
表空间剩余很多但insert还在扩张高水位线未释放,pctused过低导致块不可复用检查blocks与实际数据量,考虑shrink或move

配合定期巡检,我习惯用一个SQL把链行率高的表捞出来:

select owner, table_name, num_rows, chain_cnt, round(chain_cnt / nullif(num_rows, 0) * 100, 2) as chain_pct from dba_tables where num_rows > 10000 and chain_cnt > 0 order by chain_pct desc;

这个SQL执行前提是表的统计信息足够新鲜,尤其是chain_cnt字段。如果长时间没分析,这个字段可能还是旧值,甚至为0。最好在维护窗口先跑一次analyze table或者dbms_stats收集完再查询。

排查时还要区分“行链接”和“行迁移”。行链接常见于超长行,比如把大文本直接塞进普通表字段,而且塞到接近块大小,这种情况调pctfree没用,应该考虑改CLOB、分表或者增大块大小。而行迁移的根源,就是UPDATE时块内空间不够,解决方案就是本文一直在讲的pctfree计算和move重排。

最后多提一句:如果表已经用ASSM管理,链行率又很高,单纯调pctfree之后必须做move或shrink space才能让存量块重新布局。shrink space需要先开启行移动,操作流程是alter table enable row movement,然后alter table shrink space cascade。这个方法的好处是可以在线做,锁表时间比MOVE短,但如果表上有函数索引会有限制,使用时提前确认。

我在实际项目里最深的体会是:pctfree和pctused不是那种“一枝独秀”的优化项,它们更像是表和更新之间的空间交易。调参之前永远先用数据说话——看avg_row_len变化,看chain_cnt规模,看应用接下来会不会改字段长度。把这条习惯养成之后,很多性能问题都能在变成事故之前就被拦下来。参数本身的规则很老,一点都不复杂,复杂的从来都是我们对数据增长的预判。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询