不可忽视的PG表膨胀优化


案例

先来看一例公有云服务登录web页面端卡住案例:
故障现象:WEB端登录非常缓慢,需要耗时5~10分钟才能登录,登录成功之后,就不卡了;
排查过程:网络正常,服务器资源正常,数据库层发现慢SQL
一条简单的查询select 居然耗时了150s
故障原因:根据慢SQL定位到功能点是Web端在登录时权限校验用户组织权限表t_pm_user引起的缓慢,查询该表数据量很小,只有1600行,查询表大小,发现表居然有4GB左右;表膨胀率达1526倍。
表中死元组过多,现在就明确了问题就是该表膨胀导致的。经过了解业务发现:客户配置了每小时一次人员增量同步,每次同步都update表,由于同步频率高,导致表膨胀厉害。

解决办法:
vacuum full 表并设置该表的回收阈值,并反馈研发优化这个功能点


1. 什么是表膨胀?

表膨胀是指表的数据和索引所占文件系统的空间,在有效数据量并未发生大的变化的情况下,不断增大。表膨胀不仅浪费了大量的磁盘空间,甚至某些查询场景下,导致表扫描耗时长,影响查询语句效率。就如上述例子中的表t_pm_user,仅有1600多行数据,但是表对应的物理文件已经4GB。

2. 表膨胀危害是什么?

  1. 空间持续上涨,影响查询效率。当dead tuple到达某一个临界点后,需要执行一个高额代价的vacuum full清理膨胀表,但vacuum full又是AccessExclusiveLock,8级锁,会阻塞一切访问,意味着在完成清理重组之前,都无法访问该表。
  2. 扫描的效率变低,即使所有记录都是dead状态,PostgreSQL的顺序扫描也会扫描对象所有的老版本,直到执行vacuum将dead tuple的记录删除;

3. 表膨胀是怎么产生的?

PG数据库的表膨胀是基于MVCC的存储机制:

  • 插入很简单,就是将元组插入到页面的空闲空间中;
    在这里插入图片描述
  • 删除则是将元组标记为旧版本,但是即使这个旧版本对所有事务都不可见了,这个元组占用的空间也不会归还给文件系统。
    在这里插入图片描述
  • UPDATE相当于DELETE + INSERT,等于是占用了两条元组的位置,类似DELETE,旧版本的元组依然占用着物理空间。
    在这里插入图片描述
    很明显,如上图所示,在一通增删改(insert、update、delete)操作之后,页面上的旧版本元组(dead tuple)势必是占有一定比重的,这就导致了物理文件大小明显高于实际的数据量。
    为此,PostgreSQL引入了vacuum的机制,去清理那些不再需要的死元组。
  • autovacuum;
  • vacuum tablename;
  • vacuum full tablename;

普通的vacuum会做大概如下几件事情:

  1. 清除UPDATE或DELETE操作后留下的"死元组"
  2. 跟踪表块中可用空间,更新free space map
  3. 更新visibility map,index only scan以及后续vacuum都会利用到
  4. "冻结"表中的行,防止事务ID回卷
  5. 配合ANALYZE,定期更新统计信息

可以看到,普通的vacuum只是清理死元组,"腾出"空间,在今后有新数据插入时,将优先使用该空间,直到所有被重用的空间用完时,再考虑新增页面。
在这里插入图片描述
如上图所示,普通vacuum只是标记了dead tuple,并未重组页面,导致上图3个页面使用率特别低,每个页面仅存放1个tuple,而且在某些情况下,每个页面的空闲空间又不是固定大小的,所以如果要利用这些空间空间,就需要遍历一遍数据页面来找到它们,这样会造成比较大的I/O开销,表膨胀越大,开销就越大。

因此,PG就引入vacuum full这个操作,vacuum full会对表进行重组,也就意味着表的oid会变,所以不能我们在日常操作中,因此它需要lock table,而且要定位表的oid的时候,不能通过pg_class的oid来找,得通过pg_class的relfilenode来找,这样才精确。而且,vacuum full最大会占据原来磁盘空间的两倍,所以请预留好磁盘空间。
在这里插入图片描述
既然已经了解了PG的膨胀原理,PG数据库也引入了autovacuum,为什么还是会有膨胀表存在呢?有以下几种可能性导致膨胀表不能被清理:

  • autovacuum不给力
  • 服务器IO差
  • 数据库长期存在长事务,数据库中存在长SQL或带XID的长事务。之后产生的死元组都无法被清理
  • autovacuum launcher process 的调度周期太长
  • 存在失效的复制槽,复制槽通常用于逻辑复制、CDC和物理复制等场景下
  • 开发游标未及时关闭,游标未关闭会导致backend_xmin一直持续到事务结束,产生的死元组无法回收;

