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

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

相关推荐
覆东流6 小时前
7.Java数组
java·开发语言·后端
程序员小八7776 小时前
MySQL 事务:一文把 ACID、隔离级别、MVCC、锁全串起来
数据库·mysql
threerocks7 小时前
AI 原生软件开发生命周期手册 - 如何借助 AI,逐阶段改造软件开发生命周期
前端·javascript·后端
lzhdim7 小时前
提高 SQL 语句执行速度的方法
java·开发语言·数据库·sql·oracle
JavaPub-rodert7 小时前
王仕宇在 Go Context 如何解决协程泄漏与超时控制
开发语言·后端·golang·iphone·javapub·王仕宇
ltl7 小时前
ClickHouse Distributed 引擎与分布式查询路由
数据库
Wang's Blog7 小时前
PostgreSQL笔记60: 权限与角色管理——从层级体系到最佳实践
数据库·笔记·postgresql
深漂的华哥7 小时前
Ruoyi-Plus前后端分离场景下,数据加密传输
java·spring boot·后端·开源·maven·ruoyi
whcyhhh8 小时前
头歌实践教学平台:大数据存储2023(六)
大数据·数据库·python
why技术9 小时前
eli5,我觉得这个全网在吹的技能,使用体验真的很一般啊。
前端·人工智能·后端