6000条数据执行时间9s(二)

前言

最近遇到6000条的数据在没有索引的查询下,执行时间来到了 9s,上一篇文章【6000条数据执行时间(一)】已经分析了主要引发这个问题原因 ,就是数据本身只有16MB左右,但是却占用了磁盘1.8GB的空间,全表扫描的时候遍历这1.8GB的空间就非常的慢了。

就是PG库MVCC机制会引发空间利用问题: 每次 UPDATEDELETE 不会立即物理删除旧数据,而是产生一个死元组(dead tuple),这个死会背数据自动清理机制打标记,标记改空间可重用,但是不会删除数据回收空间。

上一篇文章分析到,标记任务数据库是正常执行的,死元组(dead tuple) 基本在20%左右,所以自动清理机制(打标)是没有问题,问题就在于这些标记可重用的空间没有被重新利用起来。

✔解决表膨胀的方式有多种,我们采取的是在半夜,执行vacuum full 命令,来达到重建表的效果。

16M 的MB的数据为什么会膨胀到 1.8G

虽然PG的MVCC机制会产生垃圾空间,但是数据库本身的机制会把这些空间标记为可重用。显然我们这个表标记可重用的空间没有被利用起来。

✅下面看看DeepSeek对空间重用的说明吧,大的意思就是小于8Kb的数据能够更好的利用历史版本数据占用的空间,如果行的数据比较大则很难去利用free的空间。所以引发了空间重用利用率极低,数据又在不断的更新(占用新的空间)

空间重用的两种主要方式

1. 普通插入/更新:基于 FSM 的跨页面重用

  • 流程
    INSERT 新行 → 数据库查询 FSM → 找到第一个空闲空间足够容纳该行的页面 → 将新行写入该页面的空闲位置 → 如果该页面剩余空间变成0,FSM 条目更新。
  • 效果 :新行可以"填进"之前被删除行留下的空洞中,无需扩展文件
  • 限制:查找 FSM 有轻微开销;对于大行(超过 8KB),无法直接放入标准页面,必须使用 TOAST 存储,此时 FSM 针对的是 TOAST 表。

2. HOT 更新(堆内元组更新):同一页面内的就地重用

UPDATE 操作满足以下条件时,会触发 HOT 更新:

  • 被更新的索引列未改变(避免更新索引)。
  • 新行版本可以放入旧行所在的同一页面的空闲空间(通常页面内有碎片或已删除其他行)。

在这种情况下,数据库:

  • 将旧行标记为"死"状态。
  • 在同一页面的空闲位置写入新行。
  • 将新行的行指针指向旧行的位置(实现元组链)。
  • 好处:不产生新页面,不更新索引,性能极佳;页面内部的空间被高效复用。

你的场景:空间重用为何失效?

根据之前的分析,你的表平均每行 282KB,远大于一个页面(8KB)。这意味着:

  1. 每行跨越多个页面 。一个 282KB 的行需要占用约 282KB / 8KB ≈ 36 个页面。这些页面物理上连续(因为行存储为"块")。
  2. 更新任何字段(即使很小)都无法进行 HOT 更新 。因为新行版本与旧行版本一样大,同一页面内没有足够的连续空间再放一整行。数据库只能在新位置分配一整块连续页面(36个页面)来放置新版本,旧版本所在的36个页面变成死元组。
  3. FSM 无法有效重组这些大块空间 。FSM 记录的是每个页面的空闲字节数,但对于跨越36页的大行,FSM 只是看到分散的"小碎片"(每个8KB页面可能有一些空闲字节)。这些碎片不足以容纳另一个282KB的大行(需要36页连续空间),因此数据库永远无法将这些碎片"拼凑"起来重用,只能继续分配新页面。
  4. 结果 :每次更新都导致表文件扩展 36 页 × 8KB = 288KB。几十次更新后,就会多出几十个 288KB 的旧版本数据块。即使 VACUUM 运行,FSM 也被"无效碎片"填满,真正的重用几乎不发生 → 表膨胀从 16MB 发展到 1.8GB。

改善空间重用效率的方法

1. 减小单行大小(根本)

  • 将大字段拆分成子表(1:1 关联),让主表行变小(例如 < 8KB)。
  • 这样每次更新主表的小字段,就可以触发 HOT 更新,空间重用高效。

2. 主动整理碎片

  • VACUUM FULL:重建表,消除所有碎片,把有效数据压缩到文件头部。代价:锁表。
  • pg_repack(或 sys_squeeze):在线重建,不锁表。

🚀测试是大字段引发的空间无法重用问题

对A表进行更新操作,对比A表有大字段频繁更新和没有大字段频繁更新的前后大小。

批量更新1000次

  1. 没有大字段的情况(text类型的字段为null):全表更新1000次,表空间变成从 64KB ---> 3008KB 表空间膨胀50倍
java 复制代码
public void testA(@RequestParam String num){
    List<UnionType> types = typeService.list();
    for (int i = 0; i < Integer.parseInt(num); i++) {
        typeService.updateBatchById(types);
    }
}
  1. 没有大字段的情况(text类型的字段为null):全表更新1000次,使用单条缓慢更新,表空间从64KB ----》768K 表空间膨胀10倍
java 复制代码
public void testA(@RequestParam String num) throws InterruptedException {
   List<UnionType> types = typeService.list();
   for (int i = 0; i < Integer.parseInt(num); i++) {
       log.info("--------------:{}",i);
       for (UnionType type : types) {
           Thread.sleep(1);
           typeService.updateById(type);
       }
   }
}
  1. 将一个字段变成大字段(text类型的字段存了文本):全表更新1000次,使用单条缓慢更新,表空间从64KB----->448KB 表空间膨胀不到10倍

  2. 将一个字段变成大字段(text类型的字段存了文本):全表更新1000次,使用批量更新,表空间从64KB----->9728 kB 表空间膨胀 150多倍

✔测试结论:测试有一定的局限性,批量更新导致表膨胀的可能性最大,特别是表存在大字段的时候。

总结

在使用PG类型库的时候,还是要注意控制单条数据的大小。特别是某些表字段非常多,存在大字段的时候,批量更新之后一定要检查表占的空间大小。

数据库还是得定期维护呀🤞

相关推荐
_376271531 小时前
MySQL主从复制如何实现读写分离_利用ProxySQL进行流量分发
jvm·数据库·python
2401_833033621 小时前
SQL如何提高分组统计查询的响应速度_索引与缓存策略
jvm·数据库·python
是梦终空1 小时前
计算机源码273—基于SpringBoot+Vue3停车场管理系统带支沙箱支付(源代码+数据库)
数据库·spring boot·vue·mybatis·停车场管理系统·沙箱支付·毕设设计
dinglu1030DL1 小时前
C#怎么实现发布订阅模式 C#如何用事件总线EventBus实现模块间的松耦合消息通信【架构】
jvm·数据库·python
神明9311 小时前
PHP函数怎样利用硬件内存压缩功能_PHP启用zswap硬件加速【指南】
jvm·数据库·python
曹牧1 小时前
PL/SQL:视图(View)比较
数据库·sql
2301_781571421 小时前
如何配置用户的资源使用上限_MAX_QUERIES_PER_HOUR查询频率限制
jvm·数据库·python
2501_901200531 小时前
编写表与字段注释后数据无法保存怎么排查_权限设置与回滚处理
jvm·数据库·python