4. 如何优化膨胀表?

步骤1:获取膨胀表及膨胀索引数据
使用上面这2个视图可以获取当前数据库里的膨胀表及索引情况;可以根据膨胀率进行筛查,及时处理优化膨胀表。
步骤2:处理表膨胀
常用手段:

  • 使autovacuum加快速度干活
    使用场景:根据服务器配置调整PG参数,加快autovacuum效率
    相关参数默设置参考:
    autovacuum_max_workers:=3
    一次可以运行的autoacuum进程数量
    autovacuum_vacuum_cost_limit = -1 (So, it defaults to vacuum_cost_limit) = 200
    (清理作业总成本控制)
    autovacuum_vacuum_cost_delay = 20ms
    (当一个清理工作达到autovacuum_vacuum_cost_limit指定的成本限制时,autovacuum将休眠数毫秒)
    vacuum_cost_page_hit = 1
    (读取已在共享缓冲区中且不需要磁盘读取的页的成本)
    vacuum_cost_page_miss = 10s
    (获取不在共享缓冲区中的页的成本)
    vacuum_cost_page_dirty = 20
    (在每一页中发现死元组时写入该页的成本,清理成本)
  • 监控长事务
  • 单独小表设置vacuum阈值
    使用场景:数据变化快、增、删、改频繁的小表;
    设置:例如:表T被更新的元组数阈值超过1000或10%,并进行cost评估,低于1000,就执行 vacuum 和 analyze;
ALTER TABLE t SET (autovacuum_vacuum_threshold = 1000);
ALTER TABLE t SET (autovacuum_vacuum_scale_factor = 0.1);--表大小的缩放系数;
ALTER TABLE t SET (autovacuum_vacuum_cost_limit = 1000);---对 vacuum 执行 cost 进行评估,限制在1000,超过就延迟;
ALTER TABLE t SET (autovacuum_vacuum_cost_delay = 10);--延迟的时间即为10ms
  • 手动vacuum full 表
    根据上一步骤找出来的膨胀表,在非业务时段进行vacuum full tablename; 此操作需要lock table,8级别锁,针对特别大的表,评估是否需要做此类操作。对于大表,建议使用分区,可以加快vacuum的速度。

  • 膨胀索引在线重建
    在比较繁忙的系统或者大表上执行该语句的话,可以使用concurrently 参数:
    create index concurrently idx_table_name_x1 on table_name(col_name);

  • 插件处理
    pg_repack
    pg_sequeeze

  • 优化应用程序设计
    应用程序设计时,尽量避免如下:

    • LONG SQL(包括查,增,删,改,DDL所有的SQL)
    • 打开游标后不关闭
    • 在不必要的场景使用repeatable read或serializable事务隔离级别
    • 对大的数据库执行pg_dump进行逻辑备份(隐式repeatable read隔离级别的全库备份),长时间不关闭申请了事务号的事务(增,删,改,DDL的SQL),还包括2PC
