1. 索引碎片的前世今生

各位数据库管理员朋友们,今天咱们来聊聊达梦DM8数据库中那个让人又爱又恨的小东西——索引碎片。就像我们家里的衣柜,用久了衣服会乱七八糟一样,数据库索引用久了也会产生"碎片"。

索引碎片到底是什么呢?简单说,就是索引页中的数据排列不再紧凑有序,导致数据库引擎需要读取更多的页来获取相同数量的数据。想象一下,你要找一件衬衫,结果它被分散在衣柜的五个不同抽屉里,是不是很抓狂?

在达梦DM8中,索引碎片主要分为两种:

  • 内部碎片:单个页中未使用的空间过多,就像抽屉里衣服没放满,浪费空间
  • 外部碎片:逻辑上连续的页在物理存储上不连续,就像相邻抽屉被其他衣服隔开了

2. 索引碎片的危害与检测

碎片多了会有什么问题?最直接的影响就是查询变慢。我见过一个客户的系统,简单查询居然要5秒,重建索引后直接降到0.1秒,效果立竿见影。

在达梦DM8中,我们可以用以下SQL检测索引碎片情况:

-- 检查指定表的所有索引碎片情况
SELECT 
    TABLE_NAME, 
    INDEX_NAME, 
    INDEX_TYPE,
    BLOCKS,
    BLOCKS*8/1024 AS "索引大小(MB)",
    LEAF_BLOCKS,
    DISTINCT_KEYS,
    CLUSTERING_FACTOR,
    STATUS
FROM 
    USER_INDEXES 
WHERE 
    TABLE_NAME = 'EMPLOYEE';
    
-- 更详细的碎片分析
SELECT 
    SEGMENT_NAME,
    SEGMENT_TYPE,
    BYTES/1024/1024 AS "大小(MB)",
    BLOCKS,
    EXTENTS,
    ROUND(100*(BYTES - NVL(EMPTY_BYTES,0))/BYTES,2) AS "空间利用率(%)"
FROM 
    USER_SEGMENTS
WHERE 
    SEGMENT_NAME IN (SELECT INDEX_NAME FROM USER_INDEXES WHERE TABLE_NAME = 'EMPLOYEE');

注释说明:

  • BLOCKS 显示索引占用的数据块数量
  • LEAF_BLOCKS 是索引叶子块数量,与碎片程度相关
  • CLUSTERING_FACTOR 反映数据有序程度,值接近表行数说明有序性差
  • 空间利用率 低于70%通常意味着显著碎片

3. 重建索引 vs 整理碎片

达梦DM8提供了两种主要的碎片整理方法:重建索引(REBUILD)和整理碎片(COALESCE)。它们有什么区别?什么时候该用哪个?咱们来掰扯掰扯。

3.1 重建索引(REBUILD)

重建索引相当于把衣柜里的衣服全倒出来,重新叠好再放回去。这是最彻底的整理方式,语法如下:

-- 基本重建语法
ALTER INDEX idx_employee_name REBUILD;

-- 带参数的重建
ALTER INDEX idx_employee_name REBUILD 
    TABLESPACE users 
    ONLINE 
    COMPUTE STATISTICS
    PARALLEL 4;

注释说明:

  • TABLESPACE 指定重建后索引存放的表空间
  • ONLINE 选项允许在重建期间继续使用索引
  • COMPUTE STATISTICS 重建后自动收集统计信息
  • PARALLEL 使用并行度加速重建

重建索引的优点:

  1. 效果最好,能完全消除碎片
  2. 可以改变索引的存储参数
  3. 可以并行操作提高速度

缺点也很明显:

  1. 需要额外空间,相当于创建新索引再删除旧索引
  2. 大型索引重建可能耗时较长
  3. 如果不用ONLINE选项会阻塞DML操作

3.2 整理碎片(COALESCE)

整理碎片更像是把衣柜里松散的衣服往里推一推,让空间更紧凑。它只合并相邻的空闲空间,语法更简单:

-- 整理索引碎片
ALTER INDEX idx_employee_name COALESCE;

-- 整理表空间碎片(关联技术)
ALTER TABLESPACE users COALESCE;

整理碎片的优点:

  1. 操作快速,不需要额外空间
  2. 对系统影响小,基本不阻塞操作
  3. 可以频繁执行作为维护手段

