Skip to content

PostgreSQL 例行索引重建指南

概述

索引重建是 PostgreSQL 数据库维护的重要组成部分。随着时间推移,频繁的数据更新、删除操作会导致索引产生空间碎片化和性能下降。例行索引重建通过 REINDEX 命令可以有效解决这些问题,恢复索引的最佳性能。

为什么需要索引重建

索引膨胀问题

在高频更新的数据库环境中,索引会面临以下问题:

B树索引的空间回收机制

INFO

PostgreSQL 中的 B树索引在删除数据后会回收完全为空的页面,但对于部分空闲的页面则无法有效回收,这就导致了空间碎片化问题。

索引膨胀的具体场景

场景一:大量数据删除后的碎片化

假设我们有一个用户活动记录表,定期删除旧数据:

sql
-- 创建测试表和索引
CREATE TABLE user_activities (
    id SERIAL PRIMARY KEY,
    user_id INTEGER,
    activity_date DATE,
    activity_type VARCHAR(50),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 创建复合索引
CREATE INDEX idx_user_activities_date_type 
ON user_activities(user_id, activity_date, activity_type);

-- 插入大量测试数据(100万条记录)
INSERT INTO user_activities (user_id, activity_date, activity_type)
SELECT 
    (random() * 10000)::INTEGER,  -- 随机用户ID
    CURRENT_DATE - (random() * 365)::INTEGER,  -- 过去一年的随机日期
    CASE (random() * 3)::INTEGER 
        WHEN 0 THEN 'login'
        WHEN 1 THEN 'purchase'
        ELSE 'browse'
    END
FROM generate_series(1, 1000000);

监控索引大小变化

在删除数据前后检查索引大小:

sql
-- 查看索引大小的函数
CREATE OR REPLACE FUNCTION check_index_size(index_name TEXT)
RETURNS TABLE(
    index_name TEXT,
    size_mb NUMERIC,
    pages_count BIGINT,
    avg_leaf_density NUMERIC
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        $1::TEXT,
        pg_size_pretty(pg_relation_size($1))::TEXT::NUMERIC,
        (pg_relation_size($1) / 8192)::BIGINT,  -- 页面数量(8KB每页)
        NULL::NUMERIC  -- 简化版本,实际可通过 pageinspect 扩展获取
    ;
END;
$$ LANGUAGE plpgsql;

-- 删除前检查索引大小
SELECT * FROM check_index_size('idx_user_activities_date_type');

预期输出:

index_name                    | size_mb | pages_count
------------------------------|---------|------------
idx_user_activities_date_type |   45.2  |    5789

执行大量删除操作

sql
-- 删除6个月前的数据(约50%的数据)
DELETE FROM user_activities 
WHERE activity_date < CURRENT_DATE - INTERVAL '6 months';

-- 更新表统计信息
ANALYZE user_activities;

-- 再次检查索引大小
SELECT * FROM check_index_size('idx_user_activities_date_type');

删除后的输出:

index_name                    | size_mb | pages_count
------------------------------|---------|------------
idx_user_activities_date_type |   42.8  |    5478

WARNING

可以看到,虽然删除了约50%的数据,但索引大小只减少了很少,这就是典型的索引膨胀现象。

REINDEX 命令详解

基本语法和选项

sql
-- 重建单个索引
REINDEX INDEX index_name;

-- 重建表的所有索引
REINDEX TABLE table_name;

-- 重建数据库的所有索引
REINDEX DATABASE database_name;

-- 并发重建索引(推荐)
REINDEX INDEX CONCURRENTLY index_name;

-- 重建时启用详细输出
REINDEX (VERBOSE) INDEX index_name;

并发重建 vs 常规重建对比

特性常规 REINDEXREINDEX CONCURRENTLY
锁类型ACCESS EXCLUSIVESHARE UPDATE EXCLUSIVE
是否阻塞读操作
是否阻塞写操作部分(INSERT/UPDATE/DELETE可正常进行)
执行时间较快较慢(约2-3倍时间)
内存使用较少较多
推荐场景维护窗口期生产环境在线重建

实际重建操作示例

sql
-- 方法1:常规重建(需要维护窗口)
BEGIN;
-- 在事务中重建,确保一致性
REINDEX INDEX idx_user_activities_date_type;
COMMIT;

-- 方法2:并发重建(推荐用于生产环境)
REINDEX INDEX CONCURRENTLY idx_user_activities_date_type;

-- 验证重建效果
SELECT * FROM check_index_size('idx_user_activities_date_type');

重建后的输出:

index_name                    | size_mb | pages_count
------------------------------|---------|------------
idx_user_activities_date_type |   22.1  |    2831

TIP

重建后索引大小显著减少,空间利用率得到提升,查询性能也会相应改善。

监控索引膨胀的方法

创建索引膨胀监控视图

sql
-- 创建索引膨胀监控视图
CREATE OR REPLACE VIEW index_bloat_analysis AS
WITH index_stats AS (
    SELECT 
        schemaname,
        tablename,
        indexname,
        idx_tup_read,    -- 索引读取次数
        idx_tup_fetch,   -- 通过索引获取的行数
        pg_size_pretty(pg_relation_size(indexrelid)) as index_size,
        pg_relation_size(indexrelid) as index_size_bytes
    FROM pg_stat_user_indexes
)
SELECT 
    schemaname,
    tablename,
    indexname,
    index_size,
    idx_tup_read,
    idx_tup_fetch,
    CASE 
        WHEN idx_tup_read > 0 
        THEN round((idx_tup_fetch::numeric / idx_tup_read * 100), 2)
        ELSE 0 
    END as efficiency_ratio,
    CASE 
        WHEN idx_tup_read > 10000 AND 
             (idx_tup_fetch::numeric / idx_tup_read) < 0.95
        THEN '需要重建'
        WHEN index_size_bytes > 100 * 1024 * 1024  -- 100MB以上
        THEN '建议监控'
        ELSE '正常'
    END as recommendation
FROM index_stats
WHERE idx_tup_read > 0
ORDER BY index_size_bytes DESC;

-- 查看分析结果
SELECT * FROM index_bloat_analysis;

示例输出:

schemaname | tablename      | indexname                    | index_size | efficiency_ratio | recommendation
-----------|----------------|------------------------------|------------|------------------|---------------
public     | user_activities| idx_user_activities_date_type| 42 MB      | 87.32           | 需要重建
public     | orders         | idx_orders_customer_date     | 156 MB     | 96.78           | 建议监控
public     | products       | idx_products_category        | 23 MB      | 99.12           | 正常

自动化索引重建策略

创建索引重建存储过程

sql
-- 创建自动索引重建存储过程
CREATE OR REPLACE FUNCTION auto_reindex_maintenance(
    p_schema_name TEXT DEFAULT 'public',
    p_size_threshold_mb INTEGER DEFAULT 100,
    p_efficiency_threshold NUMERIC DEFAULT 0.90,
    p_dry_run BOOLEAN DEFAULT TRUE
) 
RETURNS TABLE(
    action_taken TEXT,
    index_name TEXT,
    table_name TEXT,
    old_size TEXT,
    new_size TEXT,
    execution_time INTERVAL
) AS $$
DECLARE
    r RECORD;
    start_time TIMESTAMP;
    end_time TIMESTAMP;
    old_size_bytes BIGINT;
    new_size_bytes BIGINT;
BEGIN
    -- 循环处理需要重建的索引
    FOR r IN 
        SELECT 
            i.schemaname,
            i.tablename,
            i.indexname,
            pg_relation_size(i.indexrelid) as size_bytes,
            CASE 
                WHEN s.idx_tup_read > 0 
                THEN s.idx_tup_fetch::numeric / s.idx_tup_read
                ELSE 1 
            END as efficiency
        FROM pg_stat_user_indexes s
        JOIN pg_indexes i ON i.indexname = s.indexname
        WHERE i.schemaname = p_schema_name
        AND pg_relation_size(s.indexrelid) > p_size_threshold_mb * 1024 * 1024
        AND (
            CASE 
                WHEN s.idx_tup_read > 0 
                THEN s.idx_tup_fetch::numeric / s.idx_tup_read
                ELSE 1 
            END
        ) < p_efficiency_threshold
    LOOP
        start_time := clock_timestamp();
        old_size_bytes := r.size_bytes;
        
        IF NOT p_dry_run THEN
            -- 执行并发重建
            EXECUTE format('REINDEX INDEX CONCURRENTLY %I.%I', 
                          r.schemaname, r.indexname);
        END IF;
        
        end_time := clock_timestamp();
        
        -- 获取重建后的大小
        IF NOT p_dry_run THEN
            SELECT pg_relation_size(indexrelid) INTO new_size_bytes
            FROM pg_stat_user_indexes 
            WHERE schemaname = r.schemaname AND indexname = r.indexname;
        ELSE
            new_size_bytes := old_size_bytes;  -- 干运行模式
        END IF;
        
        -- 返回结果
        action_taken := CASE WHEN p_dry_run THEN 'DRY_RUN' ELSE 'REINDEXED' END;
        index_name := r.indexname;
        table_name := r.tablename;
        old_size := pg_size_pretty(old_size_bytes);
        new_size := pg_size_pretty(new_size_bytes);
        execution_time := end_time - start_time;
        
        RETURN NEXT;
    END LOOP;
    
    RETURN;
END;
$$ LANGUAGE plpgsql;

-- 执行干运行测试
SELECT * FROM auto_reindex_maintenance(
    p_schema_name := 'public',
    p_size_threshold_mb := 50,
    p_efficiency_threshold := 0.90,
    p_dry_run := TRUE
);

定期维护脚本

bash
#!/bin/bash
# reindex_maintenance.sh - PostgreSQL 索引维护脚本

# 数据库连接参数
DB_HOST="${DB_HOST:-localhost}"
DB_PORT="${DB_PORT:-5432}"
DB_NAME="${DB_NAME:-your_database}"
DB_USER="${DB_USER:-postgres}"

# 维护参数
SIZE_THRESHOLD=100  # MB
EFFICIENCY_THRESHOLD=0.90
LOG_FILE="/var/log/postgresql/reindex_maintenance.log"

# 记录日志函数
log_message() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

# 执行索引维护
perform_maintenance() {
    log_message "开始索引维护检查..."
    
    # 执行维护存储过程
    psql -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -U "$DB_USER" \
         -c "SELECT * FROM auto_reindex_maintenance(
                p_size_threshold_mb := $SIZE_THRESHOLD,
                p_efficiency_threshold := $EFFICIENCY_THRESHOLD,
                p_dry_run := FALSE
             );" \
         -t -A -F'|' >> "$LOG_FILE" 2>&1
    
    if [ $? -eq 0 ]; then
        log_message "索引维护完成"
    else
        log_message "索引维护失败,请检查日志"
        return 1
    fi
}

# 主执行逻辑
main() {
    log_message "启动 PostgreSQL 索引维护脚本"
    
    # 检查数据库连接
    if ! psql -h "$DB_HOST" -p "$DB_PORT" -d "$DB_NAME" -U "$DB_USER" -c '\q' 2>/dev/null; then
        log_message "无法连接到数据库,请检查连接参数"
        exit 1
    fi
    
    # 执行维护
    perform_maintenance
    
    log_message "维护脚本执行完成"
}

# 运行主函数
main "$@"

性能影响和最佳实践

重建时间估算

不同大小索引的重建时间参考:

索引大小常规重建时间并发重建时间推荐执行时间
< 100MB10-30秒30-90秒任何时间
100MB-1GB1-5分钟3-15分钟低峰期
1GB-10GB5-30分钟15-90分钟维护窗口
> 10GB30分钟+90分钟+计划维护

监控重建进度

sql
-- 监控当前正在执行的重建操作
SELECT 
    pid,
    usename,
    application_name,
    state,
    query_start,
    now() - query_start as duration,
    substring(query, 1, 100) as current_query
FROM pg_stat_activity 
WHERE query ILIKE '%reindex%' 
AND state = 'active';

-- 监控锁等待情况
SELECT 
    blocked.pid AS blocked_pid,
    blocked.usename AS blocked_user,
    blocking.pid AS blocking_pid,
    blocking.usename AS blocking_user,
    blocked.query AS blocked_query
FROM pg_stat_activity blocked
JOIN pg_locks blocked_locks ON blocked.pid = blocked_locks.pid
JOIN pg_locks blocking_locks ON blocked_locks.locktype = blocking_locks.locktype
    AND blocked_locks.database IS NOT DISTINCT FROM blocking_locks.database
    AND blocked_locks.relation IS NOT DISTINCT FROM blocking_locks.relation
JOIN pg_stat_activity blocking ON blocking_locks.pid = blocking.pid
WHERE NOT blocked_locks.granted AND blocking_locks.granted;

最佳实践建议

重建时机选择

  1. 低峰期执行:选择数据库访问量最小的时间段
  2. 分批执行:大型索引分批重建,避免长时间锁定
  3. 监控资源:确保有足够的磁盘空间和内存
  4. 备份策略:重建前确保有可用的备份

注意事项

  1. 并发重建限制:同时只能有一个 REINDEX CONCURRENTLY 操作
  2. 磁盘空间:重建过程中需要额外的磁盘空间
  3. 内存消耗:大索引重建会消耗大量内存
  4. 事务日志:重建操作会产生大量 WAL 日志

非B树索引的特殊考虑

GIN 索引重建

sql
-- 创建GIN索引示例(全文搜索)
CREATE TABLE documents (
    id SERIAL PRIMARY KEY,
    title TEXT,
    content TEXT,
    tags TEXT[],
    search_vector tsvector
);

-- 创建GIN索引
CREATE INDEX idx_documents_search ON documents USING gin(search_vector);
CREATE INDEX idx_documents_tags ON documents USING gin(tags);

-- 监控GIN索引大小
SELECT 
    indexname,
    pg_size_pretty(pg_relation_size(indexrelid)) as size,
    idx_tup_read,
    idx_tup_fetch
FROM pg_stat_user_indexes 
WHERE indexname LIKE 'idx_documents_%';

-- GIN索引重建
REINDEX INDEX CONCURRENTLY idx_documents_search;

GIST 索引重建

sql
-- 创建GIST索引示例(地理位置)
CREATE EXTENSION IF NOT EXISTS postgis;

CREATE TABLE locations (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    coordinates geometry(POINT, 4326)
);

-- 创建GIST索引
CREATE INDEX idx_locations_geo ON locations USING gist(coordinates);

-- 重建GIST索引
REINDEX INDEX CONCURRENTLY idx_locations_geo;

总结

例行索引重建是 PostgreSQL 数据库维护的重要环节。通过合理的监控策略和自动化工具,可以:

  1. 及时发现索引膨胀问题
  2. 选择合适的时机进行重建操作
  3. 最小化业务影响,保持系统稳定运行
  4. 提升查询性能,优化资源利用率

定期的索引维护不仅能够回收存储空间,更重要的是保持数据库的最佳性能状态,为业务系统提供稳定可靠的数据服务。