<think>嗯,用户的问题是关于大数据插入、查询和维护的优化策略。从对话历史看,用户之前询问过索引分类,特别是聚簇索引的特性,现在进一步关注大数据量场景下的操作优化。用户可能正在面临实际业务中的性能瓶颈,需要可落地的解决方案。 用户提供的参考引用很有价值,特别是[1]提到索引创建策略、[4]强调执行计划分析的重要性。不过用户当前更关注插入阶段的优化,而引用内容侧重索引维护和查询优化,需要针对性补充。 在插入优化方面,核心矛盾在于速度与资源消耗的平衡。我想到几个关键维度:事务控制(避免单条提交)、批量处理(减少连接开销)、并行化(利用多核资源)、写入模式选择(COPY优于INSERT)。这些在PostgreSQL中都有成熟实践,比如COPY命令的吞吐量可达普通INSERT的10倍以上。 查询优化需要结合用户之前的索引知识。这里要强调两点:一是索引对插入的负面影响(维护成本),二是覆盖索引对查询的加速作用。用户可能没意识到,在亿级数据中,一次不必要的回操作可能导致性能下降两个数量级。 维护策略容易被忽视但至关重要。特别是[2]提到的并发清理问题——当自动清理跟不上写入速度时,膨胀会拖垮整个系统。需要教用户计算垃圾回收临界点:大小增长率 × 时间 > 自动清理窗口时长 × 清理速度。 用户可能还隐含了成本考量。比如分区虽然能提升查询性能,但分区数量超过100时,规划器开销会明显增加。这需要提醒用户根据数据特征选择合适的分区键。 最后要注意避免过度优化。比如全文索引固然快,但维护成本高,适合日志分析场景而非实时交易系统。应该建议用户先用pg_stat_statements找出TOP5慢查询再针对性优化。</think>以下是针对大数据插入、查询与维护的优化方案,结合 PostgreSQL 特性提供具体操作建议: --- ### 一、大数据插入优化 #### 1. **批量写入代替单条插入** ```sql -- 低效方式(逐条插入) INSERT INTO large_table (col1, col2) VALUES (1, 'a'); INSERT INTO large_table (col1, col2) VALUES (2, 'b'); -- 高效方式(批量插入) INSERT INTO large_table (col1, col2) VALUES (1, 'a'), (2, 'b'), ...; -- 单次插入1000~10000条[^1] ``` #### 2. **禁用索引与约束** ```sql -- 插入前禁用索引/约束 ALTER TABLE large_table DISABLE TRIGGER ALL; -- 禁用触发器 DROP INDEX idx_name; -- 删除非关键索引 -- 数据插入完成后重建 CREATE INDEX CONCURRENTLY idx_name ON large_table (column); -- 并发重建避免锁[^1] ALTER TABLE large_table ENABLE TRIGGER ALL; ``` #### 3. **使用 COPY 命令加速导入** ```sql COPY large_table(col1, col2) FROM '/path/to/data.csv' DELIMITER ',' CSV; -- 速度比INSERT快10倍以上,适合千万级数据[^1] ``` #### 4. **事务分组提交** ```sql BEGIN; INSERT ...; -- 插入10万条 COMMIT; -- 分批提交事务,避免大事务导致WAL膨胀 ``` --- ### 二、查询优化策略 #### 1. **索引设计原则** | **索引类型** | **适用场景** | **示例** | |---------------------|---------------------------------------|-----------------------------------| | **BRIN索引** | 有序大(如时间序列) | `CREATE INDEX idx_time ON logs USING BRIN (created_at)` | | **部分索引** | 高频查询特定子集 | `CREATE INDEX idx_active ON users(id) WHERE is_active=true` | | **覆盖索引** | 避免回查询 | `CREATE INDEX idx_covering ON orders (user_id, total)` | #### 2. **查询重写技巧** ```sql -- 低效: 索引失效 SELECT * FROM logs WHERE EXTRACT(YEAR FROM created_at)=2023; -- 高效: 利用索引 SELECT * FROM logs WHERE created_at BETWEEN '2023-01-01' AND '2023-12-31'; -- 支持B-tree索引[^4] ``` #### 3. **分区优化** ```sql -- 按时间范围分区 CREATE TABLE logs_partitioned (LIKE logs) PARTITION BY RANGE (created_at); -- 创建子分区 CREATE TABLE logs_2023 PARTITION OF logs_partitioned FOR VALUES FROM ('2023-01-01') TO ('2024-01-01'); ``` > **优势**:查询时仅扫描相关分区,维护时可单独清理旧分区[^2] --- ### 三、数据维护关键操作 #### 1. **自动化清理配置** ```sql -- 调整自动清理参数(postgresql.conf) autovacuum_vacuum_scale_factor = 0.05 -- 当5%数据变更时触发清理 autovacuum_analyze_scale_factor = 0.02 autovacuum_max_workers = 6 -- 增加清理进程数[^2] ``` #### 2. **手动维护窗口** ```sql -- 低峰期手动清理 VACUUM FULL ANALYZE large_table; -- 重建并更新统计信息 -- 重建索引减少碎片 REINDEX INDEX CONCURRENTLY idx_name; ``` #### 3. **并行维护工具** ```shell # 使用pg_repack在线重建 pg_repack -d mydb --table large_table ``` > 优势:不阻塞读写操作,避免`VACUUM FULL`的长时间锁[^2] --- ### 四、性能监控与诊断 #### 1. 核心监控指标 | **指标** | **监控命令** | **健康阈值** | |------------------------|---------------------------------------|--------------------| | 膨胀率 | `SELECT pg_size_pretty(pg_total_relation_size('table'))` | <20% | | 索引命中率 | `SELECT idx_scan/(idx_scan+seq_scan) FROM pg_stat_all_tables` | >99% | | 缓存命中率 | `SELECT sum(heap_blks_hit)/(sum(heap_blks_hit)+sum(heap_blks_read)) FROM pg_statio_user_tables` | >95% | #### 2. 慢查询分析 ```sql -- 启用慢查询日志(postgresql.conf) log_min_duration_statement = 1000 -- 记录超过1秒的查询 -- 使用EXPLAIN分析 EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM large_table WHERE condition; ``` --- ### 五、综合优化案例 **场景**:每日新增百万条日志数据 **优化方案**: 1. 使用COPY命令加载当日数据到临时 2. 禁用BRIN索引后批量插入主 3. 并发重建索引并更新统计信息 4. 按月分区归档历史数据 **效果**:插入时间从2小时缩短至15分钟,查询性能提升8倍[^1][^2] ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值