后浪笔记一零二四

pg为何需要HOT

  • 堆表是数据库中一种没有聚集索引的表类型,数据行按照插入顺序存储,类似于堆叠存放。
  • 数据结构中的堆是一种特殊的完全二叉树,满足堆序性。
  • 内存堆是操作系统或编程语言运行时管理动态内存分配的区域。
  • PG的ctid是真正意义上的物理地址,由页号和行偏移量组成,格式如(0,1)

Primary, Secondary, and Clustered Indexes

主键索引:

  • 主键索引,用于唯一标识表中的每一行数据。
  • 在许多数据库系统中,尤其是那些默认将主键作为聚集索引的系统(如MySQL的InnoDB),主键索引必是聚集索引。

二级索引:

  • 二级索引必是非聚集索引(Non-Clustered Index),即它指向的是主键(例如InnoDB)或数据行的指针(例如PG)。
  • 在 PostgreSQL 中,所有索引在技术上都被认为是“二级索引”,因为它们与表的主数据区域(称为堆)是分开存储的。

聚集索引(Clustered Index):

  • 定义:聚集索引决定了表中数据的物理存储顺序。这意味着表的数据行按照聚集索引键值的顺序进行排序并存储。
  • 特点:一个表只能有一个聚集索引,因为数据只能以一种方式物理排序。

堆表和索引组织表

堆表无序的,插入很快,哪里有空哪里插。堆表数据库没有聚集索引。

索引组织表是有序的,插入时为了保证b树是平衡的,需要不停渲染它。索引组织表数据库每张表必有一个聚集索引,如果没有就会自动创建一个。

  1. 堆表方案 Heap Table

堆表方案,由于索引是独立于数据存储的,因此可以有多个不同的索引服务于同一个表的不同列或组合列。

  • PostgreSQL 支持多种类型的索引,包括 B-tree、Hash、GiST、SP-GiST、GIN 和 BRIN 索引。因为支持多种索引,所以加锁和mvcc的实现都非常复杂。每行有多个版本,相当于有多个CTID,为了保证CTID不丢,pg的索引需要维护这些CTID信息。
  • mysql InnoDB只支持B-tree索引。InnoDB的二级索引不直接包含版本信息,但通过主键值间接支持MVCC。当通过二级索引访问数据时,需要先找到主键,再通过主键索引访问行数据并判断可见性。

PostgreSQL 使用堆表结构,数据行(元组)以无序方式存储在堆页面中,每个元组通过 CTID(物理地址:页号+行号) 唯一标识,索引直接指向 CTID。

更新问题:当一行被更新时,会生成新版本元组(MVCC 机制),旧元组保留(用于并发事务),当新元组与原元组不在同一页面时,新元组的 CTID 可能与旧元组不同,这时候所有索引都需要更新指向新的 CTID。

HOT更新(Heap-Only Tuples update):CTID发生变化之后,继续让索引指向旧的CTID,而旧CTID指向的旧元组指向新元组的CTID,然后通过新旧元组之间的链表来找到最新版本。

HOT触发条件包括:

  • 新旧元组必须在同一页
  • 更新的列不涉及任何索引字段
  1. 索引组织表方案 Index Organized Table,IOT

支持覆盖索引。

无序主键,性能会贼差,因为b树是有序的,插入无序的主键时,数据库为了保证b树是平衡的会不停的渲染它。

只有IOT方案有索引覆盖和索引下推的概念,因为只有这个方案有回表这个概念,而索引覆盖和索引下推都是为了避免回表或者减少回表次数的。

InnoDB的回表就是回主键索引。

  1. 对比

堆表,就是一个堆,一个劲儿往里面扔,这就没有顺序了。那没有顺序,排序怎么解决?索引本身就是有序的,行数据有序是没有意义的。

mysql的主键是在b+树上排好序的,但是堆表咣咣往里扔是无序的,然后拿着地址在堆里找。

堆表有个好处,它不排序,它插入就很快,因为它不涉及到排序,就不需要往中间插。

像mysql,为了保证有序性,需要在1和3之间插入2,搞不好还会有页分裂。没有空间了,又要插进去,页得分裂。16k的页得分俩。页分裂之后会有空余问题,导致空间浪费。

