在Oracle数据库日常运维中,表空间使用情况监控是DBA的必修课。特别是当数据库运行一段时间后,用户表数据量会呈现爆发式增长,直接影响查询性能、备份效率和存储成本。我经历过一个典型案例:某财务系统突然出现月末结账超时,排查发现核心交易表体积已膨胀到原始设计的5倍,而运维团队对此毫无察觉。
传统手工统计方式需要反复执行SELECT segment_name, bytes/1024/1024 MB FROM user_segments WHERE segment_type='TABLE'这类查询,既低效又难以形成历史对比数据。这个脚本正是为了解决以下痛点:
脚本主要从三个关键数据字典视图获取信息:
sql复制-- 基础查询示例
SELECT
t.table_name,
s.bytes/1024/1024 as size_mb,
t.num_rows,
(s.bytes/t.num_rows) as bytes_per_row
FROM
user_tables t
JOIN
user_segments s ON t.table_name = s.segment_name
WHERE
s.segment_type = 'TABLE'
使用DBMS_SQL包动态执行统计查询,避免硬编码表名:
sql复制DECLARE
cur INTEGER;
ret INTEGER;
BEGIN
cur := DBMS_SQL.OPEN_CURSOR;
DBMS_SQL.PARSE(cur, 'SELECT /*+ FULL(t) */ COUNT(*) FROM ' || tab_name || ' t', DBMS_SQL.NATIVE);
ret := DBMS_SQL.EXECUTE(cur);
...
END;
创建GATHER_TABLE_STATS存储过程统一处理:
sql复制CREATE OR REPLACE PROCEDURE gather_table_stats (
p_owner IN VARCHAR2,
p_report_mode IN VARCHAR2 DEFAULT 'SUMMARY'
) AS
-- 声明变量
BEGIN
-- 清理历史数据
DELETE FROM table_stats_history
WHERE gather_date < SYSDATE - 30;
-- 采集当前快照
INSERT INTO table_stats_history
SELECT
p_owner,
table_name,
bytes/1024/1024,
SYSDATE
FROM
dba_segments
WHERE
owner = p_owner;
-- 根据模式生成报告
IF p_report_mode = 'DETAIL' THEN
-- 详细报告逻辑
ELSE
-- 摘要报告逻辑
END IF;
END;
通过DBMS_SCHEDULER创建自动化任务:
sql复制BEGIN
DBMS_SCHEDULER.CREATE_JOB (
job_name => 'WEEKLY_STATS_COLLECTION',
job_type => 'STORED_PROCEDURE',
job_action => 'GATHER_TABLE_STATS',
number_of_arguments => 2,
start_date => SYSTIMESTAMP,
repeat_interval => 'FREQ=WEEKLY; BYDAY=MON',
enabled => FALSE);
DBMS_SCHEDULER.SET_JOB_ARGUMENT_VALUE('WEEKLY_STATS_COLLECTION',1,'SCOTT');
DBMS_SCHEDULER.SET_JOB_ARGUMENT_VALUE('WEEKLY_STATS_COLLECTION',2,'SUMMARY');
DBMS_SCHEDULER.ENABLE('WEEKLY_STATS_COLLECTION');
END;
创建历史数据存储表:
sql复制CREATE TABLE table_stats_history (
gather_date DATE,
owner VARCHAR2(30),
table_name VARCHAR2(30),
size_mb NUMBER(10,2),
growth_rate NUMBER(5,2),
CONSTRAINT pk_stats PRIMARY KEY (gather_date, owner, table_name)
) TABLESPACE tools;
CREATE INDEX idx_stats_owner ON table_stats_history(owner);
CREATE INDEX idx_stats_date ON table_stats_history(gather_date);
sql复制CREATE OR REPLACE PROCEDURE generate_table_size_report (
p_owner IN VARCHAR2,
p_threshold_mb IN NUMBER DEFAULT 100,
p_mode IN VARCHAR2 DEFAULT 'HTML'
) AS
v_report CLOB;
v_prev_total NUMBER := 0;
v_curr_total NUMBER := 0;
BEGIN
-- 初始化报告
v_report := '<h2>表空间使用报告 - ' || p_owner || '</h2>';
v_report := v_report || '<p>生成时间: ' || TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI') || '</p>';
-- 计算当前总量
SELECT NVL(SUM(bytes)/1024/1024,0)
INTO v_curr_total
FROM dba_segments
WHERE owner = p_owner;
-- 获取7天前数据
SELECT NVL(SUM(size_mb),0)
INTO v_prev_total
FROM table_stats_history
WHERE owner = p_owner
AND gather_date = (SELECT MAX(gather_date)
FROM table_stats_history
WHERE owner = p_owner
AND gather_date < SYSDATE-6);
-- 生成趋势分析
v_report := v_report || '<div class="summary">';
v_report := v_report || '<h3>空间总量变化</h3>';
v_report := v_report || '<ul>';
v_report := v_report || '<li>当前总量: ' || ROUND(v_curr_total,2) || ' MB</li>';
IF v_prev_total > 0 THEN
v_report := v_report || '<li>7天前总量: ' || ROUND(v_prev_total,2) || ' MB</li>';
v_report := v_report || '<li>增长率: ' ||
ROUND((v_curr_total-v_prev_total)/v_prev_total*100,2) || '%</li>';
END IF;
v_report := v_report || '</ul></div>';
-- 生成大表清单
v_report := v_report || '<div class="large-tables">';
v_report := v_report || '<h3>大表清单(>' || p_threshold_mb || 'MB)</h3>';
v_report := v_report || '<table border="1">';
v_report := v_report || '<tr><th>表名</th><th>大小(MB)</th><th>行数</th><th>行均大小</th></tr>';
FOR rec IN (
SELECT t.table_name,
ROUND(s.bytes/1024/1024,2) as size_mb,
t.num_rows,
CASE WHEN t.num_rows > 0
THEN ROUND(s.bytes/t.num_rows,2)
ELSE 0 END as bytes_per_row
FROM dba_tables t
JOIN dba_segments s ON t.table_name = s.segment_name
WHERE t.owner = p_owner
AND s.owner = p_owner
AND s.segment_type = 'TABLE'
AND s.bytes/1024/1024 > p_threshold_mb
ORDER BY s.bytes DESC
) LOOP
v_report := v_report || '<tr>';
v_report := v_report || '<td>' || rec.table_name || '</td>';
v_report := v_report || '<td>' || rec.size_mb || '</td>';
v_report := v_report || '<td>' || NVL(TO_CHAR(rec.num_rows),'N/A') || '</td>';
v_report := v_report || '<td>' || rec.bytes_per_row || '</td>';
v_report := v_report || '</tr>';
END LOOP;
v_report := v_report || '</table></div>';
-- 输出报告
IF p_mode = 'HTML' THEN
HTP.p(v_report);
ELSE
DBMS_OUTPUT.put_line('==== 表空间使用报告 ====');
DBMS_OUTPUT.put_line(REPLACE(REPLACE(v_report,'<p>',CHR(10)),'</p>',''));
END IF;
-- 保存当前快照
INSERT INTO table_stats_history
SELECT SYSDATE, owner, segment_name, bytes/1024/1024, NULL
FROM dba_segments
WHERE owner = p_owner
AND segment_type = 'TABLE';
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
DBMS_OUTPUT.put_line('错误: ' || SQLERRM);
END;
/
基于历史数据建立增长预测模型:
sql复制CREATE OR REPLACE FUNCTION predict_table_growth(
p_owner IN VARCHAR2,
p_table IN VARCHAR2,
p_days IN NUMBER
) RETURN NUMBER IS
v_avg_growth NUMBER;
BEGIN
SELECT AVG((curr.size_mb - prev.size_mb)/
(curr.gather_date - prev.gather_date))
INTO v_avg_growth
FROM table_stats_history curr
JOIN table_stats_history prev
ON curr.owner = prev.owner
AND curr.table_name = prev.table_name
AND curr.gather_date = (
SELECT MIN(gather_date)
FROM table_stats_history
WHERE owner = curr.owner
AND table_name = curr.table_name
AND gather_date > prev.gather_date
)
WHERE curr.owner = p_owner
AND curr.table_name = p_table;
RETURN NVL(v_avg_growth,0) * p_days;
END;
/
配置空间阈值告警:
sql复制CREATE OR REPLACE PROCEDURE check_space_thresholds AS
CURSOR c_users IS
SELECT username
FROM dba_users
WHERE account_status = 'OPEN';
v_message VARCHAR2(4000);
BEGIN
FOR user_rec IN c_users LOOP
-- 检查用户总空间
SELECT '用户 ' || owner || ' 表空间已达 ' ||
ROUND(SUM(bytes)/1024/1024) || 'MB' ||
',超过阈值 ' || threshold_value || 'MB'
INTO v_message
FROM (
SELECT owner, SUM(bytes)/1024/1024 as bytes
FROM dba_segments
WHERE owner = user_rec.username
GROUP BY owner
) s,
space_thresholds t
WHERE t.owner = s.owner
AND s.bytes > t.threshold_value
AND t.threshold_type = 'TOTAL';
IF v_message IS NOT NULL THEN
send_alert_email(
p_to => 'dba_team@company.com',
p_subject => '空间告警: ' || user_rec.username,
p_body => v_message);
END IF;
-- 检查单个大表
FOR tab_rec IN (
SELECT segment_name, bytes/1024/1024 as size_mb
FROM dba_segments
WHERE owner = user_rec.username
AND segment_type = 'TABLE'
AND bytes/1024/1024 > (
SELECT NVL(threshold_value,100)
FROM space_thresholds
WHERE owner = user_rec.username
AND threshold_type = 'TABLE'
)
) LOOP
send_alert_email(
p_to => 'dba_team@company.com',
p_subject => '大表告警: ' || user_rec.username || '.' || tab_rec.segment_name,
p_body => '表 ' || tab_rec.segment_name || ' 大小已达 ' ||
tab_rec.size_mb || 'MB');
END LOOP;
END LOOP;
END;
/
sql复制EXEC DBMS_STATS.GATHER_SCHEMA_STATS('SCOTT', estimate_percent=>DBMS_STATS.AUTO_SAMPLE_SIZE);
sql复制ANALYZE TABLE scott.emp COMPUTE STATISTICS;
SELECT chain_cnt FROM user_tables WHERE table_name = 'EMP';
问题1:报告显示表大小与DBA_TABLES.BLOCKS计算值不符
原因:DBA_TABLES显示的是已格式化的数据块数,而DBA_SEGMENTS包含未格式化的空间
解决方案:始终以DBA_SEGMENTS为准
问题2:LOB字段占用空间未统计
处理方法:增加LOB段查询:
sql复制SELECT table_name, segment_name, bytes/1024/1024
FROM dba_segments
WHERE segment_type = 'LOBSEGMENT'
AND owner = 'SCOTT';
使用CSS增强HTML输出:
sql复制HTP.p('<style>
.report-table { border-collapse: collapse; width: 100%; }
.report-table th { background: #0066CC; color: white; }
.report-table td, .report-table th { padding: 8px; border: 1px solid #ddd; }
.highlight { background-color: #FFFACD; }
</style>');
历史数据清理策略:
sql复制CREATE OR REPLACE PROCEDURE purge_stats_history AS
BEGIN
DELETE FROM table_stats_history
WHERE gather_date < ADD_MONTHS(SYSDATE, -6);
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
DBMS_OUTPUT.put_line('清理失败: ' || SQLERRM);
END;
/
增加索引空间统计:
sql复制ALTER TABLE table_stats_history ADD (index_size_mb NUMBER(10,2));
UPDATE table_stats_history t
SET index_size_mb = (
SELECT NVL(SUM(bytes)/1024/1024,0)
FROM dba_segments s
WHERE s.owner = t.owner
AND s.segment_name IN (
SELECT index_name
FROM dba_indexes
WHERE table_owner = t.owner
AND table_name = t.table_name
)
);
集成到监控系统:
sql复制CREATE OR REPLACE PACKAGE table_monitor AS
PROCEDURE daily_collection;
PROCEDURE generate_trend_report(p_days IN NUMBER);
FUNCTION get_space_usage(p_owner IN VARCHAR2) RETURN NUMBER;
END;
/
这个脚本在实际运维中已经帮助我发现了多次潜在的空间危机。比如有一次通过周报发现某个日志表以每天2GB的速度增长,及时联系开发团队增加了归档机制,避免了表空间爆满导致的业务中断。建议DBA同行们根据自己环境特点调整阈值和监控频率,特别是对重要业务系统应该设置每日监控。