但它的局限性也很明显:

  1. 只能合并相邻空闲空间,无法完全消除碎片
  2. 不能改变索引的存储结构
  3. 对严重碎片化的索引效果有限

4. 实战选择策略

知道了两种方法的区别,关键问题来了:什么时候该用哪种方法?根据我的经验,可以遵循以下策略:

4.1 选择重建索引的场景

  1. 碎片率超过30%:当检测发现索引空间利用率低于70%时
  2. 性能明显下降:关键查询比历史性能下降20%以上
  3. 定期维护窗口:如月度维护时对关键表索引重建
  4. 数据大批量变更后:如ETL过程导入大量数据后
  5. 需要修改存储参数时:如迁移表空间或调整并行度
-- 实战示例:月度维护脚本
BEGIN
    FOR idx IN (SELECT index_name FROM user_indexes WHERE table_name IN ('EMPLOYEE','DEPARTMENT','SALARY')) 
    LOOP
        EXECUTE IMMEDIATE 'ALTER INDEX ' || idx.index_name || ' REBUILD ONLINE COMPUTE STATISTICS';
        DBMS_OUTPUT.PUT_LINE('已重建索引: ' || idx.index_name);
    END LOOP;
END;
/

4.2 选择整理碎片的场景

  1. 轻度碎片(10-30%):作为日常维护手段
  2. 系统高峰时段:需要快速整理又不想影响业务
  3. 频繁DML的表:如订单表等经常增删的表
  4. 维护窗口有限时:没有足够时间进行完整重建
  5. 测试环境验证:先整理看效果再决定是否重建
-- 实战示例:每日碎片整理脚本
BEGIN
    FOR idx IN (SELECT index_name FROM user_indexes WHERE table_name IN ('ORDER_DETAILS','CUSTOMERS')) 
    LOOP
        -- 先检查空间利用率
        EXECUTE IMMEDIATE 'ALTER INDEX ' || idx.index_name || ' COALESCE';
        DBMS_OUTPUT.PUT_LINE('已整理索引: ' || idx.index_name);
    END LOOP;
END;
/

5. 性能影响与注意事项

无论是重建还是整理,都会对系统产生一定影响,我们需要特别注意以下几点:

5.1 资源消耗

重建索引是资源密集型操作,特别是大型索引:

  • CPU使用率会显著上升
  • 磁盘I/O压力增大
  • 可能需要大量临时表空间

建议:

  • 在业务低峰期执行
  • 使用PARALLEL参数控制资源使用
  • 监控系统资源使用情况

5.2 锁与阻塞

不带ONLINE选项的重建会:

  • 获取排他锁,阻塞所有DML操作
  • 可能导致应用程序超时

建议:

  • 关键业务表使用ONLINE重建
  • 提前通知业务部门维护窗口
  • 测试环境验证预估时间

5.3 统计信息

重建索引后:

  • 原有统计信息会被清除
  • 查询优化器可能暂时选择次优执行计划

建议:

  • 使用COMPUTE STATISTICS选项
  • 重建后考虑手动收集统计信息
  • 对大表使用ESTIMATE STATISTICS减少开销
-- 重建后收集统计信息的推荐做法
ALTER INDEX idx_large_table REBUILD COMPUTE STATISTICS;

-- 对大表使用采样估算
BEGIN
    DBMS_STATS.GATHER_INDEX_STATS(
        ownname => 'SCOTT',
        indname => 'IDX_LARGE_TABLE',
        estimate_percent => 20,
        degree => 4
    );
END;
/

6. 自动化维护策略

对于生产环境,我建议建立自动化的索引维护策略:

  1. 定期检测:每周自动检测高碎片索引
  2. 分级处理:根据碎片程度选择重建或整理
  3. 智能调度:在维护窗口自动执行
  4. 效果验证:维护后验证空间节省和性能提升
  5. 异常警报:处理失败或资源超限时告警
-- 自动化维护表示例
CREATE TABLE index_maintenance_log (
    log_id NUMBER GENERATED ALWAYS AS IDENTITY,
    index_name VARCHAR2(30),
    table_name VARCHAR2(30),
    operation_type VARCHAR2(10), -- 'REBUILD' or 'COALESCE'
    before_blocks NUMBER,
    after_blocks NUMBER,
    before_bytes NUMBER,
    after_bytes NUMBER,
    start_time TIMESTAMP,
    end_time TIMESTAMP,
    status VARCHAR2(10), -- 'SUCCESS' or 'FAILED'
    error_message VARCHAR2(4000),
    PRIMARY KEY (log_id)
);

