马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?用户注册
×
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_size unused
- ---------- ----------- ---------- ---------- ----------- ----------
- big_order 18423065 12654336 KB 5842176 KB 2457600 KB 4354560 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 后 | | reserved | 12654336 KB | 10128304 KB | 6127616 KB | | unused | 4354560 KB | 1819648 KB | 258048 KB | | Forwarded row | 3126880 | 3126880 | 0 | | Data page cluster ratio | 0.1847 | 0.2413 | 0.9872 | | Space utilization | 0.5219 | 0.8104 | 0.9318 | | 范围扫描逻辑读 | 284731 | 196022 | 41208 | | 范围扫描耗时 | 6214 ms | 4380 ms | 428 ms | | 重建耗时/日志量 | — | 6 分钟 / 1.8 GB | 41 分钟 / 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 语法细节略有差异,生产执行前请先在测试库验证。 |