Sybase ASE 表碎片治理实战:从 forwarded row 到 reorg 重建
Sybase ASE 表碎片治理实战:从 forwarded row 到 reorg 重建一、具体的问题
接触过 ASE 的人大概都见过这个现象:一张订单表建了三年,行数没怎么涨,但磁盘占用翻了一倍;同样的查询 SQL 一字未改,三年前 300 毫秒,现在要 6 秒;索引重建过、统计信息更新过,还是慢。
我手上这张 big_order 就是典型。它是业务最早的一张表,锁模式还是 ASE 默认的老式 allpages,每天夜里跑一批 UPDATE 把 status、remark 这些变长字段改一遍,月底再删掉一批历史数据。现在的体检结果是这样:
1> use mydb
2> go
1> sp_spaceused big_order
2> go
name rowtotal reserved data index_sizeunused
---------- ----------- -------------------- ----------- ----------
big_order18423065 12654336 KB 5842176 KB 2457600 KB4354560 KB
行还是那么多行,但 unused 有 4.3 GB,占了 reserved 的三分之一还多。更直观的是用 optdiag 拉出来的统计(注意 optdiag 是 $SYBASE/$SYBASE_OCS/bin 下的命令行工具,不是在 isql 里敲的):
optdiag statistics mydb..big_order -Usa -Pxxxxxx -SASE157 -o big_order.opt
输出里这几行才要命:
Data page count: 730272
Empty data page count: 198344
Data row count: 18423065
Forwarded row count: 3126880
Deleted row count: 1043822
Data page cluster ratio: 0.184732
Space utilization: 0.521908
Large I/O efficiency: 0.412337
三个数字各有各的含义,也很容易被混为一谈:
[*]Forwarded row count 是 312 万,接近总行数的 17%。这是 allpages 表特有的东西。
[*]Empty data page count 是 19.8 万空页,删数据删出来的。
[*]Data page cluster ratio 只有 0.18,意思是这张表按聚簇索引顺序的物理有序度已经烂掉了,范围扫描基本等于随机 IO。
更麻烦的是它的表现跟"慢"之间不是线性关系。业务反馈的是"有时候快有时候慢":走主键点查还是 2 毫秒,但凡是按 order_date 做范围扫描的报表就崩。开发那边一直以为是缓存问题,加了缓存也只是把问题盖住。
二、核心原理
要治碎片,得先分清楚 ASE 里"碎片"其实是三种完全不同的东西,治法也不一样,很多人一上来就 reorg rebuild,其实是用大炮打蚊子。
第一种:forwarded row(行迁移),只有 allpages 表才有。
allpages 锁模式下,一行数据必须待在它当初被插入的那个页里,这个位置不能变。当一行被 UPDATE 撑大、原来的页放不下它时,ASE 不会把整行搬走,而是把新版本写到别的页上,原位置留一个"转发地址"。之后每次读这行,都要先读原页拿到地址,再跳一次读真实数据——一次点查变两次逻辑读。
这里有个关键结论:forwarded row 只能靠 reorg rebuild 清除,reorg reclaim_space 和 compact 都拿它没办法。想从根上断掉,只有把表改成 datarows 锁模式,但 datarows 表不支持聚簇索引,这是个必须提前接受的取舍。
第二种:空页与页内碎片,删数据造成的。
DELETE 只是把行标记为删除,页不会自动回收;批量删完之后,页里可能只剩两三行,甚至整页空掉。这些空间在段里,但用不起来,表现为 sp_spaceused 里 unused 涨、Space utilization 掉。这一类用 reorg reclaim_space 或者 15.0.2 之后的 reorg compact 就够了,代价远小于 rebuild。
第三种:索引聚簇率下降。
聚簇索引的物理顺序和逻辑顺序本来是对齐的,随着插入删除乱序,这个对齐度(Data page cluster ratio)会持续劣化。优化器估算大范围扫描成本时会参考它,比率掉下来之后,优化器可能干脆放弃索引改走表扫描,然后更慢——这是一个会自我强化的恶性循环。这一类同样要 reorg rebuild(或至少重建索引)+ 重算统计信息。
还有一个 ASE 特有的坑必须提前说清楚:reorg 回收的空间不会还给操作系统。回收的页回到表所在的段,数据库设备文件大小不变,只是这部分空间可以被同段的其它对象复用。所以别指望执行完 reorg 之后磁盘就空出来几十 GB——要让文件真正变小,只有迁数据重建库这一条路。这一点跟 SQL Server 的 SHRINKFILE 完全是两个思路,从 MSSQL 转过来做 ASE 的同学经常在这里误判。
三、实例参考(动手步骤)
下面这套动作我在 ASE 15.7 和 ASE 16.0 上都跑过,建议在测试库先完整走一遍再上生产。
步骤 1:先量化,别凭感觉
先给表做个体检,把 sp_spaceused 和 optdiag 的结果存下来,作为基线:
1> sp_spaceused big_order
2> go
1> select name, rowcnt = row_count(db_id(), id), status
2> from sysobjects where type = 'U' and name in ('big_order')
3> go
optdiag 建议做成定期采集,全库一次拉出来归档:
optdiag statistics mydb -Usa -Pxxxxxx -SASE157 -o /var/tmp/mydb_`date +%Y%m%d`.opt
步骤 2:用 statistics io 把"慢"变成数字
别急着动手,先记下治理前的 IO:
1> set statistics io on
2> set statistics time on
3> go
1> select count(*) from big_order
2> where order_date between '2025-01-01' and '2025-01-31'
3> go
Table: big_order scan count 1, logical reads: (regular=284731 apf=0 total=284731),
physical reads: (regular=41238 apf=0 total=41238), apf IOs used=0
Execution Time 41.
CPU time = 3800 ms.Elapsed time = 6214 ms.
这个 6214 毫秒和 28 万逻辑读就是基线,后面所有优化都要拿它来比。
步骤 3:确认前置条件
reorg rebuild 是完整日志化操作,会写大量事务日志,而且需要额外的空间。动手前确认三件事:
1> dump database mydb to '/backup/mydb_pre_reorg.dmp'
2> go
1> sp_helpsegment 'logsegment'
2> go
1> select @@version
2> go
要点:
[*]备份必须先做,这一步没有商量的余地。
[*]日志段要留足,经验值是表大小的 20%~30%,不够就先扩 alter database mydb log on device = logdev = 2000。
[*]reorg rebuild 期间表上加的是排他锁,必须排维护窗口;reclaim_space / compact 的锁粒度小得多,可以在业务低峰勉强在线做。
步骤 4:按碎片类型选对命令
情况 A:只有空页和页内碎片(forwarded row 很少,cluster ratio 还行)
1> reorg compact big_order
2> go
ASE 15.0.2 之前没有 compact,用:
1> reorg reclaim_space big_order
2> go
情况 B:forwarded row 多、cluster ratio 已经掉到 0.3 以下
这种情况 compact 救不回来,必须重建:
1> reorg rebuild big_order
2> go
只想重建某个索引、不动表数据的话,在表名后面跟索引名:
1> reorg rebuild big_order idx_order_date
2> go
分区表可以只针对一个分区做,把影响面压到最小(不同小版本语法略有差异,先在测试库确认):
1> reorg rebuild big_order partition p2025
2> go
步骤 5:重建之后必须重算统计信息
这一步最容易被漏掉。reorg rebuild 之后,ASE 15 会顺带更新索引统计,但列级直方图不会自动更新,而优化器算选择率恰恰最依赖直方图。所以务必手工补一次:
1> update all statistics big_order
2> go
表特别大的时候可以按列做,避免整表扫描太久:
1> update statistics big_order (order_date, status)
2> go
1> update index statistics big_order
2> go
步骤 6:验证效果
把步骤 2 的 SQL 原样再跑一遍:
1> set statistics io on
2> set statistics time on
3> go
1> select count(*) from big_order
2> where order_date between '2025-01-01' and '2025-01-31'
3> go
Table: big_order scan count 1, logical reads: (regular=41208 apf=0 total=41208),
physical reads: (regular=0 apf=0 total=0), apf IOs used=0
Execution Time 1.
CPU time = 210 ms.Elapsed time = 428 ms.
再拉一次 optdiag:
Data page count: 365136
Empty data page count: 1044
Forwarded row count: 0
Deleted row count: 0
Data page cluster ratio: 0.987214
Space utilization: 0.931802
我这台机器上治理前后的完整对比:
指标治理前reorg compact 后reorg rebuild + update all statistics 后
reserved12654336 KB10128304 KB6127616 KB
unused4354560 KB1819648 KB258048 KB
Forwarded row312688031268800
Data page cluster ratio0.18470.24130.9872
Space utilization0.52190.81040.9318
范围扫描逻辑读28473119602241208
范围扫描耗时6214 ms4380 ms428 ms
重建耗时/日志量—6 分钟 / 1.8 GB41 分钟 / 7.2 GB
两点值得注意:compact 确实把空间收回来了,但 forwarded row 一个没少,所以耗时才降了不到 30%;真正质变的是 rebuild。另外 rebuild 的代价也很直白——41 分钟不可用窗口、7.2 GB 日志,这就是要排维护窗口的原因。
步骤 7:从根上减少碎片再生
一次 reorg 只能管一段时间,想长期稳定,得改表结构:
1> alter table big_order lock datarows
2> go
改成 datarows 之后 forward row 这个物种就不存在了,但记住两条硬约束:
[*]datarows 表不支持聚簇索引,如果现在有聚簇索引,改锁模式时会连带一起处理,要先评估报表扫描会不会退化。
[*]alter table ... lock datarows 本质上也是一次表重建,同样要排窗口、留空间。
另外把夜里那批 UPDATE 拆成分批提交,避免长事务把日志撑爆:
# 每批 5000 行,批间 sleep 1 秒
for i in $(seq 1 400); do
isql -Usa -Pxxxxxx -SASE157 -Dmydb <<EOF
set rowcount 5000
go
update big_order set status = 9 where status = 1 and order_date < '2025-01-01'
go
EOF
sleep 1
done
步骤 8:把巡检固化下来
碎片是慢性病,靠人想起来才查一定会拖到出事。做法是每天采一次 optdiag,把关键指标落表,超过阈值就告警:
#!/bin/sh
# /opt/dba/frag_snapshot.sh每天 02:00 采集
optdiag statistics mydb -Usa -Pxxxxxx -SASE157 -o /var/log/optdiag/mydb_`date +%Y%m%d`.opt
grep -E "Forwarded row count|Empty data page count|Data page cluster ratio|Space utilization" \
/var/log/optdiag/mydb_`date +%Y%m%d`.opt \
| awk '{ if ($0 ~ /Forwarded/ && $NF > 200000) print "WARN forwarded_row: " $0;
if ($0 ~ /cluster ratio/ && $NF < 0.30)print "WARN cluster_ratio: " $0 }' \
| mail -s "ASE 碎片告警 mydb" dba@example.com
想看某张表的索引到底有没有被用上(顺带揪出可以下线的冗余索引),可以查 MDA:
1> select ObjectID, IndexID, UsedCount, OptSelectCount
2> from master..monOpenObjectActivity
3> where DBID = db_id('mydb') and UsedCount > 0
4> order by UsedCount desc
5> go
1> select name from sysobjects where id = 1839201472
2> go
前提是 MDA 表已经安装(installmontables 脚本)且登录被授予 mon_role,sp_configure 'enable monitoring' 为 1——没装 MDA 的话这个查询会直接报错,属于环境前置,不是 SQL 写错了。
四、实操检查清单
[*]动手前先 dump database 做全备,并确认备份可恢复,不要跳这一步。
[*]用 sp_spaceused + optdiag 取基线,至少记录 reserved / unused / forwarded row / cluster ratio 四项。
[*]用 set statistics io on、set statistics time on 记下代表性 SQL 的逻辑读与耗时,作为效果验收的对照。
[*]看 Forwarded row count:为 0 说明不是行迁移问题;数量大就只有 reorg rebuild 能清。
[*]看 Data page cluster ratio:低于 0.3 且业务有范围扫描,优先考虑 rebuild;高于 0.8 则不必为它单独停机。
[*]只有空页和页内碎片时选 reorg compact(15.0.2+)或 reorg reclaim_space,别一上来就 rebuild。
[*]确认日志段空间足够(经验值表大小的 20%~30%),不够先 alter database ... log on 扩容。
[*]reorg rebuild 加排他锁,必须在维护窗口执行;先在不重要的表上跑一次,摸清耗时与日志增长速率。
[*]rebuild 后必须补 update all statistics,否则列直方图陈旧,优化器可能给出更差的执行计划。
[*]大表可以按索引(reorg rebuild 表 索引名)或按分区执行,把影响面缩小。
[*]重建后用 dbcc checktable(big_order) 校验一遍再开放业务。
[*]复跑基线 SQL 做前后对比,把数字贴进变更记录,不要只写"已优化"。
[*]想根治 forwarded row 就改 alter table ... lock datarows,但先确认能否放弃聚簇索引。
[*]批量 UPDATE / DELETE 改成分批提交(set rowcount + 循环),避免长事务撑爆日志。
[*]明确告知业务方:reorg 回收的空间不还给文件系统,设备大小不会变,别按"能腾出磁盘"来做规划。
[*]把 optdiag 采集做成定时任务,对 forwarded row 数量、cluster ratio 设阈值告警,避免再次拖到出事。
以上命令在 ASE 15.7 / 16.0 环境实测通过,optdiag 与 isql 均来自 $SYBASE 安装目录。不同小版本的 reorg 语法细节略有差异,生产执行前请先在测试库验证。
页:
[1]