堆表就是哪有空余就往哪里插,不用管顺序,所以insert非常快。

  1. 更新导致的写放大问题:

在 Postgres 的底层设计中,它的行数据是不可修改的,每个不可修改的行都叫做“元组”,每个唯一的元组都由一个唯一的 ctid 标识。

虽然历史版本会占用空间,但PostgreSQL的旧版本仅存储变更字段的增量信息。

不停的update会产生很多的垃圾。所以pg有vacuum命令来执行垃圾回收。

InnoDB 的行数据是可以修改的,虽然可修改但是也有写放大问题:

  • InnoDB 是以数据页的形式组织数据的,Linux 上默认数据页的大小是 16K。
  • 当你更改了一条记录时,最终会把这条记录所在的数据页整页刷回磁盘,你可能只是改了一个小字段,也许只有 4 个字节,可是最终却会导致 16K 字节的写入。

update: pg和InnoDB五五开

insert: pg的插入性能远高于InnoDB

  • 插入同样多的数据,InnoDB花费的时间是postgres的10倍以上。
  • 插入同样多的数据,InnoDB数据目录占用的空间是postgres的好几倍,因为mysql插入时需要写入好几份数据,binlog和redo log 和 todo log都有对应的数据。
  • InnoDB采用“索引组织表”结构,当前版本存储在表空间(.ibd文件),历史版本存储在独立的回滚段(undo log)中。每次更新会生成完整的前像(undo log),即使只修改少量字段

select: pg的查询速度也比InnoDB快,因为pg第二索引直接指向元组的 ctid,而InnoDB的第二索引去读数据会经历“第二索引——主键——数据”的过程。

  1. mysql有三层B+树索引可以存多少行数据的概念,为何 postgres 没有这个概念呢

基于1024分支因子的计算​​:

  • 假设每个索引项占14B(6B主键+8B指针),16KB页可存1024个索引项
  • 三层结构:1个根节点 → 1024个二级节点 → 1024×1024=1,048,576个叶子节点
  • 每个叶子节点存1024行数据(假设每行数据16B),总容量可达约10亿行
  • 当超过10亿行,B+树会增加到四层

基于1170分支因子的计算​​:

  • 假设每个索引项占16B(8B主键+8B指针),16KB页可存1170个索引项
  • 三层结构:1个根节点 → 1170个二级节点 → 1170×1170=1,368,900个叶子节点
  • 每个叶子节点存16行数据(假设每行数据1KB),总容量可达约1170×1170×16=21,902,400行(约2000万行)
  • 当超过2000万行,B+树会增加到四层

所以,三层B+树索引可以存多少行数据受如下因素控制:

  • 页大小(通常16KB)
  • 每行数据大小(行越大,每页存储的行数越少)
  • 索引项大小(由主键长度和指针长度决定)

为什么PostgreSQL没有"三层B+树"这个概念:

  • PostgreSQL使用堆表(heap tables)存储数据,索引只是指向堆元组的指针,不直接包含数据
  • 所以,pg没有回表的概念,其索引的叶子节点存放的是CTID,CTID到数据行的时间复杂度是O(1),速度贼快
  • 而InnoDB是索引组织表,数据行都在主键索引的叶子节点上,二级索引必须经过主键索引才能找到数据。一旦主键索引树超过三层,其查询效率就会贼低。
  1. InnoDB 和 PG 全表扫描的对比

InnoDB全表扫描会走主键索引么?

  • 全表扫描实际上是​​从聚簇索引的第一个叶子节点开始,依次遍历所有记录​​,直到扫描完最后一个叶子节点。

PG每行数据就有多个版本,全表扫描的时候,会不会都扫出来呢?

  • PostgreSQL每行数据都包含几个隐藏的系统列:
    • xmin:创建该行版本的事务ID
    • xmax:删除/锁定该行版本的事务ID(初始为0)
    • cmin/cmax:事务内的命令标识符
    • ctid:行版本在表中的物理位置
  • 全表扫描时,PostgreSQL会检查每一行数据的系统列(xmin和xmax)来判断该行版本是否对当前事务可见:
    • 如果xmin未提交或晚于当前事务快照 → 不可见
    • 如果xmax已提交且早于当前事务快照 → 不可见(已删除)
    • 否则可见
  • 全表扫描会遍历表中的所有行,但只会返回对当前事务可见的版本