-- 自动化维护存储过程示例
CREATE OR REPLACE PROCEDURE auto_index_maintenance AS
    CURSOR frag_indexes IS
        SELECT i.index_name, i.table_name, 
               s.bytes, s.blocks,
               ROUND(100*(s.bytes - NVL(s.empty_bytes,0))/s.bytes,2) AS usage_pct
        FROM user_indexes i
        JOIN user_segments s ON i.index_name = s.segment_name
        WHERE ROUND(100*(s.bytes - NVL(s.empty_bytes,0))/s.bytes,2) < 75
        ORDER BY usage_pct ASC;
    
    v_start_time TIMESTAMP;
    v_end_time TIMESTAMP;
    v_after_bytes NUMBER;
    v_after_blocks NUMBER;
BEGIN
    FOR idx IN frag_indexes LOOP
        v_start_time := SYSTIMESTAMP;
        
        -- 根据碎片程度选择操作类型
        IF idx.usage_pct < 60 THEN
            -- 严重碎片,重建索引
            BEGIN
                EXECUTE IMMEDIATE 'ALTER INDEX ' || idx.index_name || ' REBUILD ONLINE';
                
                -- 获取整理后的大小
                SELECT bytes, blocks INTO v_after_bytes, v_after_blocks
                FROM user_segments
                WHERE segment_name = idx.index_name;
                
                -- 记录日志
                INSERT INTO index_maintenance_log VALUES (
                    DEFAULT, idx.index_name, idx.table_name, 'REBUILD',
                    idx.blocks, v_after_blocks, idx.bytes, v_after_bytes,
                    v_start_time, SYSTIMESTAMP, 'SUCCESS', NULL
                );
                COMMIT;
            EXCEPTION
                WHEN OTHERS THEN
                    INSERT INTO index_maintenance_log VALUES (
                        DEFAULT, idx.index_name, idx.table_name, 'REBUILD',
                        idx.blocks, NULL, idx.bytes, NULL,
                        v_start_time, SYSTIMESTAMP, 'FAILED', SQLERRM
                    );
                    COMMIT;
            END;
        ELSE
            -- 轻度碎片,整理即可
            BEGIN
                EXECUTE IMMEDIATE 'ALTER INDEX ' || idx.index_name || ' COALESCE';
                
                -- 获取整理后的大小
                SELECT bytes, blocks INTO v_after_bytes, v_after_blocks
                FROM user_segments
                WHERE segment_name = idx.index_name;
                
                -- 记录日志
                INSERT INTO index_maintenance_log VALUES (
                    DEFAULT, idx.index_name, idx.table_name, 'COALESCE',
                    idx.blocks, v_after_blocks, idx.bytes, v_after_bytes,
                    v_start_time, SYSTIMESTAMP, 'SUCCESS', NULL
                );
                COMMIT;
            EXCEPTION
                WHEN OTHERS THEN
                    INSERT INTO index_maintenance_log VALUES (
                        DEFAULT, idx.index_name, idx.table_name, 'COALESCE',
                        idx.blocks, NULL, idx.bytes, NULL,
                        v_start_time, SYSTIMESTAMP, 'FAILED', SQLERRM
                    );
                    COMMIT;
            END;
        END IF;
    END LOOP;
END;
/

7. 总结与最佳实践

经过上面的详细讨论,我总结出达梦DM8索引碎片维护的几点最佳实践:

  1. 定期检测:建立定期(如每周)检测索引碎片的机制
  2. 分级处理:轻度碎片(>60%)整理,严重碎片(<60%)重建
  3. 资源规划:大型索引重建安排在业务低峰期,预留足够资源
  4. 在线操作:关键业务索引使用ONLINE选项重建
  5. 统计信息:重建后及时更新统计信息
  6. 效果验证:维护前后记录空间和性能变化,验证效果
  7. 自动化:建立自动化维护流程,减少人工干预

记住,索引维护不是越频繁越好,需要平衡维护成本和性能收益。对于特别活跃的表,可以考虑稍微容忍一定程度的碎片,减少维护频率。

最后提醒一点,达梦DM8后续版本可能会优化索引维护机制,记得关注版本更新说明,及时调整维护策略。