MVCC实现差异

PostgreSQL:堆内多版本

  • 多版本数据直接存储在堆表中,旧元组可能长期存在,索引需支持通过 CTID 找到所有可见版本。
  • HOT 通过在同一页面内维护版本链,减少索引对多版本的支持开销。
  • 使用​​事务ID(txid)​​标识版本,每个事务开始时获取活跃事务列表形成快照,通过比较xmin、xmax与快照判断可见性
  • 事务ID为32位整数,可能因耗尽引发“事务ID回卷”问题,需定期执行FREEZE操作

MySQL InnoDB:Undo 日志与聚集索引

  • 多版本数据通过 Undo 日志管理,聚集索引中仅存储最新版本。
  • 二级索引无需处理多版本,事务通过 ReadView 判断可见性,结合主键回表查询 Undo 日志。

innodb中 redo undo binlog 这三种日志的区别

binlog(二进制日志)

  • 定义​​:binlog是MySQL服务器层的​​逻辑日志​​,记录所有对数据库的修改操作
  • 层次​​:MySQL服务器层实现,与存储引擎无关
  • ​​核心作用​​:主从复制、数据恢复和时间点恢复

redo log(重做日志)

  • ​​定义​​:redo log是InnoDB存储引擎特有的​​物理日志​​,用于记录对数据页的物理修改
  • 层次​​:存储引擎层(InnoDB)实现
  • ​​核心作用​​:系统崩溃后,通过redo log重做已提交但未写入数据文件的修改

undo log(回滚日志)

  • 定义​​:undo log是InnoDB存储引擎的​​逻辑日志​​,记录事务修改前的数据状态
  • 层次​​:存储引擎层(InnoDB)实现
  • 核心作用​​:实现事务的原子性(Atomicity)和MVCC(多版本并发控制)
    • 事务回滚​​:当事务失败或主动回滚时,通过undo log将数据恢复到修改前的状态 ​​ * MVCC支持​​:为并发事务提供历史数据版本,实现非锁定读和一致性快照 ​ * ​崩溃恢复​​:系统崩溃时撤销未提交事务的修改

没有完美的方案

oracle是堆表+Undo快照,mysql是索引组织表+Undo快照, postgres是堆表+多版本

堆表方案,不用维护数据行的顺序,数据行按任意顺序排放,但是索引和数据行需要分开存储,并使用CTID来建立它们的联系。CTID就会导致MVCC时新旧元组不在同一页面的问题。

聚集索引方案,所有数据行按照主键顺序排放,主键需要是全局唯一的,这样才能保证通过主键唯一定位到数据行,达到数据行即主键索引的目的。所以InnoDB没有CTID的概念。

PostgreSQL​​的堆表多版本支持灵活数据模型。

综上所述,mysql InnoDB要求所有的表都必须有个主键,但是postgresql可以没有主键。

pg中FOR UPDATE和FOR NO KEY UPDATE的区别

postgres的堆表,根据ctid找到数据行的时间复杂度是多少

ctid由两部分组成:(blockid, itemid),其中:

  • blockid​​:表示数据所在的磁盘块(页)编号,从0开始。
  • itemid​​:表示该块内行的索引位置,从1开始。

由于ctid直接指向数据的物理位置,其查找过程分为两步:

  1. ​​定位块​​:通过blockid直接访问对应的数据块,时间复杂度为 ​​O(1)​​(假设块地址可通过哈希或数组索引直接映射)。
  2. 定位行​​:在块内通过itemid索引行,时间复杂度为 ​​O(1)​​(块内行索引通常为固定偏移量)。

因此,​​理论时间复杂度为 O(1)


专题:

本文发表于 2022-01-01,最后修改于 2022-01-01。

本站永久域名「 jiavvc.top 」,也可搜索「 后浪笔记一零二四 」找到我。


上一篇 « pg高可用 下一篇 » pg常用查询

赞赏支持

请我吃鸡腿 =^_^=

i ysf

云闪付

i wechat

微信

推荐阅读